docs(http): add ADR-043 WebTransport bidirectional ALPN substrate; fix spec drift from mid-spec pivot

A consistency review of the alknet-http specs found two classes of
issues: internal contradictions from the mid-spec pivot (the to_openapi
gateway pattern landed in prose but not in cross-references), and a
systematic client→server assumption that only holds for the OpenAPI/MCP
case leaking into the WebTransport architecture.

Class 1 (internal contradictions):
- C1: to_openapi was half-refactored — body described the ADR-042
  gateway pattern but the decisions table and ADR-036 still said
  'paths mirror /{service}/{op}'. ADR-036's to_openapi clause is now
  amended as superseded by ADR-042; the stale decisions row and README
  Principle 2 are fixed.
- C2: the axum Router route list didn't include the 5 gateway endpoints
  (/search, /schema, /call, /batch, /subscribe). Added them; clarified
  /openapi.json as the gateway description doc; added gateway paths to
  the decoy exclusion list.
- C3: ADR-034 §5 still talked about the 'h3/WebTransport deferral
  bucket' that ADR-038 eliminated. Amended §5/Consequences/References
  to drop the deferral framing (the auth-model decision stands; only
  the 'when' wording was stale).

Class 2 (one-way direction assumption):
- C4/C5/C6: the WebTransport specs framed the session as browser→hub
  one-way, when the call protocol is bidirectional and WebTransport is
  a general ALPN transport substrate. New ADR-043 reframes WebTransport
  as a bidirectional ALPN transport substrate (call protocol is the
  first/canonical target; needs no WASM parser), names the call
  protocol's bidirectionality over WebTransport sessions, and states
  the inbound no-PeerId connection-local overlay as the mirror of
  ADR-034 §2. webtransport.md is updated to reflect this framing;
  ADR-040 is repositioned (not superseded) as the substrate's non-call-
  ALPN mechanism.
- C7: the HTTP/1.1+HTTP/2 surface's one-directionality is now named as
  a lossy consequence of HTTP request/response; WebTransport is named
  as the surface that restores the bidirectional call model.
- C8: overview.md acknowledges the from/to direction model is
  OpenAPI/MCP-specific, not a call-protocol property.

A review subagent pass on ADR-043 + webtransport.md found no critical
issues; warnings W1-W3 (residual browser-as-subject framing, ADR-009
rationale in spec, opening abstract tone) and suggestions S2/S4/S5
were addressed.
This commit is contained in:
2026-06-29 10:43:18 +00:00
parent 69ebe58bab
commit 0a78306686
10 changed files with 660 additions and 85 deletions

View File

@@ -30,10 +30,11 @@ pub struct HttpAdapter {
}
/// The stealth decoy surface for paths that are not registered
/// operations (and not `/healthz`, `/openapi.json`, or the MCP route).
/// Set by the assembly layer at `HttpAdapter` construction. The
/// existence of the decoy path is fixed by ADR-010; the variant is a
/// two-way-door config default.
/// operations (and not `/healthz`, `/openapi.json`, the `to_openapi`
/// gateway endpoints `/search`/`/schema`/`/call`/`/batch`/`/subscribe`,
/// or the MCP route). Set by the assembly layer at `HttpAdapter`
/// construction. The existence of the decoy path is fixed by ADR-010;
/// the variant is a two-way-door config default.
pub enum DecoyConfig {
/// Serve a fake `404 Not Found` (the default — matches the reference
/// implementation's "fake nginx 404").
@@ -99,10 +100,25 @@ identity provider through the router's state.
The axum `Router` is the single routing surface for HTTP requests. It
contains:
- The call-protocol projection routes (`POST /{service}/{op}`
`call.requested` dispatch — ADR-036).
- **The direct-call surface** (`POST /{service}/{op}` `call.requested`
dispatch — ADR-036). This is the HTTP projection of the call protocol's
`/{service}/{op}` operation path; an HTTP client that knows the
operation name calls it directly.
- **The `to_openapi` gateway endpoints** (`/search`, `/schema`, `/call`,
`/batch`, `/subscribe` — ADR-042). These are the fixed 5-endpoint
gateway that an OpenAPI consumer uses to discover and invoke
operations without knowing operation names up front. `/call` and
`/subscribe` dispatch through the same `OperationRegistry::invoke()`
as the direct-call surface; `/search` and `/schema` dispatch the
`services/list` / `services/schema` discovery ops. The gateway and
the direct-call surface coexist on the same router — they are two
projections of the same operation registry, not two registries.
- `GET /healthz` (raw route, no auth, no call protocol).
- `GET /openapi.json` (serves the `to_openapi` projection).
- `GET /openapi.json` (serves the `to_openapi` projection — the OpenAPI
document that *describes* the 5 gateway endpoints. Post-ADR-042 this
is the gateway's description doc, not a per-operation REST spec; the
doc describes the 5 fixed endpoints, and the per-caller operation
surface is discovered via `/search`, not preloaded into `paths`).
- The stealth decoy fallback (unknown paths).
- (Feature-gated) `POST /mcp` (the `to_mcp` streamable HTTP service —
[http-mcp.md](http-mcp.md)).
@@ -155,6 +171,32 @@ 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)).
### One-directional projection (HTTP request/response)
The HTTP/1.1 + HTTP/2 surface is a **lossy, one-directional projection**
of the call protocol. HTTP is request/response: the client initiates,
the server responds. The call protocol is bidirectional — both sides can
initiate calls (see
[../call/call-protocol.md](../call/call-protocol.md) §"Bidirectional
Calls": the server can call operations on the client just as the client
calls operations on the server). The HTTP projection carries only the
client→server call direction; the server→client call direction has no
HTTP expression (there is no HTTP mechanism for the server to initiate a
request to the client). `Subscription` streaming is the one partial
exception — the server streams `call.responded` frames back over the
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.
### Auth
Inbound HTTP auth is `Authorization: Bearer <token>`, resolved via
@@ -219,8 +261,9 @@ routes. `healthz` is the one exception. See ADR-036.
### Stealth decoy
For paths that are not registered operations (and not `/healthz`,
`/openapi.json`, or the MCP route), the HTTP handler serves a decoy. The
decoy is configurable (`DecoyConfig`):
`/openapi.json`, the `to_openapi` gateway endpoints `/search`/`/schema`/
`/call`/`/batch`/`/subscribe`, or the MCP route), the HTTP handler serves
a decoy. The decoy is configurable (`DecoyConfig`):
- A fake `404 Not Found` (the default — matches the reference
implementation's "fake nginx 404").
@@ -235,9 +278,13 @@ config is a two-way-door default (an operator picks what to serve); the
## Constraints
- **The HTTP path IS the operation path.** `POST /fs/readFile`
`call.requested` for `fs/readFile`. No second routing table. See
ADR-036.
- **The HTTP path IS the operation path on the direct-call surface.**
`POST /fs/readFile``call.requested` for `fs/readFile`. No second
routing table for the direct-call surface. See ADR-036. The
`to_openapi` gateway (`/search`, `/schema`, `/call`, `/batch`,
`/subscribe`) is a separate fixed-endpoint surface (ADR-042) that
coexists with the direct-call surface on the same axum `Router`; it
does not replace it.
- **`External` operations only.** `Internal` operations return `404`
on the HTTP handler.
- **Bearer-only auth.** `Authorization: Bearer`
@@ -256,7 +303,8 @@ config is a two-way-door default (an operator picks what to serve); the
| Decision | ADR | Summary |
|----------|-----|---------|
| Direct path mapping (HTTP path = operation path) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `POST /{service}/{op}``call.requested` |
| Direct path mapping (HTTP path = operation path) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `POST /{service}/{op}``call.requested` (direct-call surface) |
| `to_openapi` gateway endpoints on the router | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | `/search`/`/schema`/`/call`/`/batch`/`/subscribe` coexist with the direct-call surface |
| SSE projection for subscriptions over h2/http1.1 | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `call.responded` stream → SSE frames |
| `/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 |