--- status: draft last_updated: 2026-08-27 --- # alkhttp — Overview The HTTP interface crate: serves inbound HTTP on standard ALPNs (with WebSocket upgrade carrying the channels protocol for browser bidirectional access) and hosts the HTTP-backed call-protocol adapters. This document covers the crate's two roles, its dependency edges, and the adapter location map. Component details are in the sibling documents. ## What alkhttp is the HTTP protocol handler crate for the alk stack. It serves two roles in one crate ([ADR-039](decisions/039-http-server-and-client-host-colocated.md)): 1. **HTTP server** — a `ProtocolHandler` (`HttpAdapter`) that accepts HTTP/2 and HTTP/1.1 connections on the standard IANA ALPNs (`h2`, `http/1.1`), plus WebSocket upgrade (for browser bidirectional access to the call protocol). It serves REST APIs, the `to_openapi`/`to_mcp` projections of local call-protocol operations, the `/healthz` operational endpoint, and the decoy surface for stealth mode. The WS path carries the **channels protocol** ([ADR-067](decisions/067-websocket-carries-channels.md)): the 8-byte chunk header multiplexes N channels over the WS connection; channel 0 is pre-negotiated as `alk/call` and runs the shared `Dispatcher`. 2. **HTTP client host** — the home of the HTTP-transport-backed call adapters: `from_openapi` (import external HTTP APIs described by an OpenAPI document, using `reqwest` for outbound calls), `from_mcp` (import remote MCP tools over streamable HTTP, using `reqwest`), `from_jsonschema` (import a single non-OpenAPI HTTP endpoint), and `from_wss` (import a remote alk node's operations over a WSS channels connection — same-protocol importer). The reverse projections `to_openapi` (generate an OpenAPI doc from the local registry's `External` operations) and `to_mcp` (expose local ops as MCP tools over streamable HTTP, using `axum`) also live here. Both directions share the same HTTP dependencies (`axum` for serving, `reqwest` for calling out), which is why they live in one crate rather than being split into a server crate and a client crate. ## Why The crate's purpose is to be the HTTP interface library for downstream crates that need to expose an HTTP interface. A downstream consumer (a hub deployment, a browser-facing service, the alknet assembly layer) wires `HttpAdapter` into the handler registry for the standard HTTP ALPNs and gets a full HTTP surface: the gateway projection of the call protocol, OpenAPI discovery, MCP tool exposure, and WebSocket for browser bidirectional access to both the call protocol and data channels. ### The producer/consumer model The call protocol is bidirectional — both sides can initiate calls — and the channels protocol is symmetric — either side can open channels. **Producer** and **consumer** therefore describe *what a side does on a given operation or channel*, not what it is as a connection endpoint: - A **producer** (call) registers operations on an `OperationRegistry` and runs a `Dispatcher`; a **consumer** (call) uses a `CallConnection` to invoke them. On one connection both sides can be producers of some ops and consumers of others. - A **producer** (channels) accepts data channels (registers openable ALPNs); a **consumer** (channels) opens them via the per-ALPN open ops on channel 0. Both sides can open and accept on the same connection. The HTTP surface maps onto this model as follows: - The `HttpAdapter` (gateway + WS upgrade) is the **producer-facing surface**: it runs the shared `Dispatcher` on channel 0 for WS sessions, and the gateway dispatches into the local registry for HTTP requests. A browser or HTTP client is a **consumer** of those ops. - The WS session is fully bidirectional: the browser side can also be a producer (registering ops via the connection-local Layer 2 overlay, opening channels), making the hub a consumer of browser ops on the same connection. - The `from_*` adapters are **consumer-side**: they import external surfaces (OpenAPI, MCP, WSS) into the local registry as ops the local process can then consume or compose. - The `to_*` adapters are **producer-side projections**: they expose the local registry's ops through foreign protocols (OpenAPI, MCP). Connection direction (who opened the HTTP/TCP connection) is independent of these roles, exactly as in the call protocol. A note on the "from/to" direction model: the `from_openapi`/`to_openapi` and `from_mcp`/`to_mcp` adapters are *inherently directional* because OpenAPI and MCP are client/server protocols — one side serves, the other calls. That directionality is a property of those protocols, not of the call protocol itself. The call protocol is bidirectional. The HTTP/1.1 + HTTP/2 surface inherits HTTP's request/response constraint and projects the call protocol one-directionally (consumer→producer calls only). **WebSocket restores the call protocol's native bidirectionality for browsers** — and now carries the channels protocol besides it. ## Dependencies ``` alkhttp ├── alkcall (call + channels protocol, vendored core types, │ registry, adapter contract — the sole alk dep) ├── axum (HTTP server — Router, extractors, middleware, WebSocket upgrade) ├── hyper / hyper-util(hTTP/1.1 + HTTP/2 framing; axum is built on hyper) ├── reqwest (HTTP client — from_openapi/from_mcp forwarding) │ + reqwest-middleware, reqwest-retry ├── tokio-tungstenite (WSS client — from_wss; behind the `wss` feature) ├── yaml_serde (YAML parse for from_openapi YAML input — ADR-051) ├── openapiv3 (OpenAPI doc model for from_openapi/to_openapi) └── rmcp (MCP streamable HTTP — feature-gated behind `mcp`) ``` > **Note:** the `h3`/WebTransport dependency is **not** in the > dependency tree and is not planned — WebTransport is out of scope in > alkhttp ([ADR-069](decisions/069-webtransport-out-of-scope.md)); when > a browser needs WebTransport it will be provided by the alknet layer, > which owns transports. The browser bidirectional path is WebSocket > (native axum support, no new dependency). ### The alkcall dependency ([ADR-003](decisions/003-crate-decomposition.md) Amendment 1) alkhttp depends on alkcall. The old rule "no handler crate depends on another handler crate" survives as "no HTTP adapter depends on another transport adapter": alkcall is the protocol-foundation crate (it owns the vendored core types — `Connection`, `ProtocolHandler`, `BiStream`, `AuthContext`, `IdentityProvider`, `Capabilities`, `OwnershipProvider`, `HandlerError`, `StreamError` — plus the registry, dispatch, and wire format). alkhttp depending on alkcall is "HTTP uses the call protocol types," not "HTTP depends on another transport." alkcall stays transport-agnostic — no `reqwest`, no `axum`, no HTTP dependencies. The `from_openapi`/`from_mcp`/`from_jsonschema`/`from_wss` forwarding handlers are opaque `Arc`-shaped registrations from the registry's perspective: constructed by alkhttp at registration time, stored in `HandlerRegistration`, dispatched by the `Dispatcher` which doesn't know `reqwest` or `tokio-tungstenite` is involved. ## ALPNs | ALPN | Handler | Transport | Browser? | |------|---------|-----------|----------| | `http/1.1` | `HttpAdapter` | HTTP/1.1 over a `BiStream` (+ WS upgrade) | Yes (WS upgrade for bidirectional) | | `h2` | `HttpAdapter` | HTTP/2 over a `BiStream` (+ WS upgrade) | Yes (WS upgrade for bidirectional) | | `h3` | — | — | Out of scope ([ADR-069](decisions/069-webtransport-out-of-scope.md)); an alknet concern | These are standard IANA ALPN strings, not `alk/`-prefixed. Any HTTP client connects without knowing about the alk stack — the TLS handshake negotiates `h2` or `http/1.1` normally, and the `HttpAdapter` serves HTTP. This is the stealth mapping ([ADR-010](decisions/010-alpn-router-and-endpoint.md)). The `HttpAdapter` registers for `http/1.1` and `h2`. The endpoint's handler registry maps each ALPN to the same adapter instance; the handler branches on `connection.remote_alpn()` to pick the framing. WebSocket upgrade rides on either HTTP version (RFC 6455 over HTTP/1.1; extended CONNECT over HTTP/2). ## Adapter Location Map The decomposition principle: the adapter trait lives where the types live (alkcall); the adapter implementations live where their transport dependencies live (alkhttp). ``` alkcall (transport-agnostic — no HTTP client, no HTTP server) ├── OperationAdapter trait (the contract — async) ├── from_call (same-protocol importer over a call connection) ├── CallConnection / CallClient (outbound call surface) └── ChannelsAdapter / ChannelClient (channels protocol, both sides) alkhttp (owns HTTP server + HTTP client + WSS client) ├── HttpAdapter (axum server — inbound HTTP on h2/http1.1 + WS upgrade route) ├── [WS upgrade → channels session] (not an adapter — hands the WS byte stream to the │ channels machinery; see websocket.md, ADR-067) ├── from_openapi (parse OpenAPI doc + reqwest forwarding handler) ├── from_jsonschema (single-endpoint reqwest forwarding handler — ADR-066) ├── to_openapi (generate OpenAPI doc from local registry — the gateway projection) ├── from_mcp (feature-gated) (import remote MCP tools over streamable HTTP — reqwest) ├── to_mcp (feature-gated) (expose local ops as MCP tools over streamable HTTP — axum) └── from_wss (feature-gated) (import a remote node's ops over WSS — ADR-070) ``` alkcall never sees the HTTP client. The forwarding handlers are opaque from the registry's perspective. alkcall stays lean; alkhttp owns all HTTP directions and the WSS consumer. ## Feature Gates ```toml [features] default = ["h2", "http1"] # the HTTP surface (incl. WebSocket upgrade for browsers) mcp = ["dep:rmcp"] # from_mcp / to_mcp (streamable HTTP only — ADR-037) wss = ["dep:tokio-tungstenite"] # from_wss consumer adapter (ADR-070) ``` - `h2` + `http1` (default): the `axum` + `hyper` HTTP/1.1 + HTTP/2 server, including WebSocket upgrade for browser bidirectional access. This is the surface all clients — including browsers, via WS upgrade — use. - `mcp`: the `rmcp` dependency with streamable HTTP transport features only. Adds `from_mcp`/`to_mcp`. See [http-mcp.md](http-mcp.md) and [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md). - `wss`: the `tokio-tungstenite` dependency. Adds `from_wss`. See [ADR-070](decisions/070-from-wss-consumer-adapter.md). **`yaml_serde` is not feature-gated** — YAML OpenAPI is a first-class input format (some providers publish YAML-only schemas), not an edge case ([ADR-051](decisions/051-yaml-input-for-from-openapi.md) §3). ## The No-Env-Vars Invariant The `from_openapi`/`from_mcp`/`from_jsonschema`/`from_wss` forwarding handlers are the **credential injection point** for the no-env-vars architecture. The path: ``` vault (alkvault) → assembly layer → Capabilities → HandlerRegistration.capabilities → OperationContext.capabilities → handler reads context.capabilities.get("") → injects into outbound HTTP Authorization header (or WSS auth) ``` This makes any downstream `std::env::var("OPENAI_API_KEY")` read unreachable — the assembly layer never calls `Default::default()` on a provider; it constructs them with vault-derived credentials, or routes outbound calls through adapter operations that carry the credential in `Capabilities`. **This is a spec-level invariant**: no handler reads outbound credentials from any source other than `OperationContext.capabilities`. All adapter implementations in alkhttp are verified against this invariant. See [ADR-014](decisions/014-secret-material-flow-and-capability-injection.md). ## Architecture (component pointers) - **[http-server.md](http-server.md)** — the `HttpAdapter` for `h2`/ `http/1.1` (+ the WS upgrade route): how axum is run over a `BiStream`, Bearer auth resolution, the `/healthz` raw route, stealth decoy, the 6-endpoint gateway dispatch, and the WS upgrade route (which hands off to the channels session). - **[websocket.md](websocket.md)** — the WebSocket browser bidirectional path: the channels protocol over WS, framing via the WS↔byte-stream adapter, channel 0 and the shared `Dispatcher`, bidirectionality, connection-local Layer 2 overlay, the browsers-are-not-peers rationale, streaming (native, no SSE). - **[http-adapters.md](http-adapters.md)** — `from_openapi`, `from_jsonschema`, `to_openapi`, and `from_wss`. Error fidelity per [ADR-023](decisions/023-operation-error-schemas.md). - **[http-mcp.md](http-mcp.md)** — `from_mcp`/`to_mcp` (feature-gated), streamable HTTP only (ADR-037), the rmcp integration. ## Design Decisions | Decision | ADR | Summary | |----------|-----|---------| | HTTP-to-call operation mapping | [ADR-036](decisions/036-http-to-call-operation-mapping.md) | ~~Direct path mapping~~ — **routing superseded by ADR-047**; non-routing clauses survive (SSE, auth, `/healthz`, stealth, error mapping) | | MCP stdio transport exclusion | [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built (RCE vector) | | WebSocket carries the channels protocol | [ADR-067](decisions/067-websocket-carries-channels.md) | WS = in-line channels substrate; channel 0 = `alk/call`; upgrade path `/alk/channels`; new in alkhttp | | Gateway `/publish` endpoint | [ADR-068](decisions/068-gateway-publish-endpoint.md) | 6th gateway endpoint for `Pub` ops; NDJSON request body → `call.published` chunks; new in alkhttp | | WebTransport out of scope | [ADR-069](decisions/069-webtransport-out-of-scope.md) | h3/WebTransport removed from alkhttp scope; an alknet concern; supersedes ADR-044's deferral framing | | `from_wss` consumer adapter | [ADR-070](decisions/070-from-wss-consumer-adapter.md) | Same-protocol importer over WSS; `wss` feature; new in alkhttp | | WS carries the native session, not the gateway shape | [ADR-048](decisions/048-websocket-native-session-not-gateway.md) | Amended by ADR-067: channel 0 carries the native call session; gateway endpoints are HTTP-only | | HTTP server + client host colocated | [ADR-039](decisions/039-http-server-and-client-host-colocated.md) | One crate for server + adapters (shared HTTP deps, shared mapping) | | `to_mcp` tool-gateway pattern | [ADR-041](decisions/041-mcp-tool-gateway-pattern.md) | 4 fixed gateway tools (search/schema/call/batch); Sub and Pub excluded | | `to_openapi` gateway pattern | [ADR-042](decisions/042-openapi-gateway-pattern.md), [ADR-047](decisions/047-remove-direct-call-http-surface.md) | Fixed gateway endpoints are the sole HTTP invoke path (no per-operation `POST /{service}/{op}`); per-caller AccessControl-filtered `/search` is the discovery; extended with `/publish` (ADR-068) | | Assembly-layer custom HTTP routes | [ADR-046](decisions/046-assembly-layer-custom-http-routes.md) | `extra_routes: Option` at construction; default surface takes precedence on collision | | alkhttp is protocol-foundation-dependent | [ADR-003](decisions/003-crate-decomposition.md) Am. 1 | alkhttp depends on alkcall alone (types, not a peer transport) | | Bearer auth via `resolve_from_token` | [ADR-004](decisions/004-auth-as-shared-core.md) | HTTP handler credential source + resolution (settled; alkcall ADR-003 owns the mechanism) | | Stealth mode = HTTP handler on standard ALPNs | [ADR-010](decisions/010-alpn-router-and-endpoint.md) | Decoy for unknown paths (settled) | | Adapter-registered ops are `Internal` | [ADR-015](decisions/015-privilege-model-and-authority-context.md) | `from_openapi`/`from_mcp`/`from_jsonschema`/`from_wss` produce `Internal` leaves (record: alkcall ADR-017) | | `OperationAdapter` trait is async | [ADR-017](decisions/017-call-protocol-client-and-adapter-contract.md) | HTTP adapters implement the async trait (record: alkcall ADR-022) | | `to_*` adapters are projections | [ADR-017](decisions/017-call-protocol-client-and-adapter-contract.md) | `to_openapi`/`to_mcp` consume the registry, don't produce entries (record: alkcall ADR-022) | | Error schema fidelity | [ADR-023](decisions/023-operation-error-schemas.md) | `from_openapi` maps HTTP status → `HTTP_` codes; `to_openapi` projects back (record: alkcall ADR-016) | | Browsers require X.509 | [ADR-027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | Browser-facing TLS uses X.509 (TLS provisioning itself is an alknet concern) | | Browsers are not alk peers | [ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 | Browser over WS = bearer token, no `PeerId` | ## Open Questions See [open-questions.md](open-questions.md) for full details. - **OQ-01** (resolved): WS message ↔ byte-stream adaptation — the production adapter (`src/websocket/byte_adapter.rs`) is validated in both directions (server upgrade path + `from_wss` client). - **OQ-02** (resolved): `/publish` body framing — first line `{operation, chunk}`; terminal errors as plain HTTP status + JSON body (ADR-068). - **OQ-03** (open): `from_wss` reconnection semantics — v1 = drop → retryable failures (ADR-070); policy deferred to the assembly layer. ## References - The alkcall crate — `docs/architecture/` for the call and channels protocol decisions; `README.md` §"Roles and Composition" for the producer/consumer role definitions. - The alkvault crate — secrets, feeding `Capabilities` at the assembly layer. - The alknet mono-repo — endpoint/transport concerns and the source specs this crate was ported from.