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,23 +1,26 @@
---
status: draft
last_updated: 2026-06-29
last_updated: 2026-06-30
---
# alknet-http
HTTP interface for alknet: serves HTTP/1.1, HTTP/2, and HTTP/3 (WebTransport)
on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
(`from_openapi`, `to_openapi`, `from_mcp`, `to_mcp`).
HTTP interface for alknet: serves HTTP/1.1 and HTTP/2 on standard ALPNs
(with WebSocket upgrade for browser bidirectional access to the call
protocol), and hosts the HTTP-backed call-protocol adapters
(`from_openapi`, `to_openapi`, `from_mcp`, `to_mcp`). HTTP/3 + WebTransport
(`h3`) is deferred per
[ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md).
## Documents
| Document | Status | Description |
|----------|--------|-------------|
| [overview.md](overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map |
| [http-server.md](http-server.md) | draft | `HttpAdapter` (`ProtocolHandler` for `h2`/`http/1.1`), axum over QUIC, Bearer auth, stealth, `/healthz` |
| [http-server.md](http-server.md) | draft | `HttpAdapter` (`ProtocolHandler` for `h2`/`http/1.1` + WS upgrade), axum over QUIC, Bearer auth, stealth, `/healthz`, WebSocket browser path |
| [http-adapters.md](http-adapters.md) | draft | `from_openapi` (reqwest client) and `to_openapi` (OpenAPI projection); no-env-vars invariant point |
| [http-mcp.md](http-mcp.md) | draft | `from_mcp` / `to_mcp` (feature-gated), streamable-HTTP-only, stdio exclusion |
| [webtransport.md](webtransport.md) | draft | `h3`/WebTransport handler — the browser streaming path |
| [webtransport.md](webtransport.md) | deferred | `h3`/WebTransport handler — **deferred per ADR-044**; spec kept intact for revival |
## Applicable ADRs
@@ -34,23 +37,24 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
| [017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `OperationAdapter` trait; `to_*` are projections; published-spec contract |
| [022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, Composition Authority | `from_openapi`/`from_mcp` produce leaf bundles |
| [023](../../decisions/023-operation-error-schemas.md) | Operation Error Schemas | `from_openapi`/`to_openapi` error fidelity; `HTTP_<status>` error codes |
| [027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign | Browsers require X.509; WebTransport requires X.509 |
| [034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Outgoing-Only X.509 and Three Peer Roles | Browsers are not alknet peers; WebTransport relay-as-proxy recorded |
| [027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign | Browsers require X.509; applies to WebTransport (deferred) and any browser-facing TLS |
| [034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Outgoing-Only X.509 and Three Peer Roles | Browsers are not alknet peers (§4 amended by ADR-044 §5 with the addressability rationale) |
| [036](../../decisions/036-http-to-call-operation-mapping.md) | HTTP-to-Call Operation Mapping | Direct path mapping; `to_openapi` is projection, not router |
| [037](../../decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Streamable HTTP only; stdio not built |
| [038](../../decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | `h3` in scope, not deferred |
| [038](../../decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | **Superseded by ADR-044** (anti-pattern correction stands; specific decision reversed) |
| [039](../../decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | One crate for server + client host (shared HTTP deps, shared mapping) |
| [040](../../decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser |
| [040](../../decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | **Parked** per ADR-044; revives unchanged when WebTransport revives |
| [041](../../decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation; Subscription excluded |
| [042](../../decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered |
| [043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | WebTransport carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls; no-`PeerId` non-peer clients use a connection-local overlay |
| [043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | **Parked** per ADR-044; §2/§3 transfer to WebSocket for v1 |
| [044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) | Defer h3/WebTransport; Browsers Use WebSocket | `h3`/WebTransport deferred (scope); browser bidirectional path uses WebSocket; "browser is not a peer" rationale |
## Relevant Open Questions
| OQ | Title | Status | Relevance |
|----|-------|--------|-----------|
| OQ-11 | Handler-level auth resolution observability | resolved | HTTP handler stores resolved identity on `Connection` via `set_identity` |
| OQ-12 | TLS identity provisioning | resolved | Browsers require X.509 (gates the entire `h3` feature) |
| OQ-12 | TLS identity provisioning | resolved | Browsers require X.509 (applies to WebTransport when it revives; WebSocket uses the same TLS as h2/http1.1) |
| OQ-13 | Operation path format | resolved | `/{service}/{op}` is the HTTP path (ADR-036) |
| OQ-17 | Call protocol client and adapter contract | resolved | `OperationAdapter` trait; `to_*` projections |
| OQ-24 | Operation error schemas | resolved | `from_openapi`/`to_openapi` error fidelity |
@@ -63,11 +67,11 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
## Key Design Principles
1. **HTTP is both a server surface and a client transport for adapters.**
Inbound HTTP (`h2`/`http/1.1`/`h3`) is served by `axum` over a QUIC
stream; outbound HTTP (`from_openapi`/`from_mcp` forwarding) uses
`reqwest`. Both directions share the same HTTP dependencies, which is
why they live in one crate rather than being split. See
[overview.md](overview.md).
Inbound HTTP (`h2`/`http/1.1` + WebSocket upgrade) is served by `axum`
over a QUIC stream; outbound HTTP (`from_openapi`/`from_mcp`
forwarding) uses `reqwest`. Both directions share the same HTTP
dependencies, which is why they live in one crate rather than being
split. See [overview.md](overview.md).
2. **The HTTP surface is a projection of the call protocol.** An HTTP
request at `POST /fs/readFile` becomes a `call.requested` for
`/fs/readFile`. The HTTP path IS the operation path on the
@@ -78,7 +82,7 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
(direct-call surface) and [ADR-042](../../decisions/042-openapi-gateway-pattern.md)
(`to_openapi` gateway, superseding ADR-036's original `to_openapi`
clause).
3. **Standard ALPNs, not alknet ALPNs.** `h2`, `http/1.1`, `h3` are
3. **Standard ALPNs, not alknet ALPNs.** `h2`, `http/1.1` are
IANA-registered ALPN strings. Any HTTP client (browser, curl, axios)
connects without knowing about alknet — the TLS handshake negotiates
`h2` or `http/1.1` normally. This is the stealth mapping (ADR-010).
@@ -91,36 +95,34 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
arbitrary executable = RCE. Streamable HTTP is network-isolated,
auth-gatable, and runs under alknet's auth model. See
[ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md).
6. **HTTP/3 + WebTransport is a first-class transport, not a deferral.**
The browser streaming path uses QUIC streams directly. See
[ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md).
7. **The `h3` handler is an ALPN-stream-proxy for browsers.** A browser
with a WASM parser can reach any non-call ALPN handler (SSH, git,
SFTP) via WebTransport — no install, no native client, no VPN. The
call protocol needs no proxy (it speaks EventEnvelope directly);
the ALPN-stream-proxy is the substrate's mechanism for the protocols
that need a client-side parser. SSH-over-WebTransport is
HTTPS-shaped at the network layer (anti-censorship). See
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md)
and [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md).
8. **`h3` requires X.509.** Browsers don't support RFC 7250 raw keys
(ADR-027). A node serving WebTransport must have an X.509 identity.
This is a browser limitation, not an alknet decision.
9. **WebTransport is a bidirectional ALPN transport substrate.**
WebTransport carries ALPN protocols as bidirectional streams; the
call protocol is the first/canonical target (JSON-RPC over QUIC
streams, needs no WASM parser, runs in Deno/Node/browsers/native
Rust). Both sides of a WebTransport call-protocol session can
initiate calls — the call protocol's bidirectionality applies
unchanged. The HTTP/1.1 + HTTP/2 surface is the one-directional
projection (HTTP is request/response); WebTransport restores the
bidirectional call model. See
[ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md).
6. **WebSocket is the browser bidirectional path.** A browser upgrades
an HTTP/1.1 or HTTP/2 request to WebSocket and speaks the call
protocol over binary WS messages — full-duplex, both sides can
initiate calls (the call protocol's native bidirectionality, ADR-012).
HTTP/3 + WebTransport (`h3`) is deferred per
[ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md)
— a scope decision (the browser bidirectional path doesn't require
WebTransport's stream model; WebSocket suffices). The reversal
trigger is a concrete ALPN-stream-proxy use case (a browser running
a WASM SSH/SFTP/git client).
7. **Browsers are not alknet peers.** A browser over WebSocket (or, when
it revives, WebTransport) authenticates by bearer token, gets no
`PeerId`, and its registered ops land in a connection-local Layer 2
overlay. "Peer" means an 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 cryptographic identity of its own, ephemeral, not addressable
from other nodes. See
[ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)
§4 (amended by ADR-044 §5 with the addressability rationale).
## References
- `docs/research/alknet-http/phase-0-findings.md` — Phase 0 research
(directionally close; DH-2's deferral framing is corrected by ADR-038)
(directionally close; DH-2's deferral framing was corrected by
ADR-038, then ADR-038 was superseded by ADR-044 which re-defers
`h3`/WebTransport as a genuine scope decision)
- `docs/research/alknet-call-completion/gap-analysis.md` — adapter
location map, no-env-vars invariant
- `/workspace/@alkdev/operations/src/from_openapi.ts`,
@@ -128,4 +130,6 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
- `/workspace/rust-sdk/` — MCP Rust SDK (rmcp v1.8.0); streamable HTTP
transport examples
- `/workspace/wtransport/` — pure-Rust WebTransport reference
implementation (the `h3` feature's candidate dependency)
implementation (read during research; not a dependency. See ADR-044
§"Research note" for why `wtransport` is probably not the right
revival choice — the hyperium stack fits the axum integration better.)

View File

@@ -1,15 +1,17 @@
---
status: draft
last_updated: 2026-06-29
last_updated: 2026-06-30
---
# HTTP Server
The `HttpAdapter` — the `ProtocolHandler` for `h2` and `http/1.1` (and
`h3`, covered in [webtransport.md](webtransport.md)). This document
WebSocket upgrade — see §"WebSocket browser path"). The `h3`/WebTransport
path is deferred per [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md);
the deferred spec is at [webtransport.md](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.
resolution, the HTTP-to-call dispatch, the `/healthz` raw route, stealth
decoy, and the WebSocket browser path.
## What
@@ -54,12 +56,15 @@ impl ProtocolHandler for HttpAdapter {
}
```
The `HttpAdapter` registers for multiple ALPNs (`http/1.1`, `h2`, `h3`).
The `HttpAdapter` registers for multiple ALPNs (`http/1.1`, `h2`).
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](webtransport.md)).
HTTP/1.1 or HTTP/2 over a QUIC bidirectional stream. WebSocket upgrade
(§"WebSocket browser path") layers on top of the same hyper connection
driver — a WS upgrade is an HTTP/1.1 or HTTP/2 request that switches
protocols. The `h3` ALPN is deferred (ADR-044); the deferred handler
design is at [webtransport.md](webtransport.md).
## Why
@@ -167,9 +172,12 @@ A `Subscription` operation served over `h2`/`http/1.1` projects its
sends `call.aborted` for 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](webtransport.md)).
This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket
(§"WebSocket browser path" below), the subscription projects directly
onto the WS connection — `call.responded` events as binary WS messages,
no SSE framing. WebTransport (`h3`, deferred per ADR-044) would project
onto WebTransport bidirectional streams; see
[webtransport.md](webtransport.md).
### One-directional projection (HTTP request/response)
@@ -188,14 +196,79 @@ SSE response — but even there, the *call* is client-initiated; only the
*results* flow server→client.
This is a structural property of HTTP, not a design choice in this
crate. WebTransport (`h3`) restores the bidirectional call model: a
WebTransport session is a long-lived connection over which either side
can open bidirectional streams and send `call.requested` events in
either direction — the call protocol's native bidirectionality applies
unchanged. See [webtransport.md](webtransport.md) and ADR-043. The
HTTP/1.1 + HTTP/2 surface is the projection for clients that only speak
HTTP; WebTransport is the surface for clients that can speak the call
protocol in both directions.
crate. **WebSocket restores the bidirectional call model for browsers**
(see §"WebSocket browser path" below): a WS connection is a long-lived
full-duplex channel over which either side can send `call.requested`
frames in either direction — the call protocol's native bidirectionality
applies unchanged (ADR-012 — stream-agnostic correlation; a WS message
stream is another `BiStream`-satisfying transport). WebTransport (`h3`)
would restore it via native multi-stream multiplexing, but WebTransport
is deferred per ADR-044 — WebSocket is the v1 browser bidirectional path.
The HTTP/1.1 + HTTP/2 surface is the projection for clients that only
speak HTTP; WebSocket is the surface for browser clients that speak the
call protocol in both directions.
### WebSocket browser path (ADR-044)
A browser connecting to a hub upgrades an HTTP/1.1 or HTTP/2 request to
WebSocket (RFC 6455). The resulting full-duplex WS connection carries
call-protocol `EventEnvelope` frames as binary WebSocket messages — one
envelope per message. The browser authenticates by bearer token on the
upgrade request (the HTTP `Authorization` header), resolved by the hub's
`IdentityProvider::resolve_from_token`, same as any HTTP request. The WS
connection is then a **bidirectional call-protocol session**:
- The browser opens the WS connection to `/alknet/call` (or `/`).
- The handler hands the WS message stream to the call protocol's
`Dispatcher` — the same dispatch loop the `CallAdapter` uses for
`alknet/call` QUIC connections (ADR-012, stream-agnostic correlation).
- The browser writes `EventEnvelope` frames as binary WS messages; the
handler reads them and dispatches via `OperationRegistry::invoke()`.
- Responses (`call.responded`, `call.error`, `call.completed`,
`call.aborted`) are written back as binary WS messages.
**Bidirectionality:** the WS call-protocol session inherits the call
protocol's native bidirectionality — both sides can initiate calls
(ADR-043 §2, transferred to WebSocket per ADR-044 §3). The browser calls
operations on the hub; the hub can call operations registered on the
browser's side, over the same session, using the same `PendingRequestMap`
and `EventEnvelope` framing as `alknet/call`. The browser case where the
client registers no operations of its own is the common case — the
server→client call direction is unused because the browser has nothing
to call. That is a use-case scoping, not an architectural limitation.
**No SSE translation.** A `Subscription` operation served over WebSocket
projects its `call.responded` stream directly as binary WS messages — no
SSE `data:` framing. `call.completed` closes the stream; `call.aborted`
closes it with an error frame. This is the native streaming projection
for the WS path; SSE (ADR-036) is the projection for `h2`/`http/1.1`
clients that don't upgrade to WebSocket.
**Browsers are not alknet peers.** A browser over WebSocket authenticates
by bearer token, gets no `PeerId`, does not enter `PeerCompositeEnv`, and
its registered ops (if any) land in a connection-local Layer 2 overlay —
the inbound mirror of ADR-034 §2. The rationale (addressability vs.
bidirectionality) is stated in ADR-044 §5 and amends ADR-034 §4 by
reference. In short: "peer" means an 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 thing but not the
first — it has no stable cryptographic identity of its own (it presents
a bearer token the hub issued; nothing to pin), it is ephemeral (close
the tab → connection dies → the connection-local overlay dies with it),
and it is not addressable from other nodes (another alknet node has no
way to reach "the browser currently connected to hub-A"; the hub holds
it as a live `CallConnection` handle, not a peer-graph entry). The
connection-local overlay is what gives the browser bidirectional-call
capability *without* peer-graph membership.
**What WebSocket does not provide (deferred to WebTransport, ADR-044):**
the ALPN-stream-proxy (ADR-040) — a browser running a WASM parser for
SSH/SFTP/git to reach a non-call ALPN — requires WebTransport's
multi-stream model and is the speculative use case whose deferral is
ADR-044's reversal trigger. WebSocket carries the call protocol from a
browser; it does not carry the non-call-ALPN substrate. A browser cannot
reach SSH/SFTP/git ALPNs in the v1 release. See ADR-044.
### Auth
@@ -294,10 +367,11 @@ config is a two-way-door default (an operator picks what to serve); the
Capabilities are used for outbound calls (`from_openapi`), never
serialized into HTTP response bodies.
- **`/healthz` is raw.** No auth, no call protocol. The one raw route.
- **The `h3` ALPN is a first-class transport.** The `HttpAdapter`
registers for `h3` when the `h3` feature is enabled (ADR-038). The
`h3` handler is covered in [webtransport.md](webtransport.md); this
document covers the `h2`/`http/1.1` path.
- **WebSocket is the browser bidirectional path (ADR-044).** A browser
upgrades an HTTP request to WS and speaks the call protocol over binary
messages. `h3`/WebTransport is deferred (ADR-044); the ALPN-stream-proxy
(ADR-040) is not available in v1. The `h3` ALPN and its feature gate are
not implemented in the initial release.
## Design Decisions
@@ -309,7 +383,8 @@ config is a two-way-door default (an operator picks what to serve); the
| `/healthz` is a raw route | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | No auth, no call protocol |
| Stealth decoy | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | HTTP handler on standard ALPNs serves decoy |
| Bearer auth via `resolve_from_token` | [ADR-004](../../decisions/004-auth-as-shared-core.md) | HTTP handler credential source (settled) |
| `h3` is first-class (not deferred) | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | The `h3` ALPN handler lives in this crate |
| WebSocket is the browser bidirectional path | [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) | Browsers upgrade to WS; `EventEnvelope` over binary messages; `h3`/WebTransport deferred |
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 (amended by ADR-044 §5) | Bearer token, no `PeerId`, connection-local overlay (addressability vs. bidirectionality) |
| Error mapping (call codes → HTTP status) | [ADR-023](../../decisions/023-operation-error-schemas.md) | Protocol/operation codes distinct; `HTTP_<status>` prefix for imported |
## Open Questions
@@ -327,10 +402,13 @@ See [open-questions.md](../../open-questions.md) for full details.
- [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) — the
HTTP-to-call mapping this server implements
- [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md)
the `h3`/WebTransport companion to this server
- [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md)
WebSocket is the v1 browser bidirectional path; `h3`/WebTransport
deferred. States the "browser is not a peer" rationale (addressability
vs. bidirectionality) that ADR-034 §4 closes without arguing.
- [overview.md](overview.md) — crate overview, adapter location map
- [webtransport.md](webtransport.md) — the `h3` ALPN handler
- [webtransport.md](webtransport.md) — the deferred `h3` ALPN handler
(kept intact for revival)
- [http-adapters.md](http-adapters.md) — `from_openapi`/`to_openapi`
- [../core/auth.md](../core/auth.md) — `IdentityProvider`, Bearer →
`resolve_from_token`

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)

View File

@@ -1,10 +1,43 @@
---
status: draft
last_updated: 2026-06-29
status: deferred
last_updated: 2026-06-30
---
# WebTransport — the h3 ALPN handler
> **DEFERRED per [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md).**
> This spec is kept intact for revival. `h3`/WebTransport is not
> implemented in the initial `alknet-http` release; the browser
> bidirectional path uses WebSocket (see
> [http-server.md](http-server.md) §"WebSocket browser path"). ADR-038
> is superseded; ADR-040 and ADR-043 are parked (their decisions revive
> unchanged when WebTransport revives). The reversal trigger is a
> concrete deployment needing the ALPN-stream-proxy (a browser running
> a WASM SSH/SFTP/git client to reach a non-call ALPN). Two transfers
> apply during the deferment: ADR-043 §2 (call-protocol bidirectionality)
> and §3 (the no-`PeerId` connection-local overlay) apply over WebSocket
> unchanged; ADR-040 (the ALPN-stream-proxy) and ADR-043 §4 (the
> non-call-ALPN substrate) do not — they require WebTransport's stream
> model and revive with it.
>
> **Research note (for revival):** `wtransport` (v0.7.1, the reference
> implementation read during initial research) is *probably not* the
> right dependency choice at revival time, despite being a complete and
> readable implementation. The load-bearing integration concern is that
> the `h3` handler must route HTTP/3 requests through the same axum
> `Router` as `h2`/`http/1.1` (ADR-036), and `wtransport` owns its own
> HTTP serving path — bridging its request type into the `http::Request`
> axum consumes is cross-ecosystem adapter work. The hyperium stack
> (`h3` + `h3-quinn` + `h3-webtransport` + `h3-datagram`) operates at
> the stream level and produces `http::Request` types natively, which is
> a better fit for the axum integration — but its server-side
> WebTransport API needs verification before commitment (the axum-bridge
> feasibility is the load-bearing claim and is not yet confirmed against
> actual crate APIs, only against READMEs and design philosophy). This
> research is **not** run now (WebTransport is deferred); it is recorded
> here so the revival does not re-derive the question from scratch. See
> ADR-044 §"Research note (for revival)" for the cross-reference.
The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and
WebTransport. WebTransport is a **bidirectional ALPN transport
substrate** (ADR-043) — it carries ALPN protocols as bidirectional
@@ -386,13 +419,19 @@ as a first-class transport.
## Design Decisions
> **Note:** This table reflects the design as written for revival. ADR-038
> is **superseded by [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md)**;
> ADR-040 and ADR-043 are **parked** (implementation deferred per ADR-044).
> The decisions revive unchanged when WebTransport revives — see the
> header note and ADR-044 for the scope rationale and reversal trigger.
| Decision | ADR | Summary |
|----------|-----|---------|
| `h3`/WebTransport is first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | In scope, not deferred; browser streaming uses QUIC streams |
| WebTransport is a bidirectional ALPN transport substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | Carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls |
| 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 |
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation) |
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Bearer token, no `PeerId` |
| ~~`h3`/WebTransport is first-class~~ | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | **Superseded by ADR-044** (scope deferral); originally "in scope, not deferred; browser streaming uses QUIC streams" |
| WebTransport is a bidirectional ALPN transport substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | **Parked** per ADR-044. Carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls |
| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | **Parked** per ADR-044. The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser |
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation; 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) | Bearer token, no `PeerId` (rationale in ADR-044 §5) |
| WebTransport streams → call protocol directly | [ADR-012](../../decisions/012-call-protocol-stream-model.md) | Stream-agnostic; WebTransport stream = QUIC bidirectional stream |
| `BiStream` is a trait (WASM door) | [ADR-007](../../decisions/007-bistream-type-definition.md) | Browser implements `BiStream` over WebTransport stream; WASM parser speaks the protocol |
| Stealth on h3 | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Unknown paths get the decoy |