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).
10 KiB
ADR-036: HTTP-to-Call Operation Mapping
Status
Proposed
Context
alknet-http implements ProtocolHandler for the standard HTTP ALPNs (h2,
http/1.1, h3). An inbound HTTP request that targets an alknet operation
must become a call-protocol call.requested dispatch — the HTTP handler is a
projection of the call protocol, not a parallel routing layer. The
question is how an HTTP request maps to an operation invocation.
Three options were considered in the alknet-http Phase 0 research
(docs/research/alknet-http/phase-0-findings.md, decision point DH-3):
- (a) Direct path mapping.
POST /{service}/{op}→call.requestedfor/{service}/{op}. The HTTP handler parses the request body as the operation input, sendscall.requested, and returns the response as JSON. The HTTP surface is a thin projection of the call protocol's/{service}/{op}operation path format (resolved by OQ-13). - (b) OpenAPI-defined routes. The HTTP surface is defined by the
to_openapiprojection — routes, methods, schemas are generated from the registry'sExternaloperations, and the HTTP handler dispatches based on the generated OpenAPI spec's path mapping. - (c) Explicit route registration. The assembly layer registers HTTP routes explicitly, mapping URL paths to operations. Most flexible, most boilerplate.
This is a load-bearing architectural choice. Once the HTTP surface's routing contract is published and external clients build against it, changing the mapping (e.g., from "the HTTP path IS the operation path" to "the HTTP path is a generated alias") is a one-way door: every client breaks. It needs an ADR before implementation.
The call protocol's operation path format is /{service}/{op} (OQ-13,
resolved). The HTTP handler serves these operations over HTTP. The mapping
must be a projection of that single operation surface, not a second
routing table that has to be kept in sync with the registry.
Decision
Direct path mapping is the default HTTP surface; to_openapi is the
discovery/projection layer, not a parallel router.
The HttpAdapter receives an HTTP request whose path is /{service}/{op}
(e.g., POST /fs/readFile, POST /agent/chat), constructs a
call.requested dispatch with operationId: /{service}/{op} and input: <parsed body>, and returns the operation's response as JSON. The HTTP path
IS the operation path — one routing surface, the call protocol's.
to_openapi generates the OpenAPI spec that describes this surface for
external consumers (route paths, methods, request/response schemas, error
schemas per ADR-023). It does not define separate routes — the generated
spec's paths mirror the /{service}/{op} operation paths. An external
client reading the OpenAPI doc learns the same routes the HTTP handler
serves; there is no second mapping.
HTTP method semantics
The call protocol's OperationType (Query, Mutation, Subscription,
per operation-registry.md) maps to HTTP methods on the default surface:
OperationType |
Default HTTP method | Notes |
|---|---|---|
Query |
GET |
Read-only, idempotent. Input from query parameters + optional body. |
Mutation |
POST (or PUT/PATCH/DELETE if the operation declares it) |
Default POST; the op may declare a specific mutation method in its spec metadata. |
Subscription |
GET with Accept: text/event-stream |
Streaming — the HTTP handler projects the subscription's call.responded stream as SSE chunks. |
The default method for an External operation with no explicit HTTP method
declared is POST for Mutation, GET for Query. This is the
least-surprise default; an operation that wants a specific HTTP verb
declares it. The method-to-OperationType mapping is a two-way-door
default (changing it later is additive — a new method is added, existing
methods keep working).
Streaming projection (SSE)
A Subscription operation served over HTTP/1.1 or HTTP/2 projects its
call.responded stream as Server-Sent Events. Each call.responded event
becomes an SSE data: frame; call.completed closes the SSE stream;
call.aborted closes the stream with an SSE error event. 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 is needed (see ADR-038 for the WebTransport path).
Auth
Inbound HTTP auth is Authorization: Bearer <token>, resolved via
IdentityProvider::resolve_from_token() (auth.md's handler table —
HttpAdapter, Bearer header, resolve_from_token). This is settled by
ADR-004 and OQ-11; this ADR does not change it. Bearer-only is the auth
mechanism; other HTTP auth schemes (Basic, API key in query param) are not
implemented. An unauthenticated request to an operation with
AccessControl restrictions returns 401/403 (mapped from the call
protocol's FORBIDDEN protocol code).
Stealth mode
The HTTP handler on h2/http/1.1 serves a decoy (configurable: fake
404, a static site, a redirect) for paths that are not registered
operations. This is the ALPN-based stealth mapping from endpoint.md —
clients that don't offer alknet ALPNs get the HTTP handler, and unknown
HTTP paths get the decoy. The decoy is a two-way-door config default (an
operator picks what to serve); the existence of the stealth path is fixed
by ADR-010.
/healthz and operational endpoints
GET /healthz is a raw HTTP route outside the call protocol — no auth, no
operation registration. It exists for infrastructure (load balancers,
orchestrators). Other operational endpoints (metrics, dashboard) are
call-protocol operations if built (/metrics/list, /dashboard/view),
not raw HTTP routes. healthz is the one exception: it must be callable
without auth before identity is resolvable.
Consequences
Positive:
- One routing surface. The HTTP handler does not maintain a second routing
table; it projects the call protocol's
/{service}/{op}paths directly. No sync drift between the operation registry and the HTTP routes. to_openapiis a pure projection (generate a spec that describes the existing surface), not a routing authority. The generated spec is always consistent with what the handler actually serves because they're the same paths.- External HTTP clients (curl, axios, browser
fetch) can call alknet operations without knowing about the call protocol — the HTTP surface is a standard REST-like API. - The abort cascade (ADR-016) is preserved: an HTTP client disconnecting
mid-subscription is detected as a stream close, and the HTTP handler
sends
call.abortedfor the in-flight subscription, which cascades to descendants. - The HTTP method mapping (
Query→GET,Mutation→POST,Subscription→SSE) is the standard REST projection — no surprise verbs, no exotic method semantics.
Negative:
- The HTTP surface inherits the call protocol's
/{service}/{op}path shape. An operation namedfs/readFileis served atPOST /fs/readFile, not at a REST-nestedPOST /fs/files/:id/reador any other REST-conventional path. Operations that want a REST-nested HTTP path must declare it in spec metadata (a two-way-door extension); the default is the operation path verbatim. This is a deliberate least-surprise-for-alknet choice, not a REST-purist choice. - HTTP request/response semantics don't map cleanly onto every call
protocol operation. A
Querywith a large input has to put the input in the body (GET-with-body is non-standard). AMutationthat is idempotent doesn't getPUTsemantics unless it declares them. The projection is lossy at the edges; operations that need precise HTTP semantics declare them. to_openapiis a published compatibility contract (ADR-017 Consequences: once external clients build against the generated spec, the mapping is one-way). The generated spec's versioning (tied to the registry'sExternaloperation set version) must be emitted as a spec marker so consumers can detect mapping changes. This is OQ-17's published-artifact concern, applied to the HTTP projection.
Assumptions
-
The operation path IS the HTTP path. An operation
fs/readFileis served at/fs/readFile. There is no separate HTTP path mapping layer. If a deployment wants different HTTP paths (e.g., a REST-nested convention), that's a future projection layer, not a change to this mapping. -
Externaloperations are the HTTP surface.Internaloperations (composition-only, ADR-015) are not served over HTTP — they return404on the HTTP handler, matching the call protocol'sNOT_FOUNDfor wire calls to Internal ops. The HTTP handler dispatches onlyExternaloperations. -
HTTP auth is Bearer-only. The HTTP handler resolves identity from the
Authorization: Bearerheader viaresolve_from_token. 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 middleware (two-way door), but the default surface is Bearer-only.
References
- ADR-004 —
IdentityProvider, Bearer →resolve_from_token(the auth model this ADR uses, unchanged) - ADR-010 — stealth mode as ALPN dispatch (the HTTP handler on standard ALPNs serves the decoy)
- ADR-015 — External/Internal visibility (Internal ops are not served over HTTP)
- ADR-016 — abort cascade (HTTP
client disconnect →
call.aborted→ cascade to descendants) - ADR-017 —
to_openapias a projection; published-spec compatibility contract - ADR-023 — error schema fidelity in
from_openapi/to_openapi; HTTP status mapping - OQ-13 (resolved) — operation path format
/{service}/{op} docs/research/alknet-http/phase-0-findings.mdDH-3 — the decision this ADR resolvescrates/http/http-server.md— the spec that implements this mapping