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

@@ -18,9 +18,9 @@ The storage and auth strategy research (`docs/research/alknet-storage-strategy/f
The alknet-call crate is **implemented and reviewed** — both the server-side core and the client/adapter surface (207 lib + 2 integration tests passing). The alknet-core and alknet-call crate specs are in draft; the alknet-vault crate specs are stable. The alknet-call crate is **implemented and reviewed** — both the server-side core and the client/adapter surface (207 lib + 2 integration tests passing). The alknet-core and alknet-call crate specs are in draft; the alknet-vault crate specs are stable.
**alknet-http specs drafted and consistency-reviewed.** The alknet-http crate (HTTP interface — `h2`/`http/1.1`/`h3` server + `from_openapi`/`to_openapi`/`from_mcp`/`to_mcp` adapters) now has architecture specs: [crates/http/](crates/http/) (overview, http-server, http-adapters, http-mcp, webtransport) and eight ADRs — [ADR-036](decisions/036-http-to-call-operation-mapping.md) (HTTP-to-call mapping; direct-call surface), [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) (MCP stdio exclusion), [ADR-038](decisions/038-http3-and-webtransport-as-first-class.md) (HTTP/3 + WebTransport as first-class, correcting the Phase 0 deferral framing), [ADR-039](decisions/039-http-server-and-client-host-colocated.md) (HTTP server + client host colocated in one crate), [ADR-040](decisions/040-webtransport-alpn-stream-proxy.md) (WebTransport ALPN-stream-proxy — the substrate's mechanism for non-call ALPNs like SSH/git/SFTP via WASM parser; the "VPN-like without being a VPN" use case), [ADR-041](decisions/041-mcp-tool-gateway-pattern.md) (`to_mcp` tool-gateway pattern — 4 fixed gateway tools instead of one tool per operation, addressing LLM context tool-bloat), [ADR-042](decisions/042-openapi-gateway-pattern.md) (`to_openapi` gateway pattern — 5 fixed gateway endpoints instead of one path per operation; per-caller AccessControl-filtered API surface; supersedes ADR-036's original `to_openapi` clause), [ADR-043](decisions/043-webtransport-bidirectional-alpn-substrate.md) (WebTransport as a bidirectional ALPN transport substrate — reframes WebTransport as carrying ALPNs as bidirectional streams with the call protocol as the first/canonical target; names the call protocol's bidirectionality over WebTransport; states the inbound no-`PeerId` connection-local overlay as the mirror of ADR-034 §2). ADR-003 Amendment 1 clarifies that `alknet-call` is a protocol-foundation crate (the `alknet-http``alknet-call` dependency edge). A consistency review pass corrected drift from the mid-spec pivot (the `to_openapi` gateway pattern landed in the prose but not in cross-references; the WebTransport specs inherited the OpenAPI/MCP direction assumption that doesn't hold for the call protocol) — ADR-036's `to_openapi` clause is now amended as superseded by ADR-042, ADR-034 §5's "deferral bucket" wording is corrected (the decision stands), and the http specs now name the one-directional HTTP projection vs. the bidirectional WebTransport substrate. The specs are in draft; implementation has not started. Three open questions carried: OQ-38 (WebTransport standalone relay service scope — distinct from the in-process ALPN-stream-proxy resolved by ADR-040), OQ-39 (`to_openapi` published-spec versioning), OQ-40 (reqwest client config). **alknet-http specs drafted and consistency-reviewed.** The alknet-http crate (HTTP interface — `h2`/`http/1.1` server + WebSocket browser path + `from_openapi`/`to_openapi`/`from_mcp`/`to_mcp` adapters) now has architecture specs: [crates/http/](crates/http/) (overview, http-server, http-adapters, http-mcp, webtransport) and nine ADRs — [ADR-036](decisions/036-http-to-call-operation-mapping.md) (HTTP-to-call mapping; direct-call surface), [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) (MCP stdio exclusion), [ADR-038](decisions/038-http3-and-webtransport-as-first-class.md) (HTTP/3 + WebTransport as first-class**superseded by ADR-044**; its correction of the two-way-door-as-deferral anti-pattern stands, its specific decision is reversed by the scope deferral), [ADR-039](decisions/039-http-server-and-client-host-colocated.md) (HTTP server + client host colocated in one crate), [ADR-040](decisions/040-webtransport-alpn-stream-proxy.md) (WebTransport ALPN-stream-proxy — **parked** per ADR-044; revives unchanged when WebTransport revives), [ADR-041](decisions/041-mcp-tool-gateway-pattern.md) (`to_mcp` tool-gateway pattern — 4 fixed gateway tools instead of one tool per operation, addressing LLM context tool-bloat), [ADR-042](decisions/042-openapi-gateway-pattern.md) (`to_openapi` gateway pattern — 5 fixed gateway endpoints instead of one path per operation; per-caller AccessControl-filtered API surface; supersedes ADR-036's original `to_openapi` clause), [ADR-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), [ADR-044](decisions/044-defer-webtransport-browsers-use-websocket.md) (defer `h3`/WebTransport; browsers use WebSocket for the bidirectional call-protocol path; a scope decision per ADR-009 §"What this framework is NOT"; reversal trigger = a concrete ALPN-stream-proxy use case; states the "browser is not a peer" rationale — addressability vs. bidirectionality — that amends ADR-034 §4). ADR-003 Amendment 1 clarifies that `alknet-call` is a protocol-foundation crate (the `alknet-http``alknet-call` dependency edge). A consistency review pass corrected drift from the mid-spec pivot (the `to_openapi` gateway pattern landed in the prose but not in cross-references; the WebTransport specs inherited the OpenAPI/MCP direction assumption that doesn't hold for the call protocol) — ADR-036's `to_openapi` clause is now amended as superseded by ADR-042, ADR-034 §5's "deferral bucket" wording is corrected (the decision stands), and the http specs now name the one-directional HTTP projection vs. the bidirectional WebSocket (and, when revived, WebTransport) substrate. The specs are in draft; implementation has not started. Three open questions carried: OQ-38 (WebTransport standalone relay service scope — distinct from the in-process ALPN-stream-proxy resolved by ADR-040), OQ-39 (`to_openapi` published-spec versioning), OQ-40 (reqwest client config).
**Next step**: The storage/repo-pattern ADRs (030033) are accepted and amend the core and call specs. The next implementation phase is the ADR-029 migration (peer-keyed overlays, `PeerRef` routing, retire `remote_safe`/`trusted_peer`) with the ADR-030 `PeerEntry` change and the ADR-032 `forwarded_for` field folded in — the `OperationContext`, `from_call` handler, and `AuthPolicy` are all under edit, making this the cheapest window. After that: alknet-http implementation (specs drafted, ADRs 036038 proposed), which consumes the `CredentialStore` trait and the `OperationAdapter` contract. The alknet-ssh crate (the other post-core crate, specced in parallel) proceeds independently — it depends on `alknet-core`, not `alknet-call`. **Next step**: The storage/repo-pattern ADRs (030033) are accepted and amend the core and call specs. The next implementation phase is the ADR-029 migration (peer-keyed overlays, `PeerRef` routing, retire `remote_safe`/`trusted_peer`) with the ADR-030 `PeerEntry` change and the ADR-032 `forwarded_for` field folded in — the `OperationContext`, `from_call` handler, and `AuthPolicy` are all under edit, making this the cheapest window. After that: alknet-http implementation (specs drafted; `h3`/WebTransport deferred per ADR-044, browser bidirectional path uses WebSocket), which consumes the `CredentialStore` trait and the `OperationAdapter` contract. The alknet-ssh crate (the other post-core crate, specced in parallel) proceeds independently — it depends on `alknet-core`, not `alknet-call`.
## Architecture Documents ## Architecture Documents
@@ -39,10 +39,10 @@ The alknet-call crate is **implemented and reviewed** — both the server-side c
| [crates/call/client-and-adapters.md](crates/call/client-and-adapters.md) | draft | CallClient (outbound connection opener), from_call / from_jsonschema, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern | | [crates/call/client-and-adapters.md](crates/call/client-and-adapters.md) | draft | CallClient (outbound connection opener), from_call / from_jsonschema, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern |
| [crates/http/README.md](crates/http/README.md) | draft | alknet-http crate index | | [crates/http/README.md](crates/http/README.md) | draft | alknet-http crate index |
| [crates/http/overview.md](crates/http/overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map | | [crates/http/overview.md](crates/http/overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map |
| [crates/http/http-server.md](crates/http/http-server.md) | draft | HttpAdapter for h2/http1.1, axum over QUIC, Bearer auth, stealth, /healthz | | [crates/http/http-server.md](crates/http/http-server.md) | draft | HttpAdapter for h2/http1.1 + WebSocket browser path, axum over QUIC, Bearer auth, stealth, /healthz |
| [crates/http/http-adapters.md](crates/http/http-adapters.md) | draft | from_openapi (reqwest) and to_openapi (projection); no-env-vars injection point | | [crates/http/http-adapters.md](crates/http/http-adapters.md) | draft | from_openapi (reqwest) and to_openapi (projection); no-env-vars injection point |
| [crates/http/http-mcp.md](crates/http/http-mcp.md) | draft | from_mcp / to_mcp (feature-gated), streamable-HTTP-only, stdio exclusion | | [crates/http/http-mcp.md](crates/http/http-mcp.md) | draft | from_mcp / to_mcp (feature-gated), streamable-HTTP-only, stdio exclusion |
| [crates/http/webtransport.md](crates/http/webtransport.md) | draft | h3/WebTransport handler — the browser streaming path | | [crates/http/webtransport.md](crates/http/webtransport.md) | deferred | h3/WebTransport handler — deferred per ADR-044; browser bidirectional path uses WebSocket (see http-server.md). Spec kept intact for revival. |
| [crates/vault/README.md](crates/vault/README.md) | stable | alknet-vault crate index | | [crates/vault/README.md](crates/vault/README.md) | stable | alknet-vault crate index |
| [crates/vault/mnemonic-derivation.md](crates/vault/mnemonic-derivation.md) | stable | BIP39, SLIP-0010, BIP-0032, derivation paths, key types | | [crates/vault/mnemonic-derivation.md](crates/vault/mnemonic-derivation.md) | stable | BIP39, SLIP-0010, BIP-0032, derivation paths, key types |
| [crates/vault/encryption.md](crates/vault/encryption.md) | stable | AES-256-GCM, EncryptedData, key versioning, salt (Phase B reserved) | | [crates/vault/encryption.md](crates/vault/encryption.md) | stable | AES-256-GCM, EncryptedData, key versioning, salt (Phase B reserved) |
@@ -90,12 +90,13 @@ The alknet-call crate is **implemented and reviewed** — both the server-side c
| [035](decisions/035-concrete-persistence-adapter-shapes.md) | Concrete Persistence Adapter Shapes — Read/Write Split, honker+SQLite | Accepted | | [035](decisions/035-concrete-persistence-adapter-shapes.md) | Concrete Persistence Adapter Shapes — Read/Write Split, honker+SQLite | Accepted |
| [036](decisions/036-http-to-call-operation-mapping.md) | HTTP-to-Call Operation Mapping | Proposed | | [036](decisions/036-http-to-call-operation-mapping.md) | HTTP-to-Call Operation Mapping | Proposed |
| [037](decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Proposed | | [037](decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Proposed |
| [038](decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | Proposed | | [038](decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | ~~Proposed~~**Superseded** by ADR-044 |
| [039](decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | Proposed | | [039](decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | Proposed |
| [040](decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | Proposed | | [040](decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | Proposed**parked** (implementation deferred per ADR-044) |
| [041](decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | Proposed | | [041](decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | Proposed |
| [042](decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | Proposed | | [042](decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | Proposed |
| [043](decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | Proposed | | [043](decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | Proposed**parked** (implementation deferred per ADR-044; §2/§3 transfer to WebSocket) |
| [044](decisions/044-defer-webtransport-browsers-use-websocket.md) | Defer h3/WebTransport; Browsers Use WebSocket | Accepted |
## Open Questions ## Open Questions

View File

@@ -1,23 +1,26 @@
--- ---
status: draft status: draft
last_updated: 2026-06-29 last_updated: 2026-06-30
--- ---
# alknet-http # alknet-http
HTTP interface for alknet: serves HTTP/1.1, HTTP/2, and HTTP/3 (WebTransport) HTTP interface for alknet: serves HTTP/1.1 and HTTP/2 on standard ALPNs
on standard ALPNs, and hosts the HTTP-backed call-protocol adapters (with WebSocket upgrade for browser bidirectional access to the call
(`from_openapi`, `to_openapi`, `from_mcp`, `to_mcp`). 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 ## Documents
| Document | Status | Description | | Document | Status | Description |
|----------|--------|-------------| |----------|--------|-------------|
| [overview.md](overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map | | [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-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 | | [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 ## 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 | | [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 | | [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 | | [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 | | [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; WebTransport relay-as-proxy recorded | | [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 | | [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 | | [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) | | [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 | | [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 | | [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 ## Relevant Open Questions
| OQ | Title | Status | Relevance | | OQ | Title | Status | Relevance |
|----|-------|--------|-----------| |----|-------|--------|-----------|
| OQ-11 | Handler-level auth resolution observability | resolved | HTTP handler stores resolved identity on `Connection` via `set_identity` | | 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-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-17 | Call protocol client and adapter contract | resolved | `OperationAdapter` trait; `to_*` projections |
| OQ-24 | Operation error schemas | resolved | `from_openapi`/`to_openapi` error fidelity | | 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 ## Key Design Principles
1. **HTTP is both a server surface and a client transport for adapters.** 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 Inbound HTTP (`h2`/`http/1.1` + WebSocket upgrade) is served by `axum`
stream; outbound HTTP (`from_openapi`/`from_mcp` forwarding) uses over a QUIC stream; outbound HTTP (`from_openapi`/`from_mcp`
`reqwest`. Both directions share the same HTTP dependencies, which is forwarding) uses `reqwest`. Both directions share the same HTTP
why they live in one crate rather than being split. See dependencies, which is why they live in one crate rather than being
[overview.md](overview.md). split. See [overview.md](overview.md).
2. **The HTTP surface is a projection of the call protocol.** An HTTP 2. **The HTTP surface is a projection of the call protocol.** An HTTP
request at `POST /fs/readFile` becomes a `call.requested` for request at `POST /fs/readFile` becomes a `call.requested` for
`/fs/readFile`. The HTTP path IS the operation path on the `/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) (direct-call surface) and [ADR-042](../../decisions/042-openapi-gateway-pattern.md)
(`to_openapi` gateway, superseding ADR-036's original `to_openapi` (`to_openapi` gateway, superseding ADR-036's original `to_openapi`
clause). 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) IANA-registered ALPN strings. Any HTTP client (browser, curl, axios)
connects without knowing about alknet — the TLS handshake negotiates connects without knowing about alknet — the TLS handshake negotiates
`h2` or `http/1.1` normally. This is the stealth mapping (ADR-010). `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, arbitrary executable = RCE. Streamable HTTP is network-isolated,
auth-gatable, and runs under alknet's auth model. See auth-gatable, and runs under alknet's auth model. See
[ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md). [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md).
6. **HTTP/3 + WebTransport is a first-class transport, not a deferral.** 6. **WebSocket is the browser bidirectional path.** A browser upgrades
The browser streaming path uses QUIC streams directly. See an HTTP/1.1 or HTTP/2 request to WebSocket and speaks the call
[ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md). protocol over binary WS messages — full-duplex, both sides can
7. **The `h3` handler is an ALPN-stream-proxy for browsers.** A browser initiate calls (the call protocol's native bidirectionality, ADR-012).
with a WASM parser can reach any non-call ALPN handler (SSH, git, HTTP/3 + WebTransport (`h3`) is deferred per
SFTP) via WebTransport — no install, no native client, no VPN. The [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md)
call protocol needs no proxy (it speaks EventEnvelope directly); — a scope decision (the browser bidirectional path doesn't require
the ALPN-stream-proxy is the substrate's mechanism for the protocols WebTransport's stream model; WebSocket suffices). The reversal
that need a client-side parser. SSH-over-WebTransport is trigger is a concrete ALPN-stream-proxy use case (a browser running
HTTPS-shaped at the network layer (anti-censorship). See a WASM SSH/SFTP/git client).
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) 7. **Browsers are not alknet peers.** A browser over WebSocket (or, when
and [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md). it revives, WebTransport) authenticates by bearer token, gets no
8. **`h3` requires X.509.** Browsers don't support RFC 7250 raw keys `PeerId`, and its registered ops land in a connection-local Layer 2
(ADR-027). A node serving WebTransport must have an X.509 identity. overlay. "Peer" means an addressable node in the call-protocol peer
This is a browser limitation, not an alknet decision. graph (stable `PeerId`, `PeerRef::Specific`-reachable, identity
9. **WebTransport is a bidirectional ALPN transport substrate.** stable across reconnects) — not "any endpoint that exchanges calls
WebTransport carries ALPN protocols as bidirectional streams; the during a live session." A browser is the second but not the first: no
call protocol is the first/canonical target (JSON-RPC over QUIC stable cryptographic identity of its own, ephemeral, not addressable
streams, needs no WASM parser, runs in Deno/Node/browsers/native from other nodes. See
Rust). Both sides of a WebTransport call-protocol session can [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)
initiate calls — the call protocol's bidirectionality applies §4 (amended by ADR-044 §5 with the addressability rationale).
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).
## References ## References
- `docs/research/alknet-http/phase-0-findings.md` — Phase 0 research - `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 - `docs/research/alknet-call-completion/gap-analysis.md` — adapter
location map, no-env-vars invariant location map, no-env-vars invariant
- `/workspace/@alkdev/operations/src/from_openapi.ts`, - `/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 - `/workspace/rust-sdk/` — MCP Rust SDK (rmcp v1.8.0); streamable HTTP
transport examples transport examples
- `/workspace/wtransport/` — pure-Rust WebTransport reference - `/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 status: draft
last_updated: 2026-06-29 last_updated: 2026-06-30
--- ---
# HTTP Server # HTTP Server
The `HttpAdapter` — the `ProtocolHandler` for `h2` and `http/1.1` (and 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 covers how axum is run over a QUIC bidirectional stream, Bearer auth
resolution, the HTTP-to-call dispatch, the `/healthz` raw route, and resolution, the HTTP-to-call dispatch, the `/healthz` raw route, stealth
stealth decoy. decoy, and the WebSocket browser path.
## What ## 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 The endpoint's `HandlerRegistry` maps each ALPN byte string to the same
adapter instance; `handle()` branches on `connection.remote_alpn()` to adapter instance; `handle()` branches on `connection.remote_alpn()` to
pick the HTTP framing. For `http/1.1` and `h2`, the framing is hyper's 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 HTTP/1.1 or HTTP/2 over a QUIC bidirectional stream. WebSocket upgrade
WebTransport/HTTP/3 path (see [webtransport.md](webtransport.md)). (§"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 ## 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 sends `call.aborted` for the in-flight subscription, which cascades
to descendants per ADR-016. to descendants per ADR-016.
This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebTransport This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket
(`h3`), the subscription projects directly onto a WebTransport (§"WebSocket browser path" below), the subscription projects directly
bidirectional stream — no SSE framing (see [webtransport.md](webtransport.md)). 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) ### 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. *results* flow server→client.
This is a structural property of HTTP, not a design choice in this This is a structural property of HTTP, not a design choice in this
crate. WebTransport (`h3`) restores the bidirectional call model: a crate. **WebSocket restores the bidirectional call model for browsers**
WebTransport session is a long-lived connection over which either side (see §"WebSocket browser path" below): a WS connection is a long-lived
can open bidirectional streams and send `call.requested` events in full-duplex channel over which either side can send `call.requested`
either direction — the call protocol's native bidirectionality applies frames in either direction — the call protocol's native bidirectionality
unchanged. See [webtransport.md](webtransport.md) and ADR-043. The applies unchanged (ADR-012 — stream-agnostic correlation; a WS message
HTTP/1.1 + HTTP/2 surface is the projection for clients that only speak stream is another `BiStream`-satisfying transport). WebTransport (`h3`)
HTTP; WebTransport is the surface for clients that can speak the call would restore it via native multi-stream multiplexing, but WebTransport
protocol in both directions. 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 ### 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 Capabilities are used for outbound calls (`from_openapi`), never
serialized into HTTP response bodies. serialized into HTTP response bodies.
- **`/healthz` is raw.** No auth, no call protocol. The one raw route. - **`/healthz` is raw.** No auth, no call protocol. The one raw route.
- **The `h3` ALPN is a first-class transport.** The `HttpAdapter` - **WebSocket is the browser bidirectional path (ADR-044).** A browser
registers for `h3` when the `h3` feature is enabled (ADR-038). The upgrades an HTTP request to WS and speaks the call protocol over binary
`h3` handler is covered in [webtransport.md](webtransport.md); this messages. `h3`/WebTransport is deferred (ADR-044); the ALPN-stream-proxy
document covers the `h2`/`http/1.1` path. (ADR-040) is not available in v1. The `h3` ALPN and its feature gate are
not implemented in the initial release.
## Design Decisions ## 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 | | `/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 | | 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) | | 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 | | 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 ## 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 - [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) — the
HTTP-to-call mapping this server implements HTTP-to-call mapping this server implements
- [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) - [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md)
the `h3`/WebTransport companion to this server 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 - [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` - [http-adapters.md](http-adapters.md) — `from_openapi`/`to_openapi`
- [../core/auth.md](../core/auth.md) — `IdentityProvider`, Bearer → - [../core/auth.md](../core/auth.md) — `IdentityProvider`, Bearer →
`resolve_from_token` `resolve_from_token`

View File

@@ -1,13 +1,14 @@
--- ---
status: draft status: draft
last_updated: 2026-06-29 last_updated: 2026-06-30
--- ---
# alknet-http — Overview # alknet-http — Overview
The HTTP interface crate: serves inbound HTTP on standard ALPNs and hosts The HTTP interface crate: serves inbound HTTP on standard ALPNs (with
the HTTP-backed call-protocol adapters. This document covers the crate's WebSocket upgrade for browser bidirectional access) and hosts the
two roles, its dependency edges, and the adapter location map. Component 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. details are in the sibling documents.
## What ## What
@@ -16,11 +17,14 @@ details are in the sibling documents.
architecture. It serves two roles in one crate: architecture. It serves two roles in one crate:
1. **HTTP server** — a `ProtocolHandler` (`HttpAdapter`) that accepts 1. **HTTP server** — a `ProtocolHandler` (`HttpAdapter`) that accepts
HTTP/2, HTTP/1.1, and HTTP/3 (WebTransport) connections on the HTTP/2 and HTTP/1.1 connections on the standard IANA ALPNs (`h2`,
standard IANA ALPNs (`h2`, `http/1.1`, `h3`). It serves REST APIs, the `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, `to_openapi`/`to_mcp` projections of local call-protocol operations,
the `/healthz` operational endpoint, and the decoy surface for 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 2. **HTTP client host** — the home of the HTTP-transport-backed call
adapters: `from_openapi` (import external HTTP APIs as call adapters: `from_openapi` (import external HTTP APIs as call
operations, using `reqwest` for outbound calls) and `from_mcp` (import 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 CLI binary, a hub deployment, a browser-facing service) wires
`HttpAdapter` into the `HandlerRegistry` for the standard HTTP ALPNs and `HttpAdapter` into the `HandlerRegistry` for the standard HTTP ALPNs and
gets a full HTTP surface: REST projection of the call protocol, OpenAPI 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 The key architectural insight that shapes the crate: **HTTP is both a
server surface and a client transport for adapters.** The server side 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 inherits HTTP's request/response constraint and projects the call
protocol one-directionally (client→server calls only — see protocol one-directionally (client→server calls only — see
[http-server.md](http-server.md) §"One-directional projection"). [http-server.md](http-server.md) §"One-directional projection").
WebTransport (`h3`) is the HTTP-family transport that restores the **WebSocket is the HTTP-family transport that restores the call
call protocol's native bidirectionality — it is a transport substrate protocol's native bidirectionality for browsers** (ADR-044) — a WS
for the call protocol (and, via the ALPN-stream-proxy, for any ALPN), connection is a long-lived full-duplex channel over which either side
not a browser→hub one-way path. See [webtransport.md](webtransport.md) can send `call.requested` frames in either direction. WebTransport
and ADR-043. The "from/to" naming of the OpenAPI/MCP adapters should not (`h3`, deferred) would restore it via native multi-stream multiplexing;
be read as a statement about the call protocol's directionality; it is WebSocket restores it via framed messages over one connection. The
a statement about OpenAPI's and MCP's directionality. "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 ## Dependencies
@@ -79,13 +87,22 @@ alknet-http
├── alknet-core (ProtocolHandler, Connection, AuthContext, IdentityProvider, Capabilities) ├── alknet-core (ProtocolHandler, Connection, AuthContext, IdentityProvider, Capabilities)
├── alknet-call (OperationAdapter, OperationSpec, Handler, HandlerRegistration, ├── alknet-call (OperationAdapter, OperationSpec, Handler, HandlerRegistration,
│ OperationRegistry, AdapterError, OperationProvenance) │ 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) ├── reqwest (HTTP client — from_openapi/from_mcp forwarding)
├── hyper (HTTP/1.1 + HTTP/2 framing; axum is built on hyper) ├── 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`) └── 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) ### The `alknet-call` dependency (ADR-003 Amendment 1)
`alknet-http` depends on `alknet-call`. ADR-003's rule is "no handler `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? | | ALPN | Handler | Transport | Browser? |
|------|---------|-----------|----------| |------|---------|-----------|----------|
| `http/1.1` | `HttpAdapter` | HTTP/1.1 over QUIC stream | No | | `http/1.1` | `HttpAdapter` | HTTP/1.1 over QUIC stream (+ WS upgrade) | Yes (WS upgrade for bidirectional) |
| `h2` | `HttpAdapter` | HTTP/2 over QUIC stream | No | | `h2` | `HttpAdapter` | HTTP/2 over QUIC stream (+ WS upgrade) | Yes (WS upgrade for bidirectional) |
| `h3` | `HttpAdapter` | HTTP/3 / WebTransport | Yes (X.509 required) | | `h3` | — (deferred) | HTTP/3 / WebTransport | Deferred per ADR-044 |
These are standard IANA ALPN strings, not `alknet/`-prefixed. Any HTTP These are standard IANA ALPN strings, not `alknet/`-prefixed. Any HTTP
client connects without knowing about alknet — the TLS handshake client connects without knowing about alknet — the TLS handshake
negotiates `h2` or `http/1.1` normally, and the `HttpAdapter` serves negotiates `h2` or `http/1.1` normally, and the `HttpAdapter` serves
HTTP. This is the stealth mapping (ADR-010): clients that don't offer 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 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 The `HttpAdapter` registers for `http/1.1` and `h2`. The endpoint's
features are enabled). The endpoint's `HandlerRegistry` maps each ALPN to `HandlerRegistry` maps each ALPN to the same `HttpAdapter` instance;
the same `HttpAdapter` instance; the handler branches on the handler branches on `connection.remote_alpn()` to pick the right
`connection.remote_alpn()` to pick the right framing. framing. The `h3` ALPN is not registered in v1 (deferred per ADR-044).
## Adapter Location Map ## Adapter Location Map
@@ -139,7 +157,7 @@ alknet-call (lean — no HTTP client, no HTTP server)
└── CallClient (outbound connection opener) └── CallClient (outbound connection opener)
alknet-http (owns HTTP server + HTTP client) 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) ├── from_openapi (parse OpenAPI doc + reqwest forwarding handler)
├── to_openapi (generate OpenAPI doc from local registry) ├── to_openapi (generate OpenAPI doc from local registry)
├── from_mcp (feature-gated) (import remote MCP tools over streamable HTTP — reqwest) ├── from_mcp (feature-gated) (import remote MCP tools over streamable HTTP — reqwest)
@@ -155,24 +173,27 @@ directions.
```toml ```toml
[features] [features]
default = ["h2", "http1"] # the non-browser HTTP surface default = ["h2", "http1"] # the HTTP surface (incl. WebSocket upgrade for browsers)
h3 = ["dep:wtransport"] # HTTP/3 + WebTransport (browser path; X.509 required)
mcp = ["dep:rmcp"] # from_mcp / to_mcp (streamable HTTP only — ADR-037) 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 - `h2` + `http1` (default): the `axum` + `hyper` HTTP/1.1 + HTTP/2
server. This is the surface non-browser clients use. server, including WebSocket upgrade for browser bidirectional access
- `h3`: the `wtransport` (or quinn HTTP/3 extension) dependency. Adds (ADR-044). This is the surface all clients — including browsers, via
the `h3` ALPN handler and the WebTransport streaming path. See WS upgrade — use in v1.
[webtransport.md](webtransport.md) and
[ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md).
- `mcp`: the `rmcp` dependency with streamable HTTP transport features - `mcp`: the `rmcp` dependency with streamable HTTP transport features
only. Adds `from_mcp`/`to_mcp`. See [http-mcp.md](http-mcp.md) and only. Adds `from_mcp`/`to_mcp`. See [http-mcp.md](http-mcp.md) and
[ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md). [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md).
A deployment that only needs the REST surface (no browsers, no MCP) uses A deployment that only needs the REST surface (no MCP) uses the default
the default features. A browser-facing hub enables `h3`. A deployment features. A browser-facing hub also uses the default features — the
that wants MCP tool import/export enables `mcp`. 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 ## The No-Env-Vars Invariant
@@ -202,18 +223,19 @@ verified against this invariant. See ADR-014 and
## Architecture (component pointers) ## Architecture (component pointers)
- **[http-server.md](http-server.md)** — the `HttpAdapter` for `h2`/ - **[http-server.md](http-server.md)** — the `HttpAdapter` for `h2`/
`http/1.1`: how axum is run over a QUIC bidirectional stream, Bearer `http/1.1` (+ WebSocket upgrade): how axum is run over a QUIC
auth resolution, the `/healthz` raw route, stealth decoy, and the bidirectional stream, Bearer auth resolution, the `/healthz` raw route,
HTTP-to-call dispatch (ADR-036). 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 - **[http-adapters.md](http-adapters.md)** — `from_openapi` (parse
OpenAPI, build forwarding handlers with `reqwest`) and `to_openapi` OpenAPI, build forwarding handlers with `reqwest`) and `to_openapi`
(generate an OpenAPI doc from the registry's `External` operations). (generate an OpenAPI doc from the registry's `External` operations).
Error fidelity per ADR-023. Error fidelity per ADR-023.
- **[http-mcp.md](http-mcp.md)** — `from_mcp`/`to_mcp` (feature-gated), - **[http-mcp.md](http-mcp.md)** — `from_mcp`/`to_mcp` (feature-gated),
streamable HTTP only (ADR-037), the rmcp integration. streamable HTTP only (ADR-037), the rmcp integration.
- **[webtransport.md](webtransport.md)** — the `h3` ALPN handler, - **[webtransport.md](webtransport.md)** — the deferred `h3` ALPN
WebTransport session/stream handling, the browser streaming path handler (HTTP/3 + WebTransport). **Deferred per ADR-044**; kept intact
(ADR-038). for revival when a concrete ALPN-stream-proxy use case arrives.
## Design Decisions ## 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 | | 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) | | 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) | | 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_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 | | `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) | | `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) | | 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) | | 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) | | `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) | | `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) | | 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 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) | Browser over WebTransport/HTTPS = bearer token, no `PeerId` (settled) | | 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 ## 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_openapi.ts`,
`/workspace/@alkdev/operations/src/from_mcp.ts` — TypeScript prior art `/workspace/@alkdev/operations/src/from_mcp.ts` — TypeScript prior art
- `/workspace/rust-sdk/` — MCP Rust SDK (rmcp); streamable HTTP examples - `/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 status: deferred
last_updated: 2026-06-29 last_updated: 2026-06-30
--- ---
# WebTransport — the h3 ALPN handler # 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 The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and
WebTransport. WebTransport is a **bidirectional ALPN transport WebTransport. WebTransport is a **bidirectional ALPN transport
substrate** (ADR-043) — it carries ALPN protocols as bidirectional substrate** (ADR-043) — it carries ALPN protocols as bidirectional
@@ -386,13 +419,19 @@ as a first-class transport.
## Design Decisions ## 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 | | 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 | | ~~`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) | Carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls | | 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) | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser | | 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) | | 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) | Bearer token, no `PeerId` | | 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 | | 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 | | `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 | | Stealth on h3 | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Unknown paths get the decoy |

View File

@@ -203,8 +203,8 @@ A browser reaching a hub over WebTransport (or HTTPS) is served by the
hub's `alknet-http` handler. The browser authenticates by **bearer hub's `alknet-http` handler. The browser authenticates by **bearer
token** (HTTP `Authorization`), resolved by the hub's token** (HTTP `Authorization`), resolved by the hub's
`IdentityProvider::resolve_from_token` against the hub's `IdentityProvider::resolve_from_token` against the hub's
`PeerEntry.auth_token_hash` or `ApiKeyEntry`. The browser is **not** an `PeerEntry.auth_token_hash` or `ApiKeyEntry`. The browser is **not an
alknet peer on the hub's side either — it does not get a `PeerId`, does alknet peer on the hub's side either** — it does not get a `PeerId`, does
not enter `PeerCompositeEnv`, and its "ops" are HTTP routes / WebTransport not enter `PeerCompositeEnv`, and its "ops" are HTTP routes / WebTransport
streams served by `alknet-http`, not entries in the call-protocol streams served by `alknet-http`, not entries in the call-protocol
peer-keyed overlay. The hub's `PeerEntry` for the browser (if any) is peer-keyed overlay. The hub's `PeerEntry` for the browser (if any) is
@@ -214,6 +214,29 @@ This keeps the peer graph populated only by full alknet nodes (role 3
hubs and role-3-style spoke nodes), never by browsers or pure HTTP hubs and role-3-style spoke nodes), never by browsers or pure HTTP
clients. clients.
> **Amendment (rationale added by
> [ADR-044](044-defer-webtransport-browsers-use-websocket.md) §5):** The
> closure above is correct but states the conclusion without the
> supporting argument. The distinction that makes it correct is:
> **"peer" in alknet means an addressable node in the call-protocol peer
> graph** — a stable `PeerId`, reachable via `PeerRef::Specific`, whose
> ops land in `PeerCompositeEnv`, whose identity is stable across
> reconnects. It does *not* mean "any endpoint that exchanges calls
> during a live session." A browser is the second thing but not the
> first, on three concrete grounds: (1) no stable cryptographic identity
> of its own (it presents a bearer token the hub issued; nothing to
> pin), (2) ephemeral (close the tab → connection dies → the
> connection-local overlay dies with it; a `PeerEntry` keyed to a browser
> would be dead within seconds), (3) 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 Layer 2 overlay
> (ADR-043 §3, the inbound mirror of §2 above) is what gives the browser
> bidirectional-call capability *without* peer-graph membership. This
> rationale is transport-agnostic — it applies to WebSocket (the v1
> browser path, ADR-044) and to WebTransport (when it revives) equally.
> See ADR-044 §5 for the full statement.
### 5. WebTransport relay-as-proxy is a transport-only feature, scoped separately ### 5. WebTransport relay-as-proxy is a transport-only feature, scoped separately
A **WebTransport proxy** that terminates the browser's WebTransport A **WebTransport proxy** that terminates the browser's WebTransport
@@ -237,12 +260,15 @@ is a real feature, especially for the browser-to-P2P-peer case. It is
> WebTransport lands." That framing was a residual of the "two-way door > WebTransport lands." That framing was a residual of the "two-way door
> as deferral" anti-pattern (ADR-009 §"What this framework is NOT") > as deferral" anti-pattern (ADR-009 §"What this framework is NOT")
> that [ADR-038](038-http3-and-webtransport-as-first-class.md) was later > that [ADR-038](038-http3-and-webtransport-as-first-class.md) was later
> written to reject — `h3`/WebTransport is a first-class transport, in > written to reject. ADR-038 has since been **superseded by
> scope, not deferred. The *auth-model* decision in this §5 (the proxy > [ADR-044](044-defer-webtransport-browsers-use-websocket.md)**, which
> is transport-only; it does not change identity resolution) is > re-defers `h3`/WebTransport as a genuine scope decision (the browser
> unchanged. The *scope* question (does the proxy belong in > bidirectional path uses WebSocket; WebTransport revives when a concrete
> `alknet-http` or a separate relay crate?) is tracked as OQ-38 — a > ALPN-stream-proxy use case arrives). The *auth-model* decision in this
> genuine scope question, not a deferral. > §5 (the proxy is transport-only; it does not change identity
> resolution) is unchanged by either ADR. The *scope* question (does the
> proxy belong in `alknet-http` or a separate relay crate?) is tracked
> as OQ-38 — a genuine scope question, not a deferral.
### 6. On-chain / smart-contract peer discovery fits the OQ-36 adapter pattern ### 6. On-chain / smart-contract peer discovery fits the OQ-36 adapter pattern
@@ -340,11 +366,17 @@ It is noted here only to confirm it does not reopen OQ-37.
intended — it is the same model iroh uses. intended — it is the same model iroh uses.
2. **Browsers never enter the peer-keyed overlay.** A browser is 2. **Browsers never enter the peer-keyed overlay.** A browser is
served by `alknet-http` (HTTP routes / WebTransport streams) and served by `alknet-http` (HTTP routes / WebTransport streams /, per
authenticates by bearer token. The hub may have a `PeerEntry` for ADR-044, WebSocket) and authenticates by bearer token. The hub may
the browser's token (to authorize it), but the browser is not a have a `PeerEntry` for the browser's token (to authorize it), but the
`PeerId`-bearing peer. This is the explicit closure of the browser is not a `PeerId`-bearing peer. This is the explicit closure
"browser as peer" path — browsers are clients, not peers. of the "browser as peer" path — browsers are clients, not peers.
**The rationale** (addressability vs. bidirectionality — a browser
has no stable identity of its own, is ephemeral, and is not
addressable from other nodes) is stated in
[ADR-044](044-defer-webtransport-browsers-use-websocket.md) §5, which
amends §4 above by reference. The closure applies transport-
agnostically.
3. **X.509 fingerprint pinning is only for known hubs.** Pinning an 3. **X.509 fingerprint pinning is only for known hubs.** Pinning an
X.509 fingerprint for an arbitrary public API is brittle (cert X.509 fingerprint for an arbitrary public API is brittle (cert
@@ -395,13 +427,19 @@ It is noted here only to confirm it does not reopen OQ-37.
repo/adapter pattern (trait in core, adapter additive in a separate repo/adapter pattern (trait in core, adapter additive in a separate
crate) crate)
- `docs/research/alknet-http/phase-0-findings.md` — DH-2 (h3 / - `docs/research/alknet-http/phase-0-findings.md` — DH-2 (h3 /
WebTransport; the original "deferred past v1" framing is rejected by WebTransport; the original "deferred past v1" framing was rejected by
ADR-038); the WebTransport-relay-as-proxy feature noted in this ADR's ADR-038, which is now itself superseded by
§5 is a transport-only feature whose scope is tracked as OQ-38 [ADR-044](044-defer-webtransport-browsers-use-websocket.md) — a genuine
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` / scope deferral); the WebTransport-relay-as-proxy feature noted in this
WebTransport is a first-class transport, not deferred (amends the ADR's §5 is a transport-only feature whose scope is tracked as OQ-38
"deferral bucket" wording in this ADR's §5; the auth-model decision - [ADR-038](038-http3-and-webtransport-as-first-class.md) — **superseded
stands) by [ADR-044](044-defer-webtransport-browsers-use-websocket.md)**.
ADR-038 amended the "deferral bucket" wording in this ADR's §5 (the
auth-model decision stands); ADR-044 reverses ADR-038's "h3 in scope
now" decision as a scope deferral (the browser bidirectional path
uses WebSocket; WebTransport revives when a concrete ALPN-stream-proxy
use case arrives). The "browser is not a peer" closure in §4 above is
amended by ADR-044 §5 with the addressability rationale.
- `docs/research/references/iroh/iroh/04-sub-crates.md` — iroh's - `docs/research/references/iroh/iroh/04-sub-crates.md` — iroh's
transport relay (`iroh-relay`), referenced to distinguish it from transport relay (`iroh-relay`), referenced to distinguish it from
alknet's hub role alknet's hub role

View File

@@ -7,7 +7,8 @@ Proposed
## Context ## Context
`alknet-http` implements `ProtocolHandler` for the standard HTTP ALPNs (`h2`, `alknet-http` implements `ProtocolHandler` for the standard HTTP ALPNs (`h2`,
`http/1.1`, `h3`). An inbound HTTP request that targets an alknet operation `http/1.1`; `h3`/WebTransport is deferred per
[ADR-044](044-defer-webtransport-browsers-use-websocket.md)). An inbound HTTP request that targets an alknet operation
must become a call-protocol `call.requested` dispatch — the HTTP handler is a must become a call-protocol `call.requested` dispatch — the HTTP handler is a
*projection* of the call protocol, not a parallel routing layer. The *projection* of the call protocol, not a parallel routing layer. The
question is how an HTTP request maps to an operation invocation. question is how an HTTP request maps to an operation invocation.
@@ -96,9 +97,11 @@ A `Subscription` operation served over HTTP/1.1 or HTTP/2 projects its
`call.responded` stream as Server-Sent Events. Each `call.responded` event `call.responded` stream as Server-Sent Events. Each `call.responded` event
becomes an SSE `data:` frame; `call.completed` closes the SSE stream; becomes an SSE `data:` frame; `call.completed` closes the SSE stream;
`call.aborted` closes the stream with an SSE error event. This is the `call.aborted` closes the stream with an SSE error event. This is the
HTTP/1.1 + HTTP/2 streaming projection. Over WebTransport (`h3`), the HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket (the v1 browser
subscription projects directly onto a WebTransport bidirectional stream — bidirectional path, ADR-044), the subscription projects directly onto the
no SSE framing is needed (see ADR-038 for the WebTransport path). WS connection — `call.responded` events as binary WS messages, no SSE
framing. WebTransport (`h3`) would project onto WebTransport bidirectional
streams but is deferred per ADR-044.
### Auth ### Auth

View File

@@ -2,7 +2,24 @@
## Status ## Status
Proposed **Superseded by [ADR-044](044-defer-webtransport-browsers-use-websocket.md).**
This ADR's *correction* of the "two-way-door-as-deferral" anti-pattern
(ADR-009 §"What this framework is NOT") stands as a document — the
anti-pattern is real, and the reasoning that rejected deferral-as-hedging is
correct. However, this ADR's *specific decision* — that `h3`/WebTransport is
in scope now, not deferred — is reversed by ADR-044. ADR-044 is a
**scope** decision (permitted by ADR-009: "not needed for the current
scope"), not a hedging deferral: the browser bidirectional path uses
WebSocket (RFC 6455, mature, native axum support), the ALPN-stream-proxy
(ADR-040) is the speculative use case whose deferral is the reversal
trigger, and the draft-standard + experimental-deps surface area is not
justified by a concrete v1 requirement.
ADR-040 and ADR-043 are **parked, not superseded** — their designs revive
unchanged when WebTransport revives. See ADR-044 for the full scope rationale,
the reversal trigger, and the research note on the wtransport-vs-hyperium
dependency choice (recorded for the revival so it is not re-derived).
## Context ## Context

View File

@@ -2,7 +2,19 @@
## Status ## Status
Proposed **Proposed — implementation deferred per [ADR-044](044-defer-webtransport-browsers-use-websocket.md).**
This ADR's decision is correct and is not superseded. It revives unchanged
when WebTransport revives. ADR-044 defers `h3`/WebTransport as a scope
decision (the browser bidirectional path uses WebSocket for v1; the
ALPN-stream-proxy is the speculative use case whose concrete need is the
reversal trigger). The proxy is the primary WebTransport-specific feature —
it requires WebTransport's stream model and does not transfer to WebSocket.
When a real deployment needs a browser running a WASM SSH/SFTP/git client to
reach a non-call ALPN, this ADR is un-parked and implemented as written.
The `webtransport.md` spec is kept intact and marked `deferred` so the
revival is unblocking already-written design, not re-deriving it.
## Context ## Context

View File

@@ -214,11 +214,14 @@ require it for the common case.
`/schema` at build time. `/schema` at build time.
4. **`subscribe` (SSE) is the streaming projection for the gateway.** 4. **`subscribe` (SSE) is the streaming projection for the gateway.**
Over `h2`/`http/1.1`, subscriptions are SSE. Over WebTransport Over `h2`/`http/1.1`, subscriptions are SSE. Over WebSocket (the v1
(`h3`), subscriptions project onto WebTransport streams directly browser bidirectional path, ADR-044), subscriptions project onto the
(ADR-038) — the gateway's `/subscribe` is the `h2`/`http/1.1` path; WS connection directly as binary messages — the gateway's `/subscribe`
the WebTransport path is the native call-protocol session is the `h2`/`http/1.1` SSE path; the WebSocket path is the native
(`webtransport.md`). call-protocol session (`http-server.md` §"WebSocket browser path").
WebTransport (`h3`, deferred per ADR-044) would project onto
WebTransport streams; the deferred design is at
`webtransport.md`.
## References ## References
@@ -232,9 +235,11 @@ require it for the common case.
- [ADR-036](036-http-to-call-operation-mapping.md) — the SSE projection - [ADR-036](036-http-to-call-operation-mapping.md) — the SSE projection
for subscriptions over `h2`/`http/1.1` (the gateway's `/subscribe` for subscriptions over `h2`/`http/1.1` (the gateway's `/subscribe`
endpoint uses the same SSE framing) endpoint uses the same SSE framing)
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — the - [ADR-044](044-defer-webtransport-browsers-use-websocket.md) —
WebTransport streaming path (the gateway's `/subscribe` is the WebSocket is the v1 browser bidirectional path; `h3`/WebTransport
`h2`/`http/1.1` path; WebTransport is native) deferred (the gateway's `/subscribe` is the `h2`/`http/1.1` SSE path;
the WS path is the native call-protocol session). ADR-038 is
superseded by ADR-044.
- [ADR-041](041-mcp-tool-gateway-pattern.md) — the sibling gateway - [ADR-041](041-mcp-tool-gateway-pattern.md) — the sibling gateway
pattern for `to_mcp` (4 tools; `subscribe` excluded because MCP tool pattern for `to_mcp` (4 tools; `subscribe` excluded because MCP tool
calls are request/response) calls are request/response)

View File

@@ -2,7 +2,28 @@
## Status ## Status
Proposed **Proposed — implementation deferred per [ADR-044](044-defer-webtransport-browsers-use-websocket.md).**
This ADR's decision is correct and is not superseded. It revives unchanged
when WebTransport revives, **with two transfers to WebSocket that apply
during the deferment**:
- **§2 (call-protocol bidirectionality) transfers to WebSocket unchanged.**
WebSocket is full-duplex; the call protocol's bidirectionality applies over
a WS connection exactly as §2 describes for WebTransport. The browser case
where the client registers no ops remains a use-case scoping, not an
architectural limitation.
- **§3 (the no-`PeerId` connection-local overlay) transfers to WebSocket
unchanged.** A browser over WSS has no `PeerId` on the hub's side for the
same reasons it has none over WebTransport (ADR-044 §5); the
connection-local Layer 2 overlay applies. The pattern is transport-agnostic.
What does **not** transfer to WebSocket is §4 (the non-call-ALPN substrate
mechanism / the ALPN-stream-proxy, ADR-040) and §5's WebTransport-specific
framing. Those require WebTransport's stream model and revive with it.
ADR-044 §3 states the transfer explicitly; ADR-044 §5 states the
"browser is not a peer" rationale (addressability vs. bidirectionality)
that this ADR's §3 relies on but does not argue.
## Context ## Context

View File

@@ -0,0 +1,348 @@
# ADR-044: Defer h3/WebTransport; Browsers Use WebSocket
## Status
Accepted (supersedes ADR-038; parks ADR-040, ADR-043)
## Context
ADR-038 brought `h3`/WebTransport into scope as a first-class HTTP transport,
framed against the "two-way door as deferral" anti-pattern (ADR-009 §"What
this framework is NOT"). ADR-040 (the ALPN-stream-proxy) and ADR-043 (the
bidirectional-substrate reframing) extended it. Three ADRs, one crate-spanning
spec (`webtransport.md`), and a body of design work.
Working through the implementation path surfaced a different concern than the
one ADR-038 was written to correct. ADR-038 correctly rejected *deferral-
as-hedging*; the present decision is *deferral-as-scoping*, which ADR-009
explicitly permits (a decision that "genuinely doesn't need to be made yet
because the use case isn't concrete" — scope management, not door-type
classification). The two must not be
confused. Three concrete findings drove the scope re-evaluation:
### Finding 1 — the browser bidirectional path doesn't require WebTransport
The load-bearing use case for `h3`/WebTransport in v1 is **a browser reaching
the call protocol bidirectionally**. ADR-043 §2 establishes that the call
protocol's bidirectionality applies unchanged over any bidirectional stream —
the `Dispatcher` is stream-agnostic (ADR-012). That property is not unique to
WebTransport streams. **WebSocket is a full-duplex, long-lived connection over
which either side can send framed messages**, and the call protocol's
`EventEnvelope` framing fits a WebSocket binary message boundary cleanly (an
`EventEnvelope` is a self-delimited JSON object; one frame = one WS binary
message). The `call.requested`/`call.responded`/`call.completed`/`call.aborted`
exchange works over WebSocket with no protocol change — the same `Dispatcher`,
the same `PendingRequestMap`, the same correlation by request ID.
What WebTransport gives *over* WebSocket — native multiplexed bidirectional
streams, datagrams, the "carry any ALPN as a stream" substrate framing
(ADR-043) — is genuinely better engineering, but none of it is *required* for
the call protocol from a browser. The call protocol multiplexes multiple calls
over a single connection by request ID (ADR-012); it does not need
WebTransport's per-stream multiplexing. The substrate/proxy framing (ADR-040,
ADR-043) is the thing that *does* benefit from WebTransport's stream model —
and that use case is the speculative one (see Finding 3).
### Finding 2 — WebTransport is a draft standard on an experimental dependency stack
WebTransport over HTTP/3 is still an IETF draft (`draft-ietf-webtrans-http3`,
at `-07` at time of writing), not an RFC. The Rust implementation landscape is
correspondingly immature:
- `wtransport` (the reference read during research) is a complete
pure-Rust implementation, but its own README states it "is not considered
completely production-ready" and "may undergo changes as the WebTransport
specification evolves."
- The hyperium stack (`h3` + `h3-quinn` + `h3-webtransport` + `h3-datagram`)
fits the axum/hyper ecosystem more naturally (h3 produces `http::Request`
types that axum consumes directly, which is load-bearing for the spec's
"HTTP/3 requests go through the same axum `Router`" commitment), but h3's
own README says it is "still very experimental... API could change."
- A research spike would be needed to verify the hyperium stack's
server-side WebTransport API before committing to it — the axum-bridge
feasibility is the load-bearing claim and is not yet confirmed against
actual crate APIs, only against READMEs and design philosophy.
Either choice puts a draft-standard protocol and an experimental Rust
dependency on the security surface of `alknet-http`'s first release. The `h3`
feature gate (ADR-038) isolates the risk for non-browser-facing deployments,
but a browser-facing hub must enable it — so the risk is borne precisely by
the deployment shape that motivates having a browser path at all.
### Finding 3 — the ALPN-stream-proxy is speculative; the call protocol is not
ADR-040 (the ALPN-stream-proxy — a browser with a WASM parser for SSH/SFTP/git
reaching any ALPN handler via WebTransport) is the genuinely compelling
WebTransport use case. It is also the one that is *not* required for v1:
- The call protocol from a browser works over WebSocket (Finding 1).
- The downstream crates unlocked by completing `alknet-http` (the SSH, git,
SFTP crates) do not require WebTransport or the proxy. They expose their
ALPNs natively over QUIC; the proxy is a *browser reachability* feature
for those ALPNs, not a prerequisite for the ALPNs to exist.
- The WASM parsers (the browser-side SSH/SFTP/git clients) are themselves
downstream artifacts not yet built. The proxy is only useful once a parser
exists to consume it.
The proxy is "useful, and cheap-on-top *if* WebTransport already exists" —
but WebTransport does not yet exist, and building it speculatively to enable
a proxy whose consumers do not yet exist is the scope inversion.
### The iroh precedent
iroh's own relay (`iroh-relay`, the DERP-equivalent that provides NAT traversal
fallback) chose **WebSocket (WSS)**, not WebTransport, for its fallback path.
This is a strong signal from a project whose entire design center is QUIC and
P2P connectivity: when the question was "what does a browser need to reach our
protocol bidirectionally," their answer was WSS, not WebTransport. Aligning
with that precedent is not cutting against competent practice — it is
matching it.
## Decision
### 1. Defer `h3`/WebTransport. Browsers reach the call protocol over WebSocket.
The `h3` ALPN, the `h3` feature gate, and the WebTransport dependency stack
are **deferred** — not implemented in the initial `alknet-http` release. A
browser connecting to a hub authenticates by bearer token and upgrades an
HTTP/1.1 or HTTP/2 request to WebSocket. The resulting full-duplex WS
connection carries call-protocol `EventEnvelope` frames as binary WebSocket
messages. The browser is a bidirectional call-protocol client over this
connection, using the same `Dispatcher` and `PendingRequestMap` as the
`alknet/call` QUIC path (ADR-012 — stream-agnostic correlation; a WS message
stream is just another `BiStream`-satisfying transport, extending ADR-012's
stream-agnostic claim from QUIC bidirectional streams to any framed
full-duplex byte channel).
This is a **scope** decision, not a hedging deferral (ADR-009 §"What this
framework is NOT"). The reversal trigger is concrete: **a real deployment that
needs the ALPN-stream-proxy (a browser running a WASM SSH/SFTP/git client to
reach a non-call ALPN)**. When that use case arrives, ADR-038 / ADR-040 /
ADR-043 revive as the design — they are not wrong, they are not-now. No
"v1/later/when-it-arrives" hedging language attaches; the condition is stated
as a concrete trigger.
### 2. ADR-038 is superseded by this ADR.
ADR-038's core decision — that `h3` is in scope, not deferred — is reversed
by this ADR. ADR-038's *correction* of the "two-way-door-as-deferral"
anti-pattern stands as a document (the anti-pattern is real); its specific
decision (h3 in scope now) is superseded. ADR-038 is marked Superseded.
### 3. ADR-040 and ADR-043 are parked, not superseded.
ADR-040 (the ALPN-stream-proxy) and ADR-043 (the bidirectional-substrate
reframing) are **not superseded** — their decisions are correct, and they
revive unchanged when WebTransport revives. They are marked Proposed with an
amendment noting implementation is deferred per this ADR. Two specific
transfers apply during the deferment:
- **ADR-043 §2 (call-protocol bidirectionality over WebTransport) transfers
to WebSocket unchanged.** WebSocket is full-duplex; the call protocol's
bidirectionality applies over a WS connection exactly as ADR-043 §2
describes for WebTransport. The browser case where the client registers
no ops remains a use-case scoping, not an architectural limitation.
- **ADR-043 §3 (the no-`PeerId` connection-local overlay) transfers to
WebSocket unchanged.** A browser over WSS has no `PeerId` on the hub's
side for the same reasons it has none over WebTransport (see §5 below);
the connection-local Layer 2 overlay applies. The pattern is
transport-agnostic.
What does *not* transfer to WebSocket is ADR-040 (the ALPN-stream-proxy) and
ADR-043 §4 (the non-call-ALPN substrate mechanism). Those require
WebTransport's stream model and revive with it. SSH/SFTP/git-over-WSS-from-a-
browser is technically possible (multiplex logical streams over one WS frame
stream) but is not specified here — it is the same speculative use case that
motivates deferring WebTransport, and it is not needed for v1.
### 4. WebSocket is the browser bidirectional path; HTTP/1.1+HTTP/2 remain the one-directional projection.
`alknet-http`'s browser-reachable surface becomes:
| Transport | Direction | Use case |
|-----------|-----------|----------|
| `http/1.1`, `h2` | one-directional (client→server) | HTTP clients (curl, axios, `fetch` for request/response); SSE for subscription streaming (ADR-036) |
| WebSocket (over `http/1.1` or `h2` upgrade) | **bidirectional** | Browser call-protocol clients; the path that restores the call protocol's bidirectionality for browsers |
WebSocket is the surface that **restores the call protocol's bidirectionality
for browsers** (the role ADR-043 §5 assigned to WebTransport). The
one-directional projection that ADR-043 §5 names for HTTP/1.1+HTTP/2 stands
unchanged.
### 5. Browsers over WebSocket are not alknet peers — the rationale, stated.
ADR-034 §4 established that a browser over WebTransport is not an alknet peer
(no `PeerId`, no `PeerCompositeEnv` entry). The same applies to a browser over
WebSocket, and the rationale — which ADR-034 §4 states as a closure without
the supporting argument — is worth making explicit because it is the
load-bearing distinction:
**"Peer" in alknet means an addressable node in the call-protocol peer graph
— a stable `PeerId`, reachable via `PeerRef::Specific`, whose ops land in
`PeerCompositeEnv`, whose identity is stable across reconnects.** It does
*not* mean "any endpoint that exchanges calls during a live session." A
browser is the second thing but not the first, on three concrete grounds:
1. **No stable cryptographic identity of its own.** A `PeerEntry` is anchored
to fingerprints (Ed25519, X.509) that *the peer* presents and the local
node pins. A browser presents a bearer token the *hub* issued; the
"identity" is the hub's bookkeeping for that token, not something the
browser owns or that could be pinned by another node. There is nothing
to put in `PeerEntry.fingerprints`.
2. **Ephemeral.** Close the tab → connection dies → the connection-local
Layer 2 overlay (ADR-043 §3 / ADR-034 §2) dies with it. A `PeerEntry`
keyed to a browser would be a permanently-dead entry within seconds.
`PeerRef::Specific("browser-X")` from another node would route to
nothing.
3. **Not addressable from other nodes.** `PeerRef::Specific` resolves through
`PeerEntry``PeerId`. Another alknet node has no way to reach "the
browser currently connected to hub-A"; the hub holds that connection as a
live `CallConnection` handle, not as a peer-graph entry. The
connection-local overlay is precisely the mechanism that gives the
browser bidirectional-call capability *without* peer-graph membership.
This is the explicit closure of the "browser as peer" path, on both the
inbound (this section) and outbound (ADR-034 §2) sides. The browser is a
**bidirectional call target during a live session**, not a **peer-graph
member**. The connection-local Layer 2 overlay (ADR-024, ADR-043 §3) is what
makes the former possible without requiring the latter.
This rationale applies transport-agnostically — to WebSocket, to WebTransport
when it revives, and to any future browser transport. ADR-034 §4 is amended
by reference to this section.
## Consequences
**Positive:**
- `alknet-http`'s first release does not carry a draft-standard protocol or
an experimental dependency stack on its security surface. The browser path
uses WebSocket, a mature, well-understood, RFC 6455 protocol with first-
class axum support (`axum::extract::ws`).
- The axum-bridge research spike for h3/WebTransport is not on the critical
path. WebSocket upgrade over HTTP/1.1 or HTTP/2 is standard axum territory.
- The downstream crates that `alknet-http` unblocks (SSH, git, SFTP) are not
blocked on WebTransport or the proxy. They expose their ALPNs natively over
QUIC; browser reachability for them is a future WebTransport feature.
- Forward momentum is preserved: the `h3` handler, the feature gate, the
`wtransport`/hyperium decision, and the ALPN-stream-proxy are all real
design work that is already done (ADR-038, ADR-040, ADR-043,
`webtransport.md`). Reviving them is unblocking already-written specs, not
designing from scratch.
**Negative:**
- ADR-038, ADR-040, and ADR-043 are not implemented in the initial release.
Their design work is preserved (the ADRs and `webtransport.md` stay in the
record), but a reader must cross-reference this ADR to know they are
parked. The `webtransport.md` spec is marked `deferred` with a header note.
- The ALPN-stream-proxy (ADR-040) is not available in v1. A browser cannot
reach SSH/SFTP/git ALPNs in the initial release — it can reach the call
protocol over WebSocket, but not the non-call ALPNs. This is the
speculative use case whose deferral this ADR commits; the reversal trigger
is a real deployment needing it.
- WebSocket is a single stream; it lacks WebTransport's native multi-stream
multiplexing. For the call protocol this is fine (correlation is by request
ID, not by stream — ADR-012), but it means a future migration to
WebTransport would be a genuine upgrade, not a no-op. The migration path
is the spec that already exists (`webtransport.md`).
- ADR-043's "WebTransport restores bidirectionality" framing (§5) becomes
"WebSocket restores bidirectionality" for v1. The framing transfer is clean
(§3 above), but the prose in `http-server.md` and the ADRs must reflect it.
## Reversal
This decision reverses when a concrete deployment needs the ALPN-stream-proxy
— i.e., a real use case of a browser running a WASM SSH/SFTP/git client to
reach a non-call ALPN over WebTransport. At that point:
1. The research spike deferred here (verify the hyperium stack's server-side
WebTransport API and the axum-bridge feasibility — see §"Research note"
in `webtransport.md`) is run.
2. ADR-038 / ADR-040 / ADR-043 are un-parked and implemented as written,
with the `webtransport.md` spec as the design.
3. The WebSocket browser path (this ADR's §4) is not removed — it remains as
the simpler browser path for deployments that don't need WebTransport's
stream model. The two coexist.
The reversal is a one-way door at the *crate surface* (the `h3` feature gate
becomes part of the published interface) but a two-way door at the
*architecture* (the `webtransport.md` design already exists; reviving it is
implementation work, not redesign). The `webtransport.md` spec is kept intact
and marked `deferred` so the revival is unblocking, not re-deriving.
## Research note (for revival)
A note for the revival: `wtransport` (the reference implementation read during
initial research) is *probably not* the right dependency choice, despite
being a complete and readable implementation. The load-bearing integration
concern is that `alknet-http`'s `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`) 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. 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 `webtransport.md` §"Research note" for the cross-reference.
## Assumptions
1. **The call protocol's `EventEnvelope` framing fits a WebSocket binary
message boundary cleanly.** An `EventEnvelope` is a self-delimited JSON
object; one envelope per WS binary message. No streaming deserializer
across message boundaries is needed. This is verified by implementation
when the WS browser path is built, not by a separate research spike — the
call protocol spec (`call-protocol.md`) and the EventEnvelope shape
already make this property clear, and WebSocket binary messages are a
standard byte-framed transport.
2. **WebSocket upgrade over HTTP/1.1 or HTTP/2 is supported by the axum/
hyper stack natively.** `axum::extract::ws` provides the upgrade handler;
the underlying connection is the same hyper HTTP connection the `h2`/
`http/1.1` handler already drives. No new framing library is needed.
3. **A browser over WebSocket has the same peer-model properties as a browser
over WebTransport.** No `PeerId`, no `PeerCompositeEnv` entry, connection-
local Layer 2 overlay (ADR-043 §3, ADR-034 §2). The rationale in §5 is
transport-agnostic and applies identically to WSS.
4. **The downstream crates (SSH, git, SFTP) do not require WebTransport or
the ALPN-stream-proxy to exist.** They expose their ALPNs natively over
QUIC; the proxy is a browser-reachability feature, not a prerequisite for
the ALPNs themselves. Browser reachability for non-call ALPNs is the
speculative use case whose deferral this ADR commits.
## References
- [ADR-009](009-one-way-door-decision-framework.md) §"What this framework is
NOT" — the anti-pattern ADR-038 was written to correct; this ADR relies on
ADR-009's explicit distinction between deferral-as-hedging (rejected) and
deferral-as-scoping (permitted: a decision that "genuinely doesn't need to
be made yet because the use case isn't concrete" — scope management, not
door-type classification)
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — **superseded by
this ADR.** Its correction of the two-way-door-as-deferral anti-pattern
stands; its specific decision (h3 in scope now) is reversed.
- [ADR-040](040-webtransport-alpn-stream-proxy.md) — **parked, not
superseded.** Revives unchanged when WebTransport revives. The proxy is
the speculative use case whose deferral is this ADR's reversal trigger.
- [ADR-043](043-webtransport-bidirectional-alpn-substrate.md) — **parked, not
superseded.** §2 (bidirectionality) and §3 (no-`PeerId` overlay) transfer
to WebSocket unchanged; §4 (non-call-ALPN substrate) and §5's
WebTransport-specific framing revive with WebTransport.
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §4 — browsers are
not alknet peers; this ADR's §5 states the rationale (addressability vs.
bidirectionality) that ADR-034 §4 closes without arguing. ADR-034 §4 is
amended by reference to this ADR's §5.
- [ADR-012](012-call-protocol-stream-model.md) — stream-agnostic correlation;
a WebSocket message stream is another `BiStream`-satisfying transport. The
call protocol multiplexes by request ID, not by stream.
- [ADR-036](036-http-to-call-operation-mapping.md) — the HTTP-to-call
mapping; the WebSocket browser path layers on top of the same axum
`Router` and `OperationRegistry::invoke()` dispatch.
- `crates/http/webtransport.md` — the deferred spec; marked `deferred` with
a header note pointing here. Kept intact for revival.
- `crates/http/http-server.md` — gains a "WebSocket browser path" section
(the v1 browser bidirectional path) and the "browser is not a peer"
rationale (this ADR's §5, transported to the spec that now carries the
browser path).

View File

@@ -774,17 +774,19 @@ is a feature extension, not an unmade architecture decision.
2. **Standalone relay service (this OQ).** A full relay — a fork of 2. **Standalone relay service (this OQ).** A full relay — a fork of
`iroh-relay` — that provides NAT traversal infrastructure with `iroh-relay` — that provides NAT traversal infrastructure with
WebTransport-based proxy as a fallback alongside websocket. This WebTransport-based proxy as a fallback alongside WebSocket. This
is a separate service, not a mode of the `h3` handler: it is a separate service, not a mode of the `h3` handler: it
terminates the browser's WebTransport connection and forwards terminates the browser's WebTransport connection and forwards
encrypted traffic to a P2P hub's Ed25519 endpoint (so the hub need encrypted traffic to a P2P hub's Ed25519 endpoint (so the hub need
not expose its own public X.509 cert). ADR-034 §5 recorded it in not expose its own public X.509 cert). ADR-034 §5 recorded it in
the h3/WebTransport bucket; ADR-038 brought h3/WebTransport into the h3/WebTransport bucket; ADR-038 brought h3/WebTransport into
scope; ADR-040 resolved the in-process proxy. This OQ is the scope (later superseded by [ADR-044](decisions/044-defer-webtransport-browsers-use-websocket.md),
remaining scope question: does the standalone relay live in a which deferred h3/WebTransport as a scope decision — the browser
future `alknet-relay` crate (a fork of `iroh-relay` with bidirectional path uses WebSocket); ADR-040 resolved the in-process
WebTransport proxy fallback) or is it out of scope for the proxy (now parked per ADR-044). This OQ is the remaining scope
current alknet work? question: does the standalone relay live in a future `alknet-relay`
crate (a fork of `iroh-relay` with WebTransport proxy fallback) or
is it out of scope for the current alknet work?
This is a genuine scope question, not a deferral. The relay use case This is a genuine scope question, not a deferral. The relay use case
is not yet concrete enough to commit the crate boundary — no is not yet concrete enough to commit the crate boundary — no
@@ -796,8 +798,8 @@ is a feature extension, not an unmade architecture decision.
The relay does not change the auth model (bearer token + The relay does not change the auth model (bearer token +
`PeerEntry.auth_token_hash`; relay is transport-only), so it does not `PeerEntry.auth_token_hash`; relay is transport-only), so it does not
block any other ADR. block any other ADR.
- **Cross-references**: ADR-027, ADR-030, ADR-034, ADR-038, ADR-040, - **Cross-references**: ADR-027, ADR-030, ADR-034, ADR-038 (superseded),
[webtransport.md](crates/http/webtransport.md) ADR-040 (parked), ADR-044, [webtransport.md](crates/http/webtransport.md)
### OQ-39: `to_openapi` Published-Spec Versioning ### OQ-39: `to_openapi` Published-Spec Versioning