First speccing pass for alknet-http (HTTP interface crate: h2/http1.1/h3 server + from_openapi/to_openapi/from_mcp/to_mcp adapters). Specs (crates/http/): - README.md, overview.md — crate index, two-roles-in-one-crate framing, adapter location map, feature gates (h3, mcp), no-env-vars invariant - http-server.md — HttpAdapter for h2/http1.1, axum over QUIC stream, Bearer auth, SSE projection for subscriptions, /healthz, stealth decoy - http-adapters.md — from_openapi (reqwest) and to_openapi (projection), error fidelity (HTTP_<status> per ADR-023), type definitions - http-mcp.md — from_mcp/to_mcp (feature-gated), streamable-HTTP-only - webtransport.md — h3/WebTransport handler, browser streaming path, HTTP/3 request vs WebTransport session distinguished at framing layer ADRs: - ADR-036 HTTP-to-Call Operation Mapping (Proposed) — direct path mapping; to_openapi is projection, not router (the load-bearing one-way door from Phase 0 DH-3) - ADR-037 MCP Stdio Transport Exclusion (Proposed) — streamable HTTP only; stdio is not built (RCE-vector security position) - ADR-038 HTTP/3 and WebTransport as First-Class HTTP Transports (Proposed) — corrects the Phase 0 DH-2 deferral framing; h3 is in scope, not deferred, per ADR-009 §'What this framework is NOT' - ADR-039 HTTP Server and Client Host Colocated in alknet-http (Proposed) — one crate for server + client host (shared HTTP deps, shared operation-spec->HTTP mapping) - ADR-003 Amendment 1 — clarifies alknet-call is a protocol-foundation crate (the alknet-http -> alknet-call dependency edge) Open questions (OQ-38, OQ-39, OQ-40 added under 'Theme: alknet-http'): - OQ-38 WebTransport relay-as-proxy scope (genuine scope question, not a deferral — the decision is made when the use case becomes concrete) - OQ-39 to_openapi published-spec versioning (one-way after first publication) - OQ-40 reqwest client config and connection pooling (two-way-door) Architecture README and overview updated with doc table, ADR table (036-039), current-state note, and crate graph (alknet-http -> alknet-call edge). Reviewed by architecture-reviewer subagent: 3 critical, 4 warning, 5 suggestion issues found and fixed (missing ADR-039, WebTransport stream routing conflation, undefined types, stale OQ-37 deferral language, README OQ table completeness, Bearer-only attribution, cross-references, ADR-038 ALPN quote, feature-gate placeholder, MCP temporal language).
14 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-29 |
HTTP Server
The HttpAdapter — the ProtocolHandler for h2 and http/1.1 (and
h3, covered in webtransport.md). This document
covers how axum is run over a QUIC bidirectional stream, Bearer auth
resolution, the HTTP-to-call dispatch, the /healthz raw route, and
stealth decoy.
What
The HttpAdapter is constructed by the assembly layer with an
Arc<dyn IdentityProvider> (constructor injection, same pattern as
SshAdapter — see auth.md) and an
Arc<OperationRegistry> (for dispatching HTTP requests to call-protocol
operations). It implements ProtocolHandler for the standard HTTP ALPNs.
pub struct HttpAdapter {
identity_provider: Arc<dyn IdentityProvider>,
registry: Arc<OperationRegistry>,
/// The default handler for paths that are not registered operations
/// (stealth decoy). Configurable: a static site, a fake 404, a
/// redirect. Two-way-door default (ADR-010).
decoy: DecoyConfig,
}
/// The stealth decoy surface for paths that are not registered
/// operations (and not `/healthz`, `/openapi.json`, or the MCP route).
/// Set by the assembly layer at `HttpAdapter` construction. The
/// existence of the decoy path is fixed by ADR-010; the variant is a
/// two-way-door config default.
pub enum DecoyConfig {
/// Serve a fake `404 Not Found` (the default — matches the reference
/// implementation's "fake nginx 404").
NotFound,
/// Serve a static site from a configured directory (the directory
/// path is the payload). For deployments that want a real decoy
/// website.
StaticSite { root: PathBuf },
/// Redirect to a configured URL.
Redirect { to: String },
}
#[async_trait]
impl ProtocolHandler for HttpAdapter {
fn alpn(&self) -> &'static [u8]; // returns the configured ALPN
async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError>;
}
The HttpAdapter registers for multiple ALPNs (http/1.1, h2, h3).
The endpoint's HandlerRegistry maps each ALPN byte string to the same
adapter instance; handle() branches on connection.remote_alpn() to
pick the HTTP framing. For http/1.1 and h2, the framing is hyper's
HTTP/1.1 or HTTP/2 over a QUIC bidirectional stream; for h3, it's the
WebTransport/HTTP/3 path (see webtransport.md).
Why
HTTP is the standard external interface. Browsers, curl, axios, API
gateways, and load balancers all speak HTTP. Serving HTTP on the standard
ALPNs means any HTTP client can connect without knowing about alknet —
the TLS handshake negotiates h2 or http/1.1 normally. This is the
stealth mapping (ADR-010): the HTTP surface is the decoy for clients that
don't offer alknet ALPNs, and the real external API surface for clients
that do know about alknet.
Architecture
Running axum over a QUIC stream
The HttpAdapter::handle() method for h2/http/1.1:
- Accepts one bidirectional stream from the QUIC connection
(
connection.accept_bi()→(SendStream, RecvStream)). - Wraps the
(SendStream, RecvStream)pair as a hyperTokioIo-compatible duplex stream — the same byte stream hyper expects for an HTTP connection. - Constructs the axum
Router(built once at adapter construction, cloned per connection — axumRouterisCloneand cheap to clone). - Hands the duplex stream + the axum router to hyper's connection
driver (
hyper::server::conn::http1::Builderorhttp2::Builder::serve_connection), which reads HTTP frames, parses them, dispatches to axum routes, and writes HTTP responses. - Returns when the HTTP connection closes (the client disconnects or the stream ends).
The axum Router is built once at adapter construction with the
Arc<OperationRegistry> and Arc<dyn IdentityProvider> embedded in its
state; cloning the Router per connection clones the Arcs (cheap,
shared state), so every request handler has access to the registry and
identity provider through the router's state.
The axum Router is the single routing surface for HTTP requests. It
contains:
- The call-protocol projection routes (
POST /{service}/{op}→call.requesteddispatch — ADR-036). GET /healthz(raw route, no auth, no call protocol).GET /openapi.json(serves theto_openapiprojection).- The stealth decoy fallback (unknown paths).
- (Feature-gated)
POST /mcp(theto_mcpstreamable HTTP service — http-mcp.md).
A single HTTP/2 or HTTP/1.1 connection multiplexes multiple requests over the one bidirectional stream (HTTP/2 multiplexing is native; HTTP/1.1 is sequential). The axum router handles each request on a tokio task; the hyper driver manages the connection lifetime.
HTTP-to-call dispatch (ADR-036)
An HTTP request at POST /fs/readFile (or GET /services/list, or any
/{service}/{op} path matching a registered External operation) is
dispatched to the call protocol:
- The axum route handler extracts the operation name from the path
(
/fs/readFile→fs/readFile, stripping the leading slash — the registry form). - It resolves the caller's identity from the
Authorization: Bearerheader viaidentity_provider.resolve_from_token(&AuthToken { raw: token_bytes }). - It parses the request body as the operation input (JSON).
- It constructs the root
OperationContext(caller identity, the registration bundle's capabilities, the connection's env composition) and dispatches through theOperationRegistry::invoke()— the same dispatch path theCallAdapteruses foralknet/callwire requests. - The response (
ResponseEnvelope) is serialized as the HTTP response body (JSON). Errors map to HTTP status codes (see Error Mapping below).
Internal operations (ADR-015) return 404 on the HTTP handler,
matching the call protocol's NOT_FOUND for wire calls to Internal
ops — the HTTP handler dispatches only External operations.
Streaming projection (SSE)
A Subscription operation served over h2/http/1.1 projects its
call.responded stream as Server-Sent Events. The axum route handler:
- Sets
Content-Type: text/event-stream. - For each
call.respondedevent, writes an SSEdata:frame (the event'soutputserialized as JSON). - On
call.completed, closes the SSE stream (normal end). - On
call.aborted, closes the stream with an SSE error event. - On HTTP client disconnect (detected as the response writer closing),
sends
call.abortedfor the in-flight subscription, which cascades to descendants per ADR-016.
This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebTransport
(h3), the subscription projects directly onto a WebTransport
bidirectional stream — no SSE framing (see webtransport.md).
Auth
Inbound HTTP auth is Authorization: Bearer <token>, resolved via
IdentityProvider::resolve_from_token() (the auth.md handler table:
HttpAdapter, Bearer header, resolve_from_token). Bearer-only is the
auth mechanism for the default surface; other HTTP auth schemes (Basic,
API key in query param) are not implemented and would be added as axum
middleware (two-way door). This is recorded in
ADR-036 §Auth;
the resolution mechanism (resolve_from_token) is from
ADR-004, and the
connection-level observability (set_identity) is OQ-11 (resolved).
- Bearer-only is the auth mechanism. Basic auth, API keys in query params, and other HTTP auth schemes are not implemented. A deployment that needs a different auth scheme adds it as axum middleware (two-way door), but the default surface is Bearer-only.
- The
HttpAdapterconstructor-injectsArc<dyn IdentityProvider>, same pattern asSshAdapter. - An unauthenticated request to an operation with
AccessControlrestrictions returns401(no token) or403(token present but insufficient scopes). The call protocol'sFORBIDDENprotocol code maps to403;NOT_FOUND(Internal op) maps to404. - The HTTP handler stores the resolved identity on the
Connectionfor observability (connection.set_identity(identity)), same as the call protocol handler.
Error Mapping
Call-protocol CallError codes (ADR-023) map to HTTP status codes:
Call code |
HTTP status | Notes |
|---|---|---|
NOT_FOUND (operation not registered, or Internal op) |
404 |
|
FORBIDDEN (insufficient scopes, or unauthenticated) |
401 (no token) / 403 (token present) |
|
INVALID_INPUT (schema mismatch) |
422 |
|
TIMEOUT |
504 |
retryable: true |
INTERNAL |
500 |
|
Operation-level domain code with http_status (ADR-023) |
the declared http_status |
from_openapi-imported ops carry the original status |
Operation-level domain code without http_status |
500 |
The retryable field from CallError maps to an HTTP Retry-After
hint for 503/429-class errors. The mapping is a two-way-door
default (the exact status for ambiguous codes can be refined
additively); the one-way constraint is that protocol-level and
operation-level codes are distinct (ADR-023) and from_openapi-imported
codes are prefixed HTTP_<status> to avoid collision with protocol
codes.
/healthz (raw route)
GET /healthz is a raw HTTP route outside the call protocol — no auth,
no operation registration, no OperationContext. It returns 200 OK
with a plain-text body (e.g., "ok") if the endpoint is healthy. This
is the infrastructure endpoint load balancers and orchestrators call;
it must work before identity is resolvable.
Other operational endpoints (metrics, dashboard) are call-protocol
operations if built (/metrics/list, /dashboard/view), not raw HTTP
routes. healthz is the one exception. See ADR-036.
Stealth decoy
For paths that are not registered operations (and not /healthz,
/openapi.json, or the MCP route), the HTTP handler serves a decoy. The
decoy is configurable (DecoyConfig):
- A fake
404 Not Found(the default — matches the reference implementation's "fake nginx 404"). - A static site (served from a configured directory).
- A redirect (to a configured URL).
The decoy is the stealth surface: a port scanner or a client that
doesn't offer alknet ALPNs connects on h2/http/1.1 and sees the
decoy. Real services use alknet/ssh, alknet/call, etc. The decoy
config is a two-way-door default (an operator picks what to serve); the
existence of the stealth path is fixed by ADR-010.
Constraints
- The HTTP path IS the operation path.
POST /fs/readFile→call.requestedforfs/readFile. No second routing table. See ADR-036. Externaloperations only.Internaloperations return404on the HTTP handler.- Bearer-only auth.
Authorization: Bearer→resolve_from_token. Other HTTP auth schemes are not implemented. - No secret material in HTTP responses. The call protocol carries no
secret material (ADR-014); the HTTP handler inherits this constraint.
Capabilities are used for outbound calls (
from_openapi), never serialized into HTTP response bodies. /healthzis raw. No auth, no call protocol. The one raw route.- The
h3ALPN is a first-class transport. TheHttpAdapterregisters forh3when theh3feature is enabled (ADR-038). Theh3handler is covered in webtransport.md; this document covers theh2/http/1.1path.
Design Decisions
| Decision | ADR | Summary |
|---|---|---|
| Direct path mapping (HTTP path = operation path) | ADR-036 | POST /{service}/{op} → call.requested |
| SSE projection for subscriptions over h2/http1.1 | ADR-036 | call.responded stream → SSE frames |
/healthz is a raw route |
ADR-036 | No auth, no call protocol |
| Stealth decoy | ADR-010 | HTTP handler on standard ALPNs serves decoy |
Bearer auth via resolve_from_token |
ADR-004 | HTTP handler credential source (settled) |
h3 is first-class (not deferred) |
ADR-038 | The h3 ALPN handler lives in this crate |
| Error mapping (call codes → HTTP status) | ADR-023 | Protocol/operation codes distinct; HTTP_<status> prefix for imported |
Open Questions
See open-questions.md for full details.
- OQ-39 (open):
to_openapipublished-spec versioning — the generated OpenAPI spec is a compatibility contract (ADR-017 Consequences); the versioning strategy needs specifying. - OQ-40 (open): reqwest client config and connection pooling —
two-way-door config shape for the outbound HTTP client used by
from_openapi/from_mcp.
References
- ADR-036 — the HTTP-to-call mapping this server implements
- ADR-038
— the
h3/WebTransport companion to this server - overview.md — crate overview, adapter location map
- webtransport.md — the
h3ALPN handler - http-adapters.md —
from_openapi/to_openapi - ../core/auth.md —
IdentityProvider, Bearer →resolve_from_token - ../core/endpoint.md — stealth mode as ALPN dispatch
- ../call/operation-registry.md —
OperationRegistry::invoke(), the dispatch path HTTP requests hit