docs(http): defer h3/WebTransport (ADR-044); browsers use WebSocket for v1

Working through the WebTransport implementation path surfaced a scope
question distinct from the hedging-as-deferral anti-pattern ADR-038 was
written to correct. Three findings drove the re-evaluation:

1. The browser bidirectional call-protocol path doesn't require
   WebTransport — WebSocket is full-duplex, EventEnvelope fits a WS
   binary message boundary cleanly, and the Dispatcher is stream-
   agnostic (ADR-012). What WebTransport gives over WebSocket (native
   multi-stream multiplexing, the ALPN-as-stream substrate) benefits the
   proxy use case, not the call protocol.
2. WebTransport is a draft standard (-07, not RFC) on an experimental
   Rust dependency stack (wtransport/h3 both self-describe as not
   production-ready). Either choice puts a draft protocol on the
   security surface of the first release.
3. The ALPN-stream-proxy (ADR-040) is speculative — its WASM parser
   consumers (browser SSH/SFTP/git clients) don't exist yet, and the
   downstream crates WebTransport deferral blocks (SSH, git, SFTP)
   expose their ALPNs natively over QUIC regardless.

This is a scope decision (per ADR-009: a decision that 'genuinely
doesn't need to be made yet because the use case isn't concrete'), not
hedging. The reversal trigger is concrete: a real deployment needing
the ALPN-stream-proxy.

ADR-038 is superseded (its anti-pattern correction stands; its specific
'h3 in scope now' decision is reversed). ADR-040 and ADR-043 are
parked, not superseded — their designs revive unchanged when WebTransport
revives, with §2 (bidirectionality) and §3 (no-PeerId overlay) of ADR-043
transferring to WebSocket for v1.

ADR-044 §5 also states the 'browser is not a peer' rationale that
ADR-034 §4 closed without arguing: peer = addressable node in the
call-protocol peer graph (stable PeerId, PeerRef::Specific-reachable,
identity stable across reconnects), not 'any endpoint that exchanges
calls during a live session.' A browser is the second but not the first
(no stable crypto identity of its own, ephemeral, not addressable from
other nodes). ADR-034 §4 and Assumption 2 are amended by reference.

The wtransport-vs-hyperium dependency question is recorded (not
resolved — WebTransport is deferred) in ADR-044 §'Research note' and
webtransport.md so the revival doesn't re-derive it: wtransport probably
isn't the right choice (axum-bridge friction — it owns its own HTTP
serving path); the hyperium stack (h3 + h3-quinn + h3-webtransport) fits
the axum integration better but its server-side WebTransport API needs
verification before commitment.

Reviewed by architecture-review subagent; all critical cross-reference
issues (ADR-034 §5 stale 'in scope' assertion, ADR-036 Context listing
h3 as implemented, webtransport.md Design Decisions table) resolved.
This commit is contained in:
2026-06-30 05:55:55 +00:00
parent 78b226d31b
commit 125cb49cc4
13 changed files with 769 additions and 176 deletions

View File

@@ -1,13 +1,14 @@
---
status: draft
last_updated: 2026-06-29
last_updated: 2026-06-30
---
# alknet-http — Overview
The HTTP interface crate: serves inbound HTTP on standard ALPNs 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
The HTTP interface crate: serves inbound HTTP on standard ALPNs (with
WebSocket upgrade 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
@@ -16,11 +17,14 @@ details are in the sibling documents.
architecture. It serves two roles in one crate:
1. **HTTP server** — a `ProtocolHandler` (`HttpAdapter`) that accepts
HTTP/2, HTTP/1.1, and HTTP/3 (WebTransport) connections on the
standard IANA ALPNs (`h2`, `http/1.1`, `h3`). It serves REST APIs, the
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.
stealth mode. HTTP/3 + WebTransport (`h3` ALPN) is deferred per
[ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md);
the deferred handler design is at [webtransport.md](webtransport.md).
2. **HTTP client host** — the home of the HTTP-transport-backed call
adapters: `from_openapi` (import external HTTP APIs as call
operations, using `reqwest` for outbound calls) and `from_mcp` (import
@@ -42,7 +46,9 @@ crates that need to expose an HTTP interface. A downstream consumer (the
CLI binary, a hub deployment, a browser-facing service) wires
`HttpAdapter` into the `HandlerRegistry` for the standard HTTP ALPNs and
gets a full HTTP surface: REST projection of the call protocol, OpenAPI
discovery, MCP tool exposure, and WebTransport for browsers.
discovery, MCP tool exposure, and WebSocket for browser bidirectional
access to the call protocol. (WebTransport is deferred per ADR-044; the
deferred browser-streaming path is at [webtransport.md](webtransport.md).)
The key architectural insight that shapes the crate: **HTTP is both a
server surface and a client transport for adapters.** The server side
@@ -64,13 +70,15 @@ Calls": both sides can initiate calls). The HTTP/1.1 + HTTP/2 surface
inherits HTTP's request/response constraint and projects the call
protocol one-directionally (client→server calls only — see
[http-server.md](http-server.md) §"One-directional projection").
WebTransport (`h3`) is the HTTP-family transport that restores the
call protocol's native bidirectionality — it is a transport substrate
for the call protocol (and, via the ALPN-stream-proxy, for any ALPN),
not a browser→hub one-way path. See [webtransport.md](webtransport.md)
and ADR-043. The "from/to" naming of the OpenAPI/MCP adapters should not
be read as a statement about the call protocol's directionality; it is
a statement about OpenAPI's and MCP's directionality.
**WebSocket is the HTTP-family transport that restores the call
protocol's native bidirectionality for browsers** (ADR-044) — a WS
connection is a long-lived full-duplex channel over which either side
can send `call.requested` frames in either direction. WebTransport
(`h3`, deferred) would restore it via native multi-stream multiplexing;
WebSocket restores it via framed messages over one connection. The
"from/to" naming of the OpenAPI/MCP adapters should not be read as a
statement about the call protocol's directionality; it is a statement
about OpenAPI's and MCP's directionality.
## Dependencies
@@ -79,13 +87,22 @@ alknet-http
├── alknet-core (ProtocolHandler, Connection, AuthContext, IdentityProvider, Capabilities)
├── alknet-call (OperationAdapter, OperationSpec, Handler, HandlerRegistration,
│ OperationRegistry, AdapterError, OperationProvenance)
├── axum (HTTP server — Router, extractors, middleware)
├── axum (HTTP server — Router, extractors, middleware, WebSocket upgrade)
├── reqwest (HTTP client — from_openapi/from_mcp forwarding)
├── hyper (HTTP/1.1 + HTTP/2 framing; axum is built on hyper)
├── wtransport (HTTP/3 + WebTransport — feature-gated behind `h3`)
└── rmcp (MCP streamable HTTP — feature-gated behind `mcp`)
```
> **Note:** the `h3`/WebTransport dependency (`wtransport` or the
> hyperium `h3` stack) is **not** in the v1 dependency tree —
> `h3`/WebTransport is deferred per
> [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md).
> The `h3` feature gate and its dependency are absent from the initial
> release; the browser bidirectional path uses WebSocket (native axum
> support, no new dependency). The deferred dependency analysis is
> recorded in ADR-044 §"Research note (for revival)" and
> [webtransport.md](webtransport.md) §"Research note".
### The `alknet-call` dependency (ADR-003 Amendment 1)
`alknet-http` depends on `alknet-call`. ADR-003's rule is "no handler
@@ -108,21 +125,22 @@ know `reqwest` is involved.
| ALPN | Handler | Transport | Browser? |
|------|---------|-----------|----------|
| `http/1.1` | `HttpAdapter` | HTTP/1.1 over QUIC stream | No |
| `h2` | `HttpAdapter` | HTTP/2 over QUIC stream | No |
| `h3` | `HttpAdapter` | HTTP/3 / WebTransport | Yes (X.509 required) |
| `http/1.1` | `HttpAdapter` | HTTP/1.1 over QUIC stream (+ WS upgrade) | Yes (WS upgrade for bidirectional) |
| `h2` | `HttpAdapter` | HTTP/2 over QUIC stream (+ WS upgrade) | Yes (WS upgrade for bidirectional) |
| `h3` | — (deferred) | HTTP/3 / WebTransport | Deferred per ADR-044 |
These are standard IANA ALPN strings, not `alknet/`-prefixed. Any HTTP
client connects without knowing about alknet — the TLS handshake
negotiates `h2` or `http/1.1` normally, and the `HttpAdapter` serves
HTTP. This is the stealth mapping (ADR-010): clients that don't offer
alknet ALPNs get the HTTP handler, just like port scanners in stealth
mode.
mode. A browser negotiates `h2` or `http/1.1` and upgrades to WebSocket
for the bidirectional call-protocol path (ADR-044).
The `HttpAdapter` registers for all three ALPNs (when the corresponding
features are enabled). The endpoint's `HandlerRegistry` maps each ALPN to
the same `HttpAdapter` instance; the handler branches on
`connection.remote_alpn()` to pick the right framing.
The `HttpAdapter` registers for `http/1.1` and `h2`. The endpoint's
`HandlerRegistry` maps each ALPN to the same `HttpAdapter` instance;
the handler branches on `connection.remote_alpn()` to pick the right
framing. The `h3` ALPN is not registered in v1 (deferred per ADR-044).
## Adapter Location Map
@@ -139,7 +157,7 @@ alknet-call (lean — no HTTP client, no HTTP server)
└── CallClient (outbound connection opener)
alknet-http (owns HTTP server + HTTP client)
├── HttpAdapter (axum server — inbound HTTP on h2/http1.1/h3)
├── HttpAdapter (axum server — inbound HTTP on h2/http1.1 + WS upgrade)
├── from_openapi (parse OpenAPI doc + reqwest forwarding handler)
├── to_openapi (generate OpenAPI doc from local registry)
├── from_mcp (feature-gated) (import remote MCP tools over streamable HTTP — reqwest)
@@ -155,24 +173,27 @@ directions.
```toml
[features]
default = ["h2", "http1"] # the non-browser HTTP surface
h3 = ["dep:wtransport"] # HTTP/3 + WebTransport (browser path; X.509 required)
default = ["h2", "http1"] # the HTTP surface (incl. WebSocket upgrade for browsers)
mcp = ["dep:rmcp"] # from_mcp / to_mcp (streamable HTTP only — ADR-037)
# h3 (HTTP/3 + WebTransport) is deferred per ADR-044 — not in the v1
# feature set. The browser bidirectional path uses WebSocket (native to
# axum, no feature gate). When WebTransport revives, the `h3` feature
# gate returns; see ADR-044 and webtransport.md.
```
- `h2` + `http1` (default): the `axum` + `hyper` HTTP/1.1 + HTTP/2
server. This is the surface non-browser clients use.
- `h3`: the `wtransport` (or quinn HTTP/3 extension) dependency. Adds
the `h3` ALPN handler and the WebTransport streaming path. See
[webtransport.md](webtransport.md) and
[ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md).
server, including WebSocket upgrade for browser bidirectional access
(ADR-044). This is the surface all clients — including browsers, via
WS upgrade — use in v1.
- `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).
A deployment that only needs the REST surface (no browsers, no MCP) uses
the default features. A browser-facing hub enables `h3`. A deployment
that wants MCP tool import/export enables `mcp`.
A deployment that only needs the REST surface (no MCP) uses the default
features. A browser-facing hub also uses the default features — the
browser bidirectional path is WebSocket, native to axum, no `h3` feature
gate needed. A deployment that wants MCP tool import/export enables
`mcp`.
## The No-Env-Vars Invariant
@@ -202,18 +223,19 @@ verified against this invariant. See ADR-014 and
## Architecture (component pointers)
- **[http-server.md](http-server.md)** — the `HttpAdapter` for `h2`/
`http/1.1`: how axum is run over a QUIC bidirectional stream, Bearer
auth resolution, the `/healthz` raw route, stealth decoy, and the
HTTP-to-call dispatch (ADR-036).
`http/1.1` (+ WebSocket upgrade): how axum is run over a QUIC
bidirectional stream, Bearer auth resolution, the `/healthz` raw route,
stealth decoy, the HTTP-to-call dispatch (ADR-036), and the WebSocket
browser bidirectional path (ADR-044).
- **[http-adapters.md](http-adapters.md)** — `from_openapi` (parse
OpenAPI, build forwarding handlers with `reqwest`) and `to_openapi`
(generate an OpenAPI doc from the registry's `External` operations).
Error fidelity per ADR-023.
- **[http-mcp.md](http-mcp.md)** — `from_mcp`/`to_mcp` (feature-gated),
streamable HTTP only (ADR-037), the rmcp integration.
- **[webtransport.md](webtransport.md)** — the `h3` ALPN handler,
WebTransport session/stream handling, the browser streaming path
(ADR-038).
- **[webtransport.md](webtransport.md)** — the deferred `h3` ALPN
handler (HTTP/3 + WebTransport). **Deferred per ADR-044**; kept intact
for revival when a concrete ALPN-stream-proxy use case arrives.
## Design Decisions
@@ -221,12 +243,13 @@ verified against this invariant. See ADR-014 and
|----------|-----|---------|
| HTTP-to-call operation mapping | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | Direct path mapping; `to_openapi` is projection, not router |
| MCP stdio transport exclusion | [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built (RCE vector) |
| HTTP/3 + WebTransport first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | `h3` in scope, not deferred; browser streaming uses QUIC streams |
| Defer h3/WebTransport; browsers use WebSocket | [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) | `h3`/WebTransport deferred (scope, not hedging); browser bidirectional path uses WebSocket; ADR-038 superseded, ADR-040/043 parked |
| 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) |
| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser |
| ~~HTTP/3 + WebTransport first-class~~ | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | **Superseded by ADR-044** (anti-pattern correction stands; specific decision reversed) |
| ~~WebTransport ALPN-stream-proxy~~ | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | **Parked** per ADR-044; revives unchanged when WebTransport revives |
| `to_mcp` tool-gateway pattern | [ADR-041](../../decisions/041-mcp-tool-gateway-pattern.md) | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation |
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe); per-caller AccessControl-filtered |
| WebTransport bidirectional ALPN substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport carries ALPNs as bidirectional streams; call protocol is the first target; both sides can initiate calls; non-peer clients use a connection-local overlay |
| ~~WebTransport bidirectional ALPN substrate~~ | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | **Parked** per ADR-044; §2/§3 transfer to WebSocket for v1; §4/§5 revive with WebTransport |
| `alknet-call` is protocol-foundation | [ADR-003](../../decisions/003-crate-decomposition.md) Am. 1 | `alknet-http` depends on `alknet-call` (types, not peer handler) |
| Bearer auth via `resolve_from_token` | [ADR-004](../../decisions/004-auth-as-shared-core.md) | HTTP handler credential source + resolution (settled) |
| Stealth mode = HTTP handler on standard ALPNs | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Decoy for unknown paths (settled) |
@@ -234,8 +257,8 @@ verified against this invariant. See ADR-014 and
| `OperationAdapter` trait is async | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | HTTP adapters implement the async trait (settled) |
| `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 (settled) |
| Error schema fidelity | [ADR-023](../../decisions/023-operation-error-schemas.md) | `from_openapi` maps HTTP status → `HTTP_<status>` codes; `to_openapi` projects back (settled) |
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3`/WebTransport needs X.509 (settled) |
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Browser over WebTransport/HTTPS = bearer token, no `PeerId` (settled) |
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3`/WebTransport needs X.509 (settled; applies when WebTransport revives) |
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 (amended by ADR-044 §5) | Browser over WS/WebTransport = bearer token, no `PeerId` (settled; rationale in ADR-044 §5) |
## Open Questions
@@ -262,4 +285,6 @@ See [open-questions.md](../../open-questions.md) for full details.
- `/workspace/@alkdev/operations/src/from_openapi.ts`,
`/workspace/@alkdev/operations/src/from_mcp.ts` — TypeScript prior art
- `/workspace/rust-sdk/` — MCP Rust SDK (rmcp); streamable HTTP examples
- `/workspace/wtransport/` — pure-Rust WebTransport reference
- `/workspace/wtransport/` — pure-Rust WebTransport reference
(read during research; not a dependency — see ADR-044 §"Research note"
for why `wtransport` is probably not the right revival choice)