Full-surface integration suite (tests/full_surface.rs, mcp feature): - one HttpAdapter over real TCP (ProtocolHandler::handle path) serving gateway endpoints, /openapi.json, /mcp, and the WS channels session - gateway: search/schema/call/subscribe/batch/publish presence, envelope shapes, error fidelity end-to-end - from_openapi import -> Internal-by-default invisible from the wire -> External facade composes it via env.invoke -> upstream HTTP API called end-to-end (ADR-015 composition model exercised) - to_openapi 6-path doc validated against openapiv3 over the wire - to_mcp: MCP client connects to /mcp on the served adapter, lists the 4 gateway tools, search returns ACL-filtered ops (Sub excluded) Production fix: the WS upgrade route was reserved but never wired into HttpAdapter's router (the ws-upgrade-session tests built their own router). Now wired with ws_bearer_auth (401 without a resolvable token) around ws_upgrade_handler. Docs sync: all 28 'Port notes' sections/blockquotes stripped from ported ADRs/specs; OQ-01/OQ-02 statuses corrected to resolved in overview.md, websocket.md, and the README table (open-questions.md was already current). Publish prep: cargo publish --dry-run --allow-dirty succeeds; cargo doc --no-deps warning-free (ADR link targets fixed); feature combinations (default / test-support / mcp / wss / all) compile warning-free under clippy -D warnings. Verified: cargo test (182 lib default), --all-features (227 lib + 29 integration), clippy -D warnings x3 feature sets, fmt, doc, publish --dry-run.
18 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 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):
- 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, theto_openapi/to_mcpprojections of local call-protocol operations, the/healthzoperational endpoint, and the decoy surface for stealth mode. The WS path carries the channels protocol (ADR-067): the 8-byte chunk header multiplexes N channels over the WS connection; channel 0 is pre-negotiated asalk/calland runs the sharedDispatcher. - HTTP client host — the home of the HTTP-transport-backed call
adapters:
from_openapi(import external HTTP APIs described by an OpenAPI document, usingreqwestfor outbound calls),from_mcp(import remote MCP tools over streamable HTTP, usingreqwest),from_jsonschema(import a single non-OpenAPI HTTP endpoint), andfrom_wss(import a remote alk node's operations over a WSS channels connection — same-protocol importer). The reverse projectionsto_openapi(generate an OpenAPI doc from the local registry'sExternaloperations) andto_mcp(expose local ops as MCP tools over streamable HTTP, usingaxum) 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
OperationRegistryand runs aDispatcher; a consumer (call) uses aCallConnectionto 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 sharedDispatcheron 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); 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 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<dyn Handler>-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); 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).
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
[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): theaxum+hyperHTTP/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: thermcpdependency with streamable HTTP transport features only. Addsfrom_mcp/to_mcp. See http-mcp.md and ADR-037.wss: thetokio-tungstenitedependency. Addsfrom_wss. See ADR-070.
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 §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("<service>")
→ 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.
Architecture (component pointers)
- http-server.md — the
HttpAdapterforh2/http/1.1(+ the WS upgrade route): how axum is run over aBiStream, Bearer auth resolution, the/healthzraw route, stealth decoy, the 6-endpoint gateway dispatch, and the WS upgrade route (which hands off to the channels session). - 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 —
from_openapi,from_jsonschema,to_openapi, andfrom_wss. Error fidelity per ADR-023. - 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 | /healthz, stealth, error mapping) |
| MCP stdio transport exclusion | ADR-037 | Streamable HTTP only; stdio is not built (RCE vector) |
| WebSocket carries the channels protocol | ADR-067 | WS = in-line channels substrate; channel 0 = alk/call; upgrade path /alk/channels; new in alkhttp |
Gateway /publish endpoint |
ADR-068 | 6th gateway endpoint for Pub ops; NDJSON request body → call.published chunks; new in alkhttp |
| WebTransport out of scope | ADR-069 | h3/WebTransport removed from alkhttp scope; an alknet concern; supersedes ADR-044's deferral framing |
from_wss consumer adapter |
ADR-070 | Same-protocol importer over WSS; wss feature; new in alkhttp |
| WS carries the native session, not the gateway shape | ADR-048 | Amended by ADR-067: channel 0 carries the native call session; gateway endpoints are HTTP-only |
| HTTP server + client host colocated | ADR-039 | One crate for server + adapters (shared HTTP deps, shared mapping) |
to_mcp tool-gateway pattern |
ADR-041 | 4 fixed gateway tools (search/schema/call/batch); Sub and Pub excluded |
to_openapi gateway pattern |
ADR-042, ADR-047 | 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 | extra_routes: Option<Router> at construction; default surface takes precedence on collision |
| alkhttp is protocol-foundation-dependent | ADR-003 Am. 1 | alkhttp depends on alkcall alone (types, not a peer transport) |
Bearer auth via resolve_from_token |
ADR-004 | HTTP handler credential source + resolution (settled; alkcall ADR-003 owns the mechanism) |
| Stealth mode = HTTP handler on standard ALPNs | ADR-010 | Decoy for unknown paths (settled) |
Adapter-registered ops are Internal |
ADR-015 | from_openapi/from_mcp/from_jsonschema/from_wss produce Internal leaves (record: alkcall ADR-017) |
OperationAdapter trait is async |
ADR-017 | HTTP adapters implement the async trait (record: alkcall ADR-022) |
to_* adapters are projections |
ADR-017 | to_openapi/to_mcp consume the registry, don't produce entries (record: alkcall ADR-022) |
| Error schema fidelity | ADR-023 | from_openapi maps HTTP status → HTTP_<status> codes; to_openapi projects back (record: alkcall ADR-016) |
| Browsers require X.509 | ADR-027 | Browser-facing TLS uses X.509 (TLS provisioning itself is an alknet concern) |
| Browsers are not alk peers | ADR-034 §4 | Browser over WS = bearer token, no PeerId |
Open Questions
See 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_wssclient). - OQ-02 (resolved):
/publishbody framing — first line{operation, chunk}; terminal errors as plain HTTP status + JSON body (ADR-068). - OQ-03 (open):
from_wssreconnection 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
Capabilitiesat the assembly layer. - The alknet mono-repo — endpoint/transport concerns and the source specs this crate was ported from.