diff --git a/docs/architecture/README.md b/docs/architecture/README.md index c25f9f8..313557f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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. -**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 (030–033) 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 036–038 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 (030–033) 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 @@ -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/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/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-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/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) | @@ -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 | | [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 | -| [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 | -| [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 | | [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 diff --git a/docs/architecture/crates/http/README.md b/docs/architecture/crates/http/README.md index 8057a78..9ce827e 100644 --- a/docs/architecture/crates/http/README.md +++ b/docs/architecture/crates/http/README.md @@ -1,23 +1,26 @@ --- status: draft -last_updated: 2026-06-29 +last_updated: 2026-06-30 --- # alknet-http -HTTP interface for alknet: serves HTTP/1.1, HTTP/2, and HTTP/3 (WebTransport) -on standard ALPNs, and hosts the HTTP-backed call-protocol adapters -(`from_openapi`, `to_openapi`, `from_mcp`, `to_mcp`). +HTTP interface for alknet: serves HTTP/1.1 and HTTP/2 on standard ALPNs +(with WebSocket upgrade for browser bidirectional access to the call +protocol), and hosts the HTTP-backed call-protocol adapters +(`from_openapi`, `to_openapi`, `from_mcp`, `to_mcp`). HTTP/3 + WebTransport +(`h3`) is deferred per +[ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md). ## Documents | Document | Status | Description | |----------|--------|-------------| | [overview.md](overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map | -| [http-server.md](http-server.md) | draft | `HttpAdapter` (`ProtocolHandler` for `h2`/`http/1.1`), axum over QUIC, Bearer auth, stealth, `/healthz` | +| [http-server.md](http-server.md) | draft | `HttpAdapter` (`ProtocolHandler` for `h2`/`http/1.1` + WS upgrade), axum over QUIC, Bearer auth, stealth, `/healthz`, WebSocket browser path | | [http-adapters.md](http-adapters.md) | draft | `from_openapi` (reqwest client) and `to_openapi` (OpenAPI projection); no-env-vars invariant point | | [http-mcp.md](http-mcp.md) | draft | `from_mcp` / `to_mcp` (feature-gated), streamable-HTTP-only, stdio exclusion | -| [webtransport.md](webtransport.md) | draft | `h3`/WebTransport handler — the browser streaming path | +| [webtransport.md](webtransport.md) | deferred | `h3`/WebTransport handler — **deferred per ADR-044**; spec kept intact for revival | ## Applicable ADRs @@ -34,23 +37,24 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters | [017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `OperationAdapter` trait; `to_*` are projections; published-spec contract | | [022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, Composition Authority | `from_openapi`/`from_mcp` produce leaf bundles | | [023](../../decisions/023-operation-error-schemas.md) | Operation Error Schemas | `from_openapi`/`to_openapi` error fidelity; `HTTP_` error codes | -| [027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign | Browsers require X.509; WebTransport requires X.509 | -| [034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Outgoing-Only X.509 and Three Peer Roles | Browsers are not alknet peers; WebTransport relay-as-proxy recorded | +| [027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign | Browsers require X.509; applies to WebTransport (deferred) and any browser-facing TLS | +| [034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Outgoing-Only X.509 and Three Peer Roles | Browsers are not alknet peers (§4 amended by ADR-044 §5 with the addressability rationale) | | [036](../../decisions/036-http-to-call-operation-mapping.md) | HTTP-to-Call Operation Mapping | Direct path mapping; `to_openapi` is projection, not router | | [037](../../decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Streamable HTTP only; stdio not built | -| [038](../../decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | `h3` in scope, not deferred | +| [038](../../decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | **Superseded by ADR-044** (anti-pattern correction stands; specific decision reversed) | | [039](../../decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | One crate for server + client host (shared HTTP deps, shared mapping) | -| [040](../../decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser | +| [040](../../decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | **Parked** per ADR-044; revives unchanged when WebTransport revives | | [041](../../decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation; Subscription excluded | | [042](../../decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered | -| [043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | WebTransport carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls; no-`PeerId` non-peer clients use a connection-local overlay | +| [043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | **Parked** per ADR-044; §2/§3 transfer to WebSocket for v1 | +| [044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) | Defer h3/WebTransport; Browsers Use WebSocket | `h3`/WebTransport deferred (scope); browser bidirectional path uses WebSocket; "browser is not a peer" rationale | ## Relevant Open Questions | OQ | Title | Status | Relevance | |----|-------|--------|-----------| | OQ-11 | Handler-level auth resolution observability | resolved | HTTP handler stores resolved identity on `Connection` via `set_identity` | -| OQ-12 | TLS identity provisioning | resolved | Browsers require X.509 (gates the entire `h3` feature) | +| OQ-12 | TLS identity provisioning | resolved | Browsers require X.509 (applies to WebTransport when it revives; WebSocket uses the same TLS as h2/http1.1) | | OQ-13 | Operation path format | resolved | `/{service}/{op}` is the HTTP path (ADR-036) | | OQ-17 | Call protocol client and adapter contract | resolved | `OperationAdapter` trait; `to_*` projections | | OQ-24 | Operation error schemas | resolved | `from_openapi`/`to_openapi` error fidelity | @@ -63,11 +67,11 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters ## Key Design Principles 1. **HTTP is both a server surface and a client transport for adapters.** - Inbound HTTP (`h2`/`http/1.1`/`h3`) is served by `axum` over a QUIC - stream; outbound HTTP (`from_openapi`/`from_mcp` forwarding) uses - `reqwest`. Both directions share the same HTTP dependencies, which is - why they live in one crate rather than being split. See - [overview.md](overview.md). + Inbound HTTP (`h2`/`http/1.1` + WebSocket upgrade) is served by `axum` + over a QUIC stream; outbound HTTP (`from_openapi`/`from_mcp` + forwarding) uses `reqwest`. Both directions share the same HTTP + dependencies, which is why they live in one crate rather than being + split. See [overview.md](overview.md). 2. **The HTTP surface is a projection of the call protocol.** An HTTP request at `POST /fs/readFile` becomes a `call.requested` for `/fs/readFile`. The HTTP path IS the operation path on the @@ -78,7 +82,7 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters (direct-call surface) and [ADR-042](../../decisions/042-openapi-gateway-pattern.md) (`to_openapi` gateway, superseding ADR-036's original `to_openapi` clause). -3. **Standard ALPNs, not alknet ALPNs.** `h2`, `http/1.1`, `h3` are +3. **Standard ALPNs, not alknet ALPNs.** `h2`, `http/1.1` are IANA-registered ALPN strings. Any HTTP client (browser, curl, axios) connects without knowing about alknet — the TLS handshake negotiates `h2` or `http/1.1` normally. This is the stealth mapping (ADR-010). @@ -91,36 +95,34 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters arbitrary executable = RCE. Streamable HTTP is network-isolated, auth-gatable, and runs under alknet's auth model. See [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md). -6. **HTTP/3 + WebTransport is a first-class transport, not a deferral.** - The browser streaming path uses QUIC streams directly. See - [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md). -7. **The `h3` handler is an ALPN-stream-proxy for browsers.** A browser - with a WASM parser can reach any non-call ALPN handler (SSH, git, - SFTP) via WebTransport — no install, no native client, no VPN. The - call protocol needs no proxy (it speaks EventEnvelope directly); - the ALPN-stream-proxy is the substrate's mechanism for the protocols - that need a client-side parser. SSH-over-WebTransport is - HTTPS-shaped at the network layer (anti-censorship). See - [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) - and [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md). -8. **`h3` requires X.509.** Browsers don't support RFC 7250 raw keys - (ADR-027). A node serving WebTransport must have an X.509 identity. - This is a browser limitation, not an alknet decision. -9. **WebTransport is a bidirectional ALPN transport substrate.** - WebTransport carries ALPN protocols as bidirectional streams; the - call protocol is the first/canonical target (JSON-RPC over QUIC - streams, needs no WASM parser, runs in Deno/Node/browsers/native - Rust). Both sides of a WebTransport call-protocol session can - initiate calls — the call protocol's bidirectionality applies - unchanged. The HTTP/1.1 + HTTP/2 surface is the one-directional - projection (HTTP is request/response); WebTransport restores the - bidirectional call model. See - [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md). +6. **WebSocket is the browser bidirectional path.** A browser upgrades + an HTTP/1.1 or HTTP/2 request to WebSocket and speaks the call + protocol over binary WS messages — full-duplex, both sides can + initiate calls (the call protocol's native bidirectionality, ADR-012). + HTTP/3 + WebTransport (`h3`) is deferred per + [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) + — a scope decision (the browser bidirectional path doesn't require + WebTransport's stream model; WebSocket suffices). The reversal + trigger is a concrete ALPN-stream-proxy use case (a browser running + a WASM SSH/SFTP/git client). +7. **Browsers are not alknet peers.** A browser over WebSocket (or, when + it revives, WebTransport) authenticates by bearer token, gets no + `PeerId`, and its registered ops land in a connection-local Layer 2 + overlay. "Peer" means an addressable node in the call-protocol peer + graph (stable `PeerId`, `PeerRef::Specific`-reachable, identity + stable across reconnects) — not "any endpoint that exchanges calls + during a live session." A browser is the second but not the first: no + stable cryptographic identity of its own, ephemeral, not addressable + from other nodes. See + [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) + §4 (amended by ADR-044 §5 with the addressability rationale). ## References - `docs/research/alknet-http/phase-0-findings.md` — Phase 0 research - (directionally close; DH-2's deferral framing is corrected by ADR-038) + (directionally close; DH-2's deferral framing was corrected by + ADR-038, then ADR-038 was superseded by ADR-044 which re-defers + `h3`/WebTransport as a genuine scope decision) - `docs/research/alknet-call-completion/gap-analysis.md` — adapter location map, no-env-vars invariant - `/workspace/@alkdev/operations/src/from_openapi.ts`, @@ -128,4 +130,6 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters - `/workspace/rust-sdk/` — MCP Rust SDK (rmcp v1.8.0); streamable HTTP transport examples - `/workspace/wtransport/` — pure-Rust WebTransport reference - implementation (the `h3` feature's candidate dependency) \ No newline at end of file + 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.) \ No newline at end of file diff --git a/docs/architecture/crates/http/http-server.md b/docs/architecture/crates/http/http-server.md index 11d0366..c11cd0a 100644 --- a/docs/architecture/crates/http/http-server.md +++ b/docs/architecture/crates/http/http-server.md @@ -1,15 +1,17 @@ --- status: draft -last_updated: 2026-06-29 +last_updated: 2026-06-30 --- # HTTP Server The `HttpAdapter` — the `ProtocolHandler` for `h2` and `http/1.1` (and -`h3`, covered in [webtransport.md](webtransport.md)). This document +WebSocket upgrade — see §"WebSocket browser path"). The `h3`/WebTransport +path is deferred per [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md); +the deferred spec is at [webtransport.md](webtransport.md). This document covers how axum is run over a QUIC bidirectional stream, Bearer auth -resolution, the HTTP-to-call dispatch, the `/healthz` raw route, and -stealth decoy. +resolution, the HTTP-to-call dispatch, the `/healthz` raw route, stealth +decoy, and the WebSocket browser path. ## What @@ -54,12 +56,15 @@ impl ProtocolHandler for HttpAdapter { } ``` -The `HttpAdapter` registers for multiple ALPNs (`http/1.1`, `h2`, `h3`). +The `HttpAdapter` registers for multiple ALPNs (`http/1.1`, `h2`). The endpoint's `HandlerRegistry` maps each ALPN byte string to the same adapter instance; `handle()` branches on `connection.remote_alpn()` to pick the HTTP framing. For `http/1.1` and `h2`, the framing is hyper's -HTTP/1.1 or HTTP/2 over a QUIC bidirectional stream; for `h3`, it's the -WebTransport/HTTP/3 path (see [webtransport.md](webtransport.md)). +HTTP/1.1 or HTTP/2 over a QUIC bidirectional stream. WebSocket upgrade +(§"WebSocket browser path") layers on top of the same hyper connection +driver — a WS upgrade is an HTTP/1.1 or HTTP/2 request that switches +protocols. The `h3` ALPN is deferred (ADR-044); the deferred handler +design is at [webtransport.md](webtransport.md). ## Why @@ -167,9 +172,12 @@ A `Subscription` operation served over `h2`/`http/1.1` projects its sends `call.aborted` for the in-flight subscription, which cascades to descendants per ADR-016. -This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebTransport -(`h3`), the subscription projects directly onto a WebTransport -bidirectional stream — no SSE framing (see [webtransport.md](webtransport.md)). +This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket +(§"WebSocket browser path" below), the subscription projects directly +onto the WS connection — `call.responded` events as binary WS messages, +no SSE framing. WebTransport (`h3`, deferred per ADR-044) would project +onto WebTransport bidirectional streams; see +[webtransport.md](webtransport.md). ### One-directional projection (HTTP request/response) @@ -188,14 +196,79 @@ SSE response — but even there, the *call* is client-initiated; only the *results* flow server→client. This is a structural property of HTTP, not a design choice in this -crate. WebTransport (`h3`) restores the bidirectional call model: a -WebTransport session is a long-lived connection over which either side -can open bidirectional streams and send `call.requested` events in -either direction — the call protocol's native bidirectionality applies -unchanged. See [webtransport.md](webtransport.md) and ADR-043. The -HTTP/1.1 + HTTP/2 surface is the projection for clients that only speak -HTTP; WebTransport is the surface for clients that can speak the call -protocol in both directions. +crate. **WebSocket restores the bidirectional call model for browsers** +(see §"WebSocket browser path" below): a WS connection is a long-lived +full-duplex channel over which either side can send `call.requested` +frames in either direction — the call protocol's native bidirectionality +applies unchanged (ADR-012 — stream-agnostic correlation; a WS message +stream is another `BiStream`-satisfying transport). WebTransport (`h3`) +would restore it via native multi-stream multiplexing, but WebTransport +is deferred per ADR-044 — WebSocket is the v1 browser bidirectional path. +The HTTP/1.1 + HTTP/2 surface is the projection for clients that only +speak HTTP; WebSocket is the surface for browser clients that speak the +call protocol in both directions. + +### WebSocket browser path (ADR-044) + +A browser connecting to a hub upgrades an HTTP/1.1 or HTTP/2 request to +WebSocket (RFC 6455). The resulting full-duplex WS connection carries +call-protocol `EventEnvelope` frames as binary WebSocket messages — one +envelope per message. The browser authenticates by bearer token on the +upgrade request (the HTTP `Authorization` header), resolved by the hub's +`IdentityProvider::resolve_from_token`, same as any HTTP request. The WS +connection is then a **bidirectional call-protocol session**: + +- The browser opens the WS connection to `/alknet/call` (or `/`). +- The handler hands the WS message stream to the call protocol's + `Dispatcher` — the same dispatch loop the `CallAdapter` uses for + `alknet/call` QUIC connections (ADR-012, stream-agnostic correlation). +- The browser writes `EventEnvelope` frames as binary WS messages; the + handler reads them and dispatches via `OperationRegistry::invoke()`. +- Responses (`call.responded`, `call.error`, `call.completed`, + `call.aborted`) are written back as binary WS messages. + +**Bidirectionality:** the WS call-protocol session inherits the call +protocol's native bidirectionality — both sides can initiate calls +(ADR-043 §2, transferred to WebSocket per ADR-044 §3). The browser calls +operations on the hub; the hub can call operations registered on the +browser's side, over the same session, using the same `PendingRequestMap` +and `EventEnvelope` framing as `alknet/call`. The browser case where the +client registers no operations of its own is the common case — the +server→client call direction is unused because the browser has nothing +to call. That is a use-case scoping, not an architectural limitation. + +**No SSE translation.** A `Subscription` operation served over WebSocket +projects its `call.responded` stream directly as binary WS messages — no +SSE `data:` framing. `call.completed` closes the stream; `call.aborted` +closes it with an error frame. This is the native streaming projection +for the WS path; SSE (ADR-036) is the projection for `h2`/`http/1.1` +clients that don't upgrade to WebSocket. + +**Browsers are not alknet peers.** A browser over WebSocket authenticates +by bearer token, gets no `PeerId`, does not enter `PeerCompositeEnv`, and +its registered ops (if any) land in a connection-local Layer 2 overlay — +the inbound mirror of ADR-034 §2. The rationale (addressability vs. +bidirectionality) is stated in ADR-044 §5 and amends ADR-034 §4 by +reference. In short: "peer" means an addressable node in the +call-protocol peer graph (stable `PeerId`, `PeerRef::Specific`-reachable, +identity stable across reconnects), not "any endpoint that exchanges +calls during a live session." A browser is the second thing but not the +first — it has no stable cryptographic identity of its own (it presents +a bearer token the hub issued; nothing to pin), it is ephemeral (close +the tab → connection dies → the connection-local overlay dies with it), +and it is not addressable from other nodes (another alknet node has no +way to reach "the browser currently connected to hub-A"; the hub holds +it as a live `CallConnection` handle, not a peer-graph entry). The +connection-local overlay is what gives the browser bidirectional-call +capability *without* peer-graph membership. + +**What WebSocket does not provide (deferred to WebTransport, ADR-044):** +the ALPN-stream-proxy (ADR-040) — a browser running a WASM parser for +SSH/SFTP/git to reach a non-call ALPN — requires WebTransport's +multi-stream model and is the speculative use case whose deferral is +ADR-044's reversal trigger. WebSocket carries the call protocol from a +browser; it does not carry the non-call-ALPN substrate. A browser cannot +reach SSH/SFTP/git ALPNs in the v1 release. See ADR-044. ### Auth @@ -294,10 +367,11 @@ config is a two-way-door default (an operator picks what to serve); the Capabilities are used for outbound calls (`from_openapi`), never serialized into HTTP response bodies. - **`/healthz` is raw.** No auth, no call protocol. The one raw route. -- **The `h3` ALPN is a first-class transport.** The `HttpAdapter` - registers for `h3` when the `h3` feature is enabled (ADR-038). The - `h3` handler is covered in [webtransport.md](webtransport.md); this - document covers the `h2`/`http/1.1` path. +- **WebSocket is the browser bidirectional path (ADR-044).** A browser + upgrades an HTTP request to WS and speaks the call protocol over binary + messages. `h3`/WebTransport is deferred (ADR-044); the ALPN-stream-proxy + (ADR-040) is not available in v1. The `h3` ALPN and its feature gate are + not implemented in the initial release. ## Design Decisions @@ -309,7 +383,8 @@ config is a two-way-door default (an operator picks what to serve); the | `/healthz` is a raw route | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | No auth, no call protocol | | Stealth decoy | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | HTTP handler on standard ALPNs serves decoy | | Bearer auth via `resolve_from_token` | [ADR-004](../../decisions/004-auth-as-shared-core.md) | HTTP handler credential source (settled) | -| `h3` is first-class (not deferred) | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | The `h3` ALPN handler lives in this crate | +| WebSocket is the browser bidirectional path | [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) | Browsers upgrade to WS; `EventEnvelope` over binary messages; `h3`/WebTransport deferred | +| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 (amended by ADR-044 §5) | Bearer token, no `PeerId`, connection-local overlay (addressability vs. bidirectionality) | | Error mapping (call codes → HTTP status) | [ADR-023](../../decisions/023-operation-error-schemas.md) | Protocol/operation codes distinct; `HTTP_` prefix for imported | ## Open Questions @@ -327,10 +402,13 @@ See [open-questions.md](../../open-questions.md) for full details. - [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) — the HTTP-to-call mapping this server implements -- [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) - — the `h3`/WebTransport companion to this server +- [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) + — WebSocket is the v1 browser bidirectional path; `h3`/WebTransport + deferred. States the "browser is not a peer" rationale (addressability + vs. bidirectionality) that ADR-034 §4 closes without arguing. - [overview.md](overview.md) — crate overview, adapter location map -- [webtransport.md](webtransport.md) — the `h3` ALPN handler +- [webtransport.md](webtransport.md) — the deferred `h3` ALPN handler + (kept intact for revival) - [http-adapters.md](http-adapters.md) — `from_openapi`/`to_openapi` - [../core/auth.md](../core/auth.md) — `IdentityProvider`, Bearer → `resolve_from_token` diff --git a/docs/architecture/crates/http/overview.md b/docs/architecture/crates/http/overview.md index 6369c25..641d354 100644 --- a/docs/architecture/crates/http/overview.md +++ b/docs/architecture/crates/http/overview.md @@ -1,13 +1,14 @@ --- status: draft -last_updated: 2026-06-29 +last_updated: 2026-06-30 --- # alknet-http — Overview -The HTTP interface crate: serves inbound HTTP on standard ALPNs and hosts -the HTTP-backed call-protocol adapters. This document covers the crate's -two roles, its dependency edges, and the adapter location map. Component +The HTTP interface crate: serves inbound HTTP on standard ALPNs (with +WebSocket upgrade for browser bidirectional access) and hosts the +HTTP-backed call-protocol adapters. This document covers the crate's two +roles, its dependency edges, and the adapter location map. Component details are in the sibling documents. ## What @@ -16,11 +17,14 @@ details are in the sibling documents. architecture. It serves two roles in one crate: 1. **HTTP server** — a `ProtocolHandler` (`HttpAdapter`) that accepts - HTTP/2, HTTP/1.1, and HTTP/3 (WebTransport) connections on the - standard IANA ALPNs (`h2`, `http/1.1`, `h3`). It serves REST APIs, the + HTTP/2 and HTTP/1.1 connections on the standard IANA ALPNs (`h2`, + `http/1.1`), plus WebSocket upgrade (for browser bidirectional + access to the call protocol). It serves REST APIs, the `to_openapi`/`to_mcp` projections of local call-protocol operations, the `/healthz` operational endpoint, and the decoy surface for - stealth mode. + stealth mode. HTTP/3 + WebTransport (`h3` ALPN) is deferred per + [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md); + the deferred handler design is at [webtransport.md](webtransport.md). 2. **HTTP client host** — the home of the HTTP-transport-backed call adapters: `from_openapi` (import external HTTP APIs as call operations, using `reqwest` for outbound calls) and `from_mcp` (import @@ -42,7 +46,9 @@ crates that need to expose an HTTP interface. A downstream consumer (the CLI binary, a hub deployment, a browser-facing service) wires `HttpAdapter` into the `HandlerRegistry` for the standard HTTP ALPNs and gets a full HTTP surface: REST projection of the call protocol, OpenAPI -discovery, MCP tool exposure, and WebTransport for browsers. +discovery, MCP tool exposure, and WebSocket for browser bidirectional +access to the call protocol. (WebTransport is deferred per ADR-044; the +deferred browser-streaming path is at [webtransport.md](webtransport.md).) The key architectural insight that shapes the crate: **HTTP is both a server surface and a client transport for adapters.** The server side @@ -64,13 +70,15 @@ Calls": both sides can initiate calls). The HTTP/1.1 + HTTP/2 surface inherits HTTP's request/response constraint and projects the call protocol one-directionally (client→server calls only — see [http-server.md](http-server.md) §"One-directional projection"). -WebTransport (`h3`) is the HTTP-family transport that restores the -call protocol's native bidirectionality — it is a transport substrate -for the call protocol (and, via the ALPN-stream-proxy, for any ALPN), -not a browser→hub one-way path. See [webtransport.md](webtransport.md) -and ADR-043. The "from/to" naming of the OpenAPI/MCP adapters should not -be read as a statement about the call protocol's directionality; it is -a statement about OpenAPI's and MCP's directionality. +**WebSocket is the HTTP-family transport that restores the call +protocol's native bidirectionality for browsers** (ADR-044) — a WS +connection is a long-lived full-duplex channel over which either side +can send `call.requested` frames in either direction. WebTransport +(`h3`, deferred) would restore it via native multi-stream multiplexing; +WebSocket restores it via framed messages over one connection. The +"from/to" naming of the OpenAPI/MCP adapters should not be read as a +statement about the call protocol's directionality; it is a statement +about OpenAPI's and MCP's directionality. ## Dependencies @@ -79,13 +87,22 @@ alknet-http ├── alknet-core (ProtocolHandler, Connection, AuthContext, IdentityProvider, Capabilities) ├── alknet-call (OperationAdapter, OperationSpec, Handler, HandlerRegistration, │ OperationRegistry, AdapterError, OperationProvenance) -├── axum (HTTP server — Router, extractors, middleware) +├── axum (HTTP server — Router, extractors, middleware, WebSocket upgrade) ├── reqwest (HTTP client — from_openapi/from_mcp forwarding) ├── hyper (HTTP/1.1 + HTTP/2 framing; axum is built on hyper) -├── wtransport (HTTP/3 + WebTransport — feature-gated behind `h3`) └── rmcp (MCP streamable HTTP — feature-gated behind `mcp`) ``` +> **Note:** the `h3`/WebTransport dependency (`wtransport` or the +> hyperium `h3` stack) is **not** in the v1 dependency tree — +> `h3`/WebTransport is deferred per +> [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md). +> The `h3` feature gate and its dependency are absent from the initial +> release; the browser bidirectional path uses WebSocket (native axum +> support, no new dependency). The deferred dependency analysis is +> recorded in ADR-044 §"Research note (for revival)" and +> [webtransport.md](webtransport.md) §"Research note". + ### The `alknet-call` dependency (ADR-003 Amendment 1) `alknet-http` depends on `alknet-call`. ADR-003's rule is "no handler @@ -108,21 +125,22 @@ know `reqwest` is involved. | ALPN | Handler | Transport | Browser? | |------|---------|-----------|----------| -| `http/1.1` | `HttpAdapter` | HTTP/1.1 over QUIC stream | No | -| `h2` | `HttpAdapter` | HTTP/2 over QUIC stream | No | -| `h3` | `HttpAdapter` | HTTP/3 / WebTransport | Yes (X.509 required) | +| `http/1.1` | `HttpAdapter` | HTTP/1.1 over QUIC stream (+ WS upgrade) | Yes (WS upgrade for bidirectional) | +| `h2` | `HttpAdapter` | HTTP/2 over QUIC stream (+ WS upgrade) | Yes (WS upgrade for bidirectional) | +| `h3` | — (deferred) | HTTP/3 / WebTransport | Deferred per ADR-044 | These are standard IANA ALPN strings, not `alknet/`-prefixed. Any HTTP client connects without knowing about alknet — the TLS handshake negotiates `h2` or `http/1.1` normally, and the `HttpAdapter` serves HTTP. This is the stealth mapping (ADR-010): clients that don't offer alknet ALPNs get the HTTP handler, just like port scanners in stealth -mode. +mode. A browser negotiates `h2` or `http/1.1` and upgrades to WebSocket +for the bidirectional call-protocol path (ADR-044). -The `HttpAdapter` registers for all three ALPNs (when the corresponding -features are enabled). The endpoint's `HandlerRegistry` maps each ALPN to -the same `HttpAdapter` instance; the handler branches on -`connection.remote_alpn()` to pick the right framing. +The `HttpAdapter` registers for `http/1.1` and `h2`. The endpoint's +`HandlerRegistry` maps each ALPN to the same `HttpAdapter` instance; +the handler branches on `connection.remote_alpn()` to pick the right +framing. The `h3` ALPN is not registered in v1 (deferred per ADR-044). ## Adapter Location Map @@ -139,7 +157,7 @@ alknet-call (lean — no HTTP client, no HTTP server) └── CallClient (outbound connection opener) alknet-http (owns HTTP server + HTTP client) -├── HttpAdapter (axum server — inbound HTTP on h2/http1.1/h3) +├── HttpAdapter (axum server — inbound HTTP on h2/http1.1 + WS upgrade) ├── from_openapi (parse OpenAPI doc + reqwest forwarding handler) ├── to_openapi (generate OpenAPI doc from local registry) ├── from_mcp (feature-gated) (import remote MCP tools over streamable HTTP — reqwest) @@ -155,24 +173,27 @@ directions. ```toml [features] -default = ["h2", "http1"] # the non-browser HTTP surface -h3 = ["dep:wtransport"] # HTTP/3 + WebTransport (browser path; X.509 required) +default = ["h2", "http1"] # the HTTP surface (incl. WebSocket upgrade for browsers) mcp = ["dep:rmcp"] # from_mcp / to_mcp (streamable HTTP only — ADR-037) +# h3 (HTTP/3 + WebTransport) is deferred per ADR-044 — not in the v1 +# feature set. The browser bidirectional path uses WebSocket (native to +# axum, no feature gate). When WebTransport revives, the `h3` feature +# gate returns; see ADR-044 and webtransport.md. ``` - `h2` + `http1` (default): the `axum` + `hyper` HTTP/1.1 + HTTP/2 - server. This is the surface non-browser clients use. -- `h3`: the `wtransport` (or quinn HTTP/3 extension) dependency. Adds - the `h3` ALPN handler and the WebTransport streaming path. See - [webtransport.md](webtransport.md) and - [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md). + server, including WebSocket upgrade for browser bidirectional access + (ADR-044). This is the surface all clients — including browsers, via + WS upgrade — use in v1. - `mcp`: the `rmcp` dependency with streamable HTTP transport features only. Adds `from_mcp`/`to_mcp`. See [http-mcp.md](http-mcp.md) and [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md). -A deployment that only needs the REST surface (no browsers, no MCP) uses -the default features. A browser-facing hub enables `h3`. A deployment -that wants MCP tool import/export enables `mcp`. +A deployment that only needs the REST surface (no MCP) uses the default +features. A browser-facing hub also uses the default features — the +browser bidirectional path is WebSocket, native to axum, no `h3` feature +gate needed. A deployment that wants MCP tool import/export enables +`mcp`. ## The No-Env-Vars Invariant @@ -202,18 +223,19 @@ verified against this invariant. See ADR-014 and ## Architecture (component pointers) - **[http-server.md](http-server.md)** — the `HttpAdapter` for `h2`/ - `http/1.1`: how axum is run over a QUIC bidirectional stream, Bearer - auth resolution, the `/healthz` raw route, stealth decoy, and the - HTTP-to-call dispatch (ADR-036). + `http/1.1` (+ WebSocket upgrade): how axum is run over a QUIC + bidirectional stream, Bearer auth resolution, the `/healthz` raw route, + stealth decoy, the HTTP-to-call dispatch (ADR-036), and the WebSocket + browser bidirectional path (ADR-044). - **[http-adapters.md](http-adapters.md)** — `from_openapi` (parse OpenAPI, build forwarding handlers with `reqwest`) and `to_openapi` (generate an OpenAPI doc from the registry's `External` operations). Error fidelity per ADR-023. - **[http-mcp.md](http-mcp.md)** — `from_mcp`/`to_mcp` (feature-gated), streamable HTTP only (ADR-037), the rmcp integration. -- **[webtransport.md](webtransport.md)** — the `h3` ALPN handler, - WebTransport session/stream handling, the browser streaming path - (ADR-038). +- **[webtransport.md](webtransport.md)** — the deferred `h3` ALPN + handler (HTTP/3 + WebTransport). **Deferred per ADR-044**; kept intact + for revival when a concrete ALPN-stream-proxy use case arrives. ## Design Decisions @@ -221,12 +243,13 @@ verified against this invariant. See ADR-014 and |----------|-----|---------| | HTTP-to-call operation mapping | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | Direct path mapping; `to_openapi` is projection, not router | | MCP stdio transport exclusion | [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built (RCE vector) | -| HTTP/3 + WebTransport first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | `h3` in scope, not deferred; browser streaming uses QUIC streams | +| Defer h3/WebTransport; browsers use WebSocket | [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md) | `h3`/WebTransport deferred (scope, not hedging); browser bidirectional path uses WebSocket; ADR-038 superseded, ADR-040/043 parked | | HTTP server + client host colocated | [ADR-039](../../decisions/039-http-server-and-client-host-colocated.md) | One crate for server + adapters (shared HTTP deps, shared mapping) | -| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser | +| ~~HTTP/3 + WebTransport first-class~~ | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | **Superseded by ADR-044** (anti-pattern correction stands; specific decision reversed) | +| ~~WebTransport ALPN-stream-proxy~~ | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | **Parked** per ADR-044; revives unchanged when WebTransport revives | | `to_mcp` tool-gateway pattern | [ADR-041](../../decisions/041-mcp-tool-gateway-pattern.md) | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation | | `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe); per-caller AccessControl-filtered | -| WebTransport bidirectional ALPN substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport carries ALPNs as bidirectional streams; call protocol is the first target; both sides can initiate calls; non-peer clients use a connection-local overlay | +| ~~WebTransport bidirectional ALPN substrate~~ | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | **Parked** per ADR-044; §2/§3 transfer to WebSocket for v1; §4/§5 revive with WebTransport | | `alknet-call` is protocol-foundation | [ADR-003](../../decisions/003-crate-decomposition.md) Am. 1 | `alknet-http` depends on `alknet-call` (types, not peer handler) | | Bearer auth via `resolve_from_token` | [ADR-004](../../decisions/004-auth-as-shared-core.md) | HTTP handler credential source + resolution (settled) | | Stealth mode = HTTP handler on standard ALPNs | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Decoy for unknown paths (settled) | @@ -234,8 +257,8 @@ verified against this invariant. See ADR-014 and | `OperationAdapter` trait is async | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | HTTP adapters implement the async trait (settled) | | `to_*` adapters are projections | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `to_openapi`/`to_mcp` consume the registry, don't produce entries (settled) | | Error schema fidelity | [ADR-023](../../decisions/023-operation-error-schemas.md) | `from_openapi` maps HTTP status → `HTTP_` codes; `to_openapi` projects back (settled) | -| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3`/WebTransport needs X.509 (settled) | -| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Browser over WebTransport/HTTPS = bearer token, no `PeerId` (settled) | +| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3`/WebTransport needs X.509 (settled; applies when WebTransport revives) | +| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 (amended by ADR-044 §5) | Browser over WS/WebTransport = bearer token, no `PeerId` (settled; rationale in ADR-044 §5) | ## Open Questions @@ -262,4 +285,6 @@ See [open-questions.md](../../open-questions.md) for full details. - `/workspace/@alkdev/operations/src/from_openapi.ts`, `/workspace/@alkdev/operations/src/from_mcp.ts` — TypeScript prior art - `/workspace/rust-sdk/` — MCP Rust SDK (rmcp); streamable HTTP examples -- `/workspace/wtransport/` — pure-Rust WebTransport reference \ No newline at end of file +- `/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) \ No newline at end of file diff --git a/docs/architecture/crates/http/webtransport.md b/docs/architecture/crates/http/webtransport.md index 6183820..d4f6b01 100644 --- a/docs/architecture/crates/http/webtransport.md +++ b/docs/architecture/crates/http/webtransport.md @@ -1,10 +1,43 @@ --- -status: draft -last_updated: 2026-06-29 +status: deferred +last_updated: 2026-06-30 --- # WebTransport — the h3 ALPN handler +> **DEFERRED per [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md).** +> This spec is kept intact for revival. `h3`/WebTransport is not +> implemented in the initial `alknet-http` release; the browser +> bidirectional path uses WebSocket (see +> [http-server.md](http-server.md) §"WebSocket browser path"). ADR-038 +> is superseded; ADR-040 and ADR-043 are parked (their decisions revive +> unchanged when WebTransport revives). The reversal trigger is a +> concrete deployment needing the ALPN-stream-proxy (a browser running +> a WASM SSH/SFTP/git client to reach a non-call ALPN). Two transfers +> apply during the deferment: ADR-043 §2 (call-protocol bidirectionality) +> and §3 (the no-`PeerId` connection-local overlay) apply over WebSocket +> unchanged; ADR-040 (the ALPN-stream-proxy) and ADR-043 §4 (the +> non-call-ALPN substrate) do not — they require WebTransport's stream +> model and revive with it. +> +> **Research note (for revival):** `wtransport` (v0.7.1, the reference +> implementation read during initial research) is *probably not* the +> right dependency choice at revival time, despite being a complete and +> readable implementation. The load-bearing integration concern is that +> the `h3` handler must route HTTP/3 requests through the same axum +> `Router` as `h2`/`http/1.1` (ADR-036), and `wtransport` owns its own +> HTTP serving path — bridging its request type into the `http::Request` +> axum consumes is cross-ecosystem adapter work. The hyperium stack +> (`h3` + `h3-quinn` + `h3-webtransport` + `h3-datagram`) operates at +> the stream level and produces `http::Request` types natively, which is +> a better fit for the axum integration — but its server-side +> WebTransport API needs verification before commitment (the axum-bridge +> feasibility is the load-bearing claim and is not yet confirmed against +> actual crate APIs, only against READMEs and design philosophy). This +> research is **not** run now (WebTransport is deferred); it is recorded +> here so the revival does not re-derive the question from scratch. See +> ADR-044 §"Research note (for revival)" for the cross-reference. + The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and WebTransport. WebTransport is a **bidirectional ALPN transport substrate** (ADR-043) — it carries ALPN protocols as bidirectional @@ -386,13 +419,19 @@ as a first-class transport. ## Design Decisions +> **Note:** This table reflects the design as written for revival. ADR-038 +> is **superseded by [ADR-044](../../decisions/044-defer-webtransport-browsers-use-websocket.md)**; +> ADR-040 and ADR-043 are **parked** (implementation deferred per ADR-044). +> The decisions revive unchanged when WebTransport revives — see the +> header note and ADR-044 for the scope rationale and reversal trigger. + | Decision | ADR | Summary | |----------|-----|---------| -| `h3`/WebTransport is first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | In scope, not deferred; browser streaming uses QUIC streams | -| WebTransport is a bidirectional ALPN transport substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | Carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls | -| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser | -| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation) | -| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Bearer token, no `PeerId` | +| ~~`h3`/WebTransport is first-class~~ | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | **Superseded by ADR-044** (scope deferral); originally "in scope, not deferred; browser streaming uses QUIC streams" | +| WebTransport is a bidirectional ALPN transport substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | **Parked** per ADR-044. Carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls | +| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | **Parked** per ADR-044. The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser | +| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation; applies when WebTransport revives) | +| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 (amended by ADR-044 §5) | Bearer token, no `PeerId` (rationale in ADR-044 §5) | | WebTransport streams → call protocol directly | [ADR-012](../../decisions/012-call-protocol-stream-model.md) | Stream-agnostic; WebTransport stream = QUIC bidirectional stream | | `BiStream` is a trait (WASM door) | [ADR-007](../../decisions/007-bistream-type-definition.md) | Browser implements `BiStream` over WebTransport stream; WASM parser speaks the protocol | | Stealth on h3 | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Unknown paths get the decoy | diff --git a/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md b/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md index 99b12be..ad34c45 100644 --- a/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md +++ b/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md @@ -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 token** (HTTP `Authorization`), resolved by the hub's `IdentityProvider::resolve_from_token` against the hub's -`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 +`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 not enter `PeerCompositeEnv`, and its "ops" are HTTP routes / WebTransport streams served by `alknet-http`, not entries in the call-protocol 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 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 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 > as deferral" anti-pattern (ADR-009 §"What this framework is NOT") > that [ADR-038](038-http3-and-webtransport-as-first-class.md) was later -> written to reject — `h3`/WebTransport is a first-class transport, in -> scope, not deferred. The *auth-model* decision in this §5 (the proxy -> is transport-only; it does not change identity resolution) is -> unchanged. 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. +> written to reject. ADR-038 has since been **superseded by +> [ADR-044](044-defer-webtransport-browsers-use-websocket.md)**, which +> re-defers `h3`/WebTransport as a genuine scope decision (the browser +> bidirectional path uses WebSocket; WebTransport revives when a concrete +> ALPN-stream-proxy use case arrives). The *auth-model* decision in this +> §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 @@ -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. 2. **Browsers never enter the peer-keyed overlay.** A browser is - served by `alknet-http` (HTTP routes / WebTransport streams) and - authenticates by bearer token. The hub may have a `PeerEntry` for - the browser's token (to authorize it), but the browser is not a - `PeerId`-bearing peer. This is the explicit closure of the - "browser as peer" path — browsers are clients, not peers. + served by `alknet-http` (HTTP routes / WebTransport streams /, per + ADR-044, WebSocket) and authenticates by bearer token. The hub may + have a `PeerEntry` for the browser's token (to authorize it), but the + browser is not a `PeerId`-bearing peer. This is the explicit closure + 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 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 crate) - `docs/research/alknet-http/phase-0-findings.md` — DH-2 (h3 / - WebTransport; the original "deferred past v1" framing is rejected by - ADR-038); the WebTransport-relay-as-proxy feature noted in this ADR's - §5 is a transport-only feature whose scope is tracked as OQ-38 -- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` / - WebTransport is a first-class transport, not deferred (amends the - "deferral bucket" wording in this ADR's §5; the auth-model decision - stands) + WebTransport; the original "deferred past v1" framing was rejected by + ADR-038, which is now itself superseded by + [ADR-044](044-defer-webtransport-browsers-use-websocket.md) — a genuine + scope deferral); the WebTransport-relay-as-proxy feature noted in this + ADR's §5 is a transport-only feature whose scope is tracked as OQ-38 +- [ADR-038](038-http3-and-webtransport-as-first-class.md) — **superseded + 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 transport relay (`iroh-relay`), referenced to distinguish it from alknet's hub role diff --git a/docs/architecture/decisions/036-http-to-call-operation-mapping.md b/docs/architecture/decisions/036-http-to-call-operation-mapping.md index d1f3b9b..23a827a 100644 --- a/docs/architecture/decisions/036-http-to-call-operation-mapping.md +++ b/docs/architecture/decisions/036-http-to-call-operation-mapping.md @@ -7,7 +7,8 @@ Proposed ## Context `alknet-http` implements `ProtocolHandler` for the standard HTTP ALPNs (`h2`, -`http/1.1`, `h3`). An inbound HTTP request that targets an alknet operation +`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 *projection* of the call protocol, not a parallel routing layer. The 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 becomes an SSE `data:` frame; `call.completed` closes the SSE stream; `call.aborted` closes the stream with an SSE error event. This is the -HTTP/1.1 + HTTP/2 streaming projection. Over WebTransport (`h3`), the -subscription projects directly onto a WebTransport bidirectional stream — -no SSE framing is needed (see ADR-038 for the WebTransport path). +HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket (the v1 browser +bidirectional path, ADR-044), the subscription projects directly onto the +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 diff --git a/docs/architecture/decisions/038-http3-and-webtransport-as-first-class.md b/docs/architecture/decisions/038-http3-and-webtransport-as-first-class.md index 9430b3e..676e288 100644 --- a/docs/architecture/decisions/038-http3-and-webtransport-as-first-class.md +++ b/docs/architecture/decisions/038-http3-and-webtransport-as-first-class.md @@ -2,7 +2,24 @@ ## 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 diff --git a/docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md b/docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md index c4e2da8..ed57a9e 100644 --- a/docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md +++ b/docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md @@ -2,7 +2,19 @@ ## 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 diff --git a/docs/architecture/decisions/042-openapi-gateway-pattern.md b/docs/architecture/decisions/042-openapi-gateway-pattern.md index cc6fc3a..6be73cb 100644 --- a/docs/architecture/decisions/042-openapi-gateway-pattern.md +++ b/docs/architecture/decisions/042-openapi-gateway-pattern.md @@ -214,11 +214,14 @@ require it for the common case. `/schema` at build time. 4. **`subscribe` (SSE) is the streaming projection for the gateway.** - Over `h2`/`http/1.1`, subscriptions are SSE. Over WebTransport - (`h3`), subscriptions project onto WebTransport streams directly - (ADR-038) — the gateway's `/subscribe` is the `h2`/`http/1.1` path; - the WebTransport path is the native call-protocol session - (`webtransport.md`). + Over `h2`/`http/1.1`, subscriptions are SSE. Over WebSocket (the v1 + browser bidirectional path, ADR-044), subscriptions project onto the + WS connection directly as binary messages — the gateway's `/subscribe` + is the `h2`/`http/1.1` SSE path; the WebSocket path is the native + 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 @@ -232,9 +235,11 @@ require it for the common case. - [ADR-036](036-http-to-call-operation-mapping.md) — the SSE projection for subscriptions over `h2`/`http/1.1` (the gateway's `/subscribe` endpoint uses the same SSE framing) -- [ADR-038](038-http3-and-webtransport-as-first-class.md) — the - WebTransport streaming path (the gateway's `/subscribe` is the - `h2`/`http/1.1` path; WebTransport is native) +- [ADR-044](044-defer-webtransport-browsers-use-websocket.md) — + WebSocket is the v1 browser bidirectional path; `h3`/WebTransport + 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 pattern for `to_mcp` (4 tools; `subscribe` excluded because MCP tool calls are request/response) diff --git a/docs/architecture/decisions/043-webtransport-bidirectional-alpn-substrate.md b/docs/architecture/decisions/043-webtransport-bidirectional-alpn-substrate.md index 20ced68..8f2894b 100644 --- a/docs/architecture/decisions/043-webtransport-bidirectional-alpn-substrate.md +++ b/docs/architecture/decisions/043-webtransport-bidirectional-alpn-substrate.md @@ -2,7 +2,28 @@ ## 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 diff --git a/docs/architecture/decisions/044-defer-webtransport-browsers-use-websocket.md b/docs/architecture/decisions/044-defer-webtransport-browsers-use-websocket.md new file mode 100644 index 0000000..c4a6f1c --- /dev/null +++ b/docs/architecture/decisions/044-defer-webtransport-browsers-use-websocket.md @@ -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). \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 05f26ae..a00e4a2 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -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 `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 terminates the browser's WebTransport connection and forwards 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 the h3/WebTransport bucket; ADR-038 brought h3/WebTransport into - scope; ADR-040 resolved the in-process proxy. This OQ is the - remaining scope 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? + scope (later superseded by [ADR-044](decisions/044-defer-webtransport-browsers-use-websocket.md), + which deferred h3/WebTransport as a scope decision — the browser + bidirectional path uses WebSocket); ADR-040 resolved the in-process + proxy (now parked per ADR-044). This OQ is the remaining scope + 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 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 + `PeerEntry.auth_token_hash`; relay is transport-only), so it does not block any other ADR. -- **Cross-references**: ADR-027, ADR-030, ADR-034, ADR-038, ADR-040, - [webtransport.md](crates/http/webtransport.md) +- **Cross-references**: ADR-027, ADR-030, ADR-034, ADR-038 (superseded), + ADR-040 (parked), ADR-044, [webtransport.md](crates/http/webtransport.md) ### OQ-39: `to_openapi` Published-Spec Versioning