docs(http): draft alknet-http architecture specs and ADRs 036-039

First speccing pass for alknet-http (HTTP interface crate: h2/http1.1/h3
server + from_openapi/to_openapi/from_mcp/to_mcp adapters).

Specs (crates/http/):
- README.md, overview.md — crate index, two-roles-in-one-crate framing,
  adapter location map, feature gates (h3, mcp), no-env-vars invariant
- http-server.md — HttpAdapter for h2/http1.1, axum over QUIC stream,
  Bearer auth, SSE projection for subscriptions, /healthz, stealth decoy
- http-adapters.md — from_openapi (reqwest) and to_openapi (projection),
  error fidelity (HTTP_<status> per ADR-023), type definitions
- http-mcp.md — from_mcp/to_mcp (feature-gated), streamable-HTTP-only
- webtransport.md — h3/WebTransport handler, browser streaming path,
  HTTP/3 request vs WebTransport session distinguished at framing layer

ADRs:
- ADR-036 HTTP-to-Call Operation Mapping (Proposed) — direct path
  mapping; to_openapi is projection, not router (the load-bearing one-way
  door from Phase 0 DH-3)
- ADR-037 MCP Stdio Transport Exclusion (Proposed) — streamable HTTP
  only; stdio is not built (RCE-vector security position)
- ADR-038 HTTP/3 and WebTransport as First-Class HTTP Transports
  (Proposed) — corrects the Phase 0 DH-2 deferral framing; h3 is in
  scope, not deferred, per ADR-009 §'What this framework is NOT'
- ADR-039 HTTP Server and Client Host Colocated in alknet-http
  (Proposed) — one crate for server + client host (shared HTTP deps,
  shared operation-spec->HTTP mapping)
- ADR-003 Amendment 1 — clarifies alknet-call is a protocol-foundation
  crate (the alknet-http -> alknet-call dependency edge)

Open questions (OQ-38, OQ-39, OQ-40 added under 'Theme: alknet-http'):
- OQ-38 WebTransport relay-as-proxy scope (genuine scope question, not
  a deferral — the decision is made when the use case becomes concrete)
- OQ-39 to_openapi published-spec versioning (one-way after first
  publication)
- OQ-40 reqwest client config and connection pooling (two-way-door)

Architecture README and overview updated with doc table, ADR table
(036-039), current-state note, and crate graph (alknet-http ->
alknet-call edge).

Reviewed by architecture-reviewer subagent: 3 critical, 4 warning, 5
suggestion issues found and fixed (missing ADR-039, WebTransport stream
routing conflation, undefined types, stale OQ-37 deferral language,
README OQ table completeness, Bearer-only attribution, cross-references,
ADR-038 ALPN quote, feature-gate placeholder, MCP temporal language).
This commit is contained in:
2026-06-29 05:53:38 +00:00
parent dd5ccf4983
commit ab47dac4ad
14 changed files with 2343 additions and 12 deletions

View File

@@ -0,0 +1,103 @@
---
status: draft
last_updated: 2026-06-29
---
# 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`).
## 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-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 |
## Applicable ADRs
| ADR | Title | Relevance |
|-----|-------|-----------|
| [001](../../decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | `HttpAdapter` registers on standard HTTP ALPNs |
| [002](../../decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | `HttpAdapter` implements `ProtocolHandler` |
| [003](../../decisions/003-crate-decomposition.md) | Crate Decomposition | `alknet-http` depends on `alknet-core` + `alknet-call` (protocol-foundation exception, Amendment 1) |
| [004](../../decisions/004-auth-as-shared-core.md) | Auth as Shared Core | Bearer → `resolve_from_token` |
| [007](../../decisions/007-bistream-type-definition.md) | BiStream Type Definition | `HttpAdapter` receives `Connection`, accepts a stream for hyper |
| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Stealth mode = HTTP handler on standard ALPNs |
| [014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow | `from_openapi`/`from_mcp` are the credential injection point |
| [015](../../decisions/015-privilege-model-and-authority-context.md) | Privilege Model | Adapter-registered ops are `Internal` by default |
| [017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `OperationAdapter` trait; `to_*` are projections; published-spec contract |
| [022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, Composition Authority | `from_openapi`/`from_mcp` produce leaf bundles |
| [023](../../decisions/023-operation-error-schemas.md) | Operation Error Schemas | `from_openapi`/`to_openapi` error fidelity; `HTTP_<status>` error codes |
| [027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign | Browsers require X.509; WebTransport requires X.509 |
| [034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Outgoing-Only X.509 and Three Peer Roles | Browsers are not alknet peers; WebTransport relay-as-proxy recorded |
| [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 |
| [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) |
## 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-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 |
| OQ-26 | OperationAdapter error type | resolved | `AdapterError` variants reused by HTTP adapters |
| OQ-37 | X.509 outgoing-only / three peer roles | resolved | Browsers are not peers; hub with mixed fingerprints |
| OQ-38 | WebTransport relay-as-proxy scope | open (scope, not deferral) | Does the proxy live in `alknet-http` or a separate relay crate? |
| OQ-39 | `to_openapi` published-spec versioning | open | Versioning strategy for generated OpenAPI specs |
| OQ-40 | reqwest client config and connection pooling | open | Two-way-door: pooling/retry config shape |
## 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).
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; `to_openapi`
describes this surface, it does not define a second one. See
[ADR-036](../../decisions/036-http-to-call-operation-mapping.md).
3. **Standard ALPNs, not alknet ALPNs.** `h2`, `http/1.1`, `h3` 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).
4. **`from_openapi`/`from_mcp` are the no-env-vars injection point.** The
forwarding handlers read `context.capabilities`, not `std::env::var`.
This is the architectural mechanism that makes aisdk's env-var reads
unreachable. See ADR-014,
[client-and-adapters.md](../call/client-and-adapters.md).
5. **MCP streamable HTTP only; stdio is not built.** stdio = spawn
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. **`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.
## References
- `docs/research/alknet-http/phase-0-findings.md` — Phase 0 research
(directionally close; DH-2's deferral framing is corrected by ADR-038)
- `docs/research/alknet-call-completion/gap-analysis.md` — adapter
location map, no-env-vars invariant
- `/workspace/@alkdev/operations/src/from_openapi.ts`,
`/workspace/@alkdev/operations/src/from_mcp.ts` — TypeScript prior art
- `/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)

View File

@@ -0,0 +1,326 @@
---
status: draft
last_updated: 2026-06-29
---
# HTTP Adapters — from_openapi and to_openapi
The OpenAPI-direction adapters: `from_openapi` imports external HTTP APIs
as call-protocol operations (reqwest-backed forwarding handlers), and
`to_openapi` generates an OpenAPI spec from the local registry's
`External` operations. This document covers both, the error fidelity
(ADR-023), and the no-env-vars credential injection point.
## What
Two adapters, both in `alknet-http`:
1. **`from_openapi`** — parses an OpenAPI document, constructs a
`HandlerRegistration` bundle per OpenAPI operation with a forwarding
handler that calls the external HTTP endpoint via `reqwest`, and
returns the bundles for registration in the `OperationRegistry`. The
adapter implements `OperationAdapter` (the async trait from
`alknet-call`, ADR-017 §5). Provenance is `FromOpenAPI` (leaf,
`composition_authority: None`, `scoped_env: None`, `Internal` by
default — ADR-015/022).
2. **`to_openapi`** — generates an OpenAPI document from the local
registry's `External` operations. A pure projection: it consumes the
registry, it does not produce entries for it (ADR-017 §5 — the `to_*`
adapters are outbound projections, not `OperationAdapter`
implementations). Served at `GET /openapi.json` by the HTTP server.
### from_openapi
```rust
pub struct FromOpenAPI {
spec: OpenAPISpec,
config: HttpServiceConfig,
}
#[async_trait]
impl OperationAdapter for FromOpenAPI {
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
}
```
#### Type definitions
```rust
/// A parsed OpenAPI document. The concrete type is a two-way-door
/// implementation detail (openapiv3::OpenApi, a local alknet-http
/// type, or a serde_json::Value-based parse); the one-way constraint is
/// that `from_openapi` accepts a standard OpenAPI 3.x JSON/YAML doc and
/// `to_openapi` produces one. Both directions share the same type.
pub struct OpenAPISpec {
pub info: OpenAPIInfo,
pub paths: BTreeMap<String, PathItem>,
pub components: Option<Components>,
// ... OpenAPI 3.x fields as needed
}
/// Configuration for an HTTP-backed adapter (`from_openapi`). Carries
/// the base URL, auth credentials (from `Capabilities` at registration,
/// not env vars — the no-env-vars invariant), and optional headers. The
/// `auth` field is the auth scheme the external API expects (bearer,
/// apiKey, basic); the credential itself is read from
/// `OperationContext.capabilities` at call time, not stored here.
pub struct HttpServiceConfig {
pub namespace: String,
pub base_url: String,
pub auth: Option<HttpAuthScheme>,
pub default_headers: HashMap<String, String>,
}
pub enum HttpAuthScheme {
Bearer, // Authorization: Bearer <token>
ApiKey { header_name: String }, // e.g., X-API-Key: <key>
Basic, // Authorization: Basic <credentials>
}
```
The adapter:
1. Parses the OpenAPI document (`OpenAPISpec``paths`, `components`,
`$ref` resolution). On parse failure, returns
`AdapterError::SchemaParse`. The TS prior art
(`@alkdev/operations/src/from_openapi.ts`) shows the parsing patterns:
`resolveRef` for `$ref`, `resolveRefsRecursive` for nested refs,
`buildInputSchema` (parameters + request body → input JSON Schema),
`buildOutputSchema` (200/201 response → output JSON Schema),
`detectOperationType` (SSE response → `Subscription`, GET → `Query`,
else `Mutation`).
2. For each `(path, method, operation)` in `spec.paths`, constructs a
`HandlerRegistration`:
- `spec.name` = the `operationId` (or a generated
`${method}_${path_parts}` name if `operationId` is absent — same
normalization as the TS `normalizeOperationId`).
- `spec.namespace` = the `config.namespace` (the importing
deployment's name for the service, not the OpenAPI doc's `info.title`).
- `spec.op_type` = `Query` / `Mutation` / `Subscription` (detected
from the method + response content type, same as TS).
- `spec.visibility` = `Internal` (adapter-registered ops are
composition material, not directly callable from the wire —
ADR-015).
- `spec.input_schema` / `output_schema` = the JSON Schemas built
from the OpenAPI parameters/responses.
- `spec.error_schemas` = the `ErrorDefinition`s built from the
non-2xx OpenAPI responses (ADR-023 §5 — see Error Fidelity below).
- `spec.access_control` = `AccessControl::default()` (the adapter
doesn't declare scopes; the composing handler that reaches the
imported op gates access).
- `handler` = a forwarding handler (see Forwarding Handler below).
- `provenance` = `FromOpenAPI`, `composition_authority: None`,
`scoped_env: None` (leaf — ADR-022).
- `capabilities` = the credentials the forwarding handler needs (the
bearer token / API key for the external HTTP endpoint, injected by
the assembly layer at registration — see No-Env-Vars below).
3. Returns the bundles. The caller (the assembly layer) registers them
in the `OperationRegistry`.
### Forwarding handler
The forwarding handler is the `Arc<dyn Handler>` stored in the
`HandlerRegistration`. At call time, it:
1. Reads the call input (`serde_json::Value`).
2. Builds the outbound HTTP request:
- URL path: substitutes path parameters (`{id}` → input value),
appends query parameters from input fields not in the path.
- Method: the OpenAPI operation's method.
- Headers: `Content-Type: application/json` + the auth header built
from `context.capabilities` (see No-Env-Vars below).
- Body: the `body` field of the input (for `Mutation`/`Subscription`).
3. Sends the request via the shared `reqwest::Client` (see HTTP Client
below).
4. For a `Query`/`Mutation`: parses the response body (JSON, text, or
binary — same content-type branching as the TS `createHTTPOperation`),
wraps it in a `ResponseEnvelope`, returns.
5. For a `Subscription` (`text/event-stream` response): streams
`call.responded` events as the SSE chunks arrive (same SSE parsing as
the TS `parseSSEFrames`), then `call.completed` on stream end.
6. On HTTP error (non-2xx): maps to the declared `ErrorDefinition` by
HTTP status code (see Error Fidelity below), returns a `CallError`.
The handler is opaque to the `CallAdapter` — it's an `Arc<dyn Handler>`
the registry dispatches. `alknet-call` never sees `reqwest`.
### HTTP client (reqwest)
`alknet-http` maintains a shared `reqwest::Client` (constructed once,
reused across all `from_openapi`/`from_mcp` forwarding handlers). The
client handles connection pooling, keep-alive, and TLS. The aisdk
`core/client.rs` reference shows the pattern worth referencing: a shared
client with `OnceLock<reqwest::Client>`, retry logic (exponential
backoff, `Retry-After` header), and separate streaming vs non-streaming
clients. `alknet-http` owns its HTTP client; it does not inherit aisdk's.
The retry/pooling config comes from `StaticConfig` or `DynamicConfig`
(hot-reloadable). The credential injection happens per-request (from
`OperationContext.capabilities`), not at client construction — the
client is shared across all operations, the credentials are per-call.
The exact pooling/retry config is a two-way-door implementation detail
(OQ-40); the one-way constraint is that `alknet-http` owns its `reqwest`
client (no env-var-based client config, no shared global client).
### No-Env-Vars credential injection
The forwarding handler is the **credential injection point** for the
no-env-vars architecture. The handler reads
`context.capabilities.get("<service>")` (e.g., `"openai"`, `"vastai"`,
`"github"`), extracts the credential, and injects it into the outbound
HTTP request:
- Bearer token → `Authorization: Bearer <token>`.
- API key → the header the OpenAPI spec declares (e.g., `X-API-Key:
<key>`, or `Authorization: ApiKey <key>` — the `HTTPServiceConfig.auth`
in the TS prior art shows the three auth types: `bearer`, `apiKey`,
`basic`).
- Basic auth → `Authorization: Basic <credentials>`.
The credential comes from `Capabilities`, which was populated by the
dispatch path from the `HandlerRegistration.capabilities` bundle
(ADR-022 §6), which was populated by the assembly layer from the vault
(ADR-014). The handler never reads `std::env::var`. This is the
spec-level invariant: no handler reads outbound credentials from any
source other than `OperationContext.capabilities`. See
[overview.md](overview.md) and
[client-and-adapters.md](../call/client-and-adapters.md).
### to_openapi
```rust
pub fn to_openapi(registry: &OperationRegistry) -> OpenAPISpec;
```
`to_openapi` generates an OpenAPI document from the local registry's
`External` operations:
1. For each `External` operation in the registry, generate an OpenAPI
path entry:
- Path: `/{service}/{op}` (the operation path, ADR-036 — the HTTP
path IS the operation path).
- Method: the operation's `OperationType` → HTTP method (`Query`→GET,
`Mutation`→POST by default, `Subscription`→GET with
`text/event-stream` response).
- `operationId`: the operation name.
- `parameters` / `requestBody` / `responses`: built from the
operation's `input_schema` / `output_schema` / `error_schemas`.
2. The `components.schemas` section holds the reusable schemas
referenced by `$ref` from the paths.
3. The `info` section carries the API title, version, and description.
This is a pure projection — it consumes the registry and produces a
spec. It does not modify the registry; it does not register operations;
it is not an `OperationAdapter`. The HTTP server serves the generated
spec at `GET /openapi.json` (or a configured path).
### Error Fidelity (ADR-023)
`from_openapi` maps OpenAPI non-2xx response status codes to
`ErrorDefinition`s (ADR-023 §5). The normative rule (review #002 W20):
`from_openapi` must not produce error codes that collide with the five
protocol-level codes (`NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`,
`INTERNAL`, `TIMEOUT`). The adapter prefixes imported error codes with
`HTTP_` and the status number:
```rust
// OpenAPI: 404: { schema: NotFoundError }
// → ErrorDefinition { code: "HTTP_404", http_status: Some(404), schema: NotFoundError }
```
`to_openapi` projects `error_schemas` back to OpenAPI response
definitions:
```yaml
responses:
'200': { schema: <output_schema> }
'404': { schema: <error_schemas[i].schema> } # where http_status = 404
'429': { schema: <error_schemas[j].schema> } # where http_status = 429
```
This makes the adapter contract from ADR-017 faithful on the error axis —
no silent dropping of error contracts. See ADR-023.
## Why
`from_openapi` is how alknet composes external HTTP APIs (OpenAI,
Anthropic, vast.ai, GitHub) into the call protocol. An operation
imported via `from_openapi` is a first-class operation: it has a spec,
it's discoverable via `services/list`, it can be composed by handlers,
its errors are typed. The agent crate's LLM provider calls go through
`from_openapi`-imported operations — that's how the no-env-vars
invariant makes aisdk's env-var reads unreachable.
`to_openapi` is how external systems discover the alknet operation
surface. An API gateway, a client generator, or a human developer reads
the OpenAPI doc to learn what operations exist and how to call them.
The generated spec is a compatibility contract (ADR-017 Consequences) —
once published, the mapping is one-way.
## Constraints
- **`from_openapi`/`from_mcp` handlers read credentials from
`OperationContext.capabilities`, not `std::env::var`.** This is the
no-env-vars invariant (ADR-014). The handler implementations are
verified against this invariant.
- **`from_openapi`-registered ops are `Internal` by default.** They are
composition material, not directly callable from the wire (ADR-015).
The handler that composes them is `External`.
- **`from_openapi` error codes are prefixed `HTTP_<status>`.** No
collision with protocol-level codes (ADR-023, review #002 W20).
- **`to_openapi` is a pure projection.** It consumes the registry, does
not produce entries for it. Not an `OperationAdapter`.
- **Published `to_openapi` specs are compatibility contracts.** The
generated spec's versioning (tied to the registry's `External`
operation set version) must be emitted so consumers can detect mapping
changes (ADR-017 Consequences, OQ-39).
- **`alknet-http` owns its `reqwest::Client`.** Shared across all
forwarding handlers, constructed once. No env-var-based client config.
Pooling/retry config is a two-way door (OQ-40).
- **TLS for outbound calls uses the system trust store by default.**
Standard HTTPS to external APIs (OpenAI, Anthropic). Custom CA bundle
+ client certs are an optional config for self-hosted API gateways.
This is a two-way-door implementation detail; the credential (API
key/token) comes from `Capabilities`, the TLS trust comes from the
system.
## Design Decisions
| Decision | ADR | Summary |
|----------|-----|---------|
| `from_openapi` is an `OperationAdapter` | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Async trait; produces `HandlerRegistration` bundles |
| `to_openapi` is a projection, not an adapter | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Consumes the registry, doesn't produce entries |
| Adapter-registered ops are `Internal` | [ADR-015](../../decisions/015-privilege-model-and-authority-context.md) | `from_openapi` ops are composition material |
| `from_openapi` provenance is a leaf | [ADR-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | `composition_authority: None`, `scoped_env: None` |
| Error fidelity (`HTTP_<status>` codes) | [ADR-023](../../decisions/023-operation-error-schemas.md) | No collision with protocol codes; `to_openapi` projects back |
| No-env-vars credential injection | [ADR-014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Handler reads `context.capabilities`, not env vars |
| HTTP path = operation path | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `to_openapi` paths mirror `/{service}/{op}` |
## Open Questions
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-39** (open): `to_openapi` published-spec versioning — the
versioning strategy for generated OpenAPI specs (tied to the
registry's `External` operation set version). One-way after first
publication.
- **OQ-40** (open): reqwest client config and connection pooling —
two-way-door: the exact pooling/retry config shape, hot-reloadable
via `DynamicConfig`.
## References
- [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md)
— `OperationAdapter` trait, `to_*` are projections
- [ADR-023](../../decisions/023-operation-error-schemas.md) — error
fidelity, `HTTP_<status>` prefix rule
- [overview.md](overview.md) — adapter location map, no-env-vars
invariant
- [../call/client-and-adapters.md](../call/client-and-adapters.md) —
`OperationAdapter` trait, `AdapterError` variants (OQ-26), no-env-vars
invariant
- `/workspace/@alkdev/operations/src/from_openapi.ts` — TypeScript prior
art (parsing, SSE, auth headers, `createHTTPOperation`)
- `/workspace/aisdk/src/core/client.rs` — HTTP client reference (pooling,
retry, streaming vs non-streaming)

View File

@@ -0,0 +1,245 @@
---
status: draft
last_updated: 2026-06-29
---
# HTTP MCP — from_mcp and to_mcp
The MCP-direction adapters (feature-gated behind `mcp`): `from_mcp`
imports remote MCP tools as call-protocol operations over streamable
HTTP (reqwest client), and `to_mcp` exposes local operations as MCP
tools over streamable HTTP (axum server). This document covers both, the
rmcp integration, and the stdio exclusion (ADR-037).
## What
Two adapters, both in `alknet-http`, both behind the `mcp` feature gate:
1. **`from_mcp`** — discovers remote MCP tools via the MCP
`tools/list` call over streamable HTTP, and registers each as a
`HandlerRegistration` bundle with a forwarding handler that calls the
remote tool via `tools/call`. Uses rmcp's
`StreamableHttpClientTransport` (reqwest-based). Provenance is
`FromMCP` (leaf, `composition_authority: None`, `scoped_env: None`,
`Internal` by default — ADR-015/022). Implements `OperationAdapter`.
2. **`to_mcp`** — exposes the local registry's `External` operations as
MCP tools over streamable HTTP, using rmcp's `StreamableHttpService`
(an axum-compatible tower service). An external MCP client (an editor,
an AI tool) discovers and calls alknet operations through the MCP
protocol. A pure projection (consumes the registry, does not produce
entries — ADR-017 §5).
### Streamable HTTP only (ADR-037)
MCP defines two transports: streamable HTTP and stdio. **alknet-http
supports only streamable HTTP.** Stdio is not built — it is the spawn-
arbitrary-executable RCE vector that the rest of the architecture is
designed to avoid (ADR-037). The `mcp` feature gate pulls in rmcp with
the streamable HTTP transport features only; the stdio transport
(`transport-child-process`) is not a dependency, not optional, not
behind a separate feature.
If an operator wants a stdio-only MCP server, they run a small
streamable-HTTP-to-stdio bridge themselves, outside alknet. The bridge
is where the RCE risk lives, explicitly in the operator's hands. See
ADR-037.
### from_mcp
```rust
pub struct FromMCP {
/// The MCP server's streamable HTTP endpoint URL.
endpoint: String,
/// Bearer token for the MCP server (from Capabilities at registration).
auth_token: Option<String>,
/// The importing deployment's name for this MCP server (becomes the
/// operation namespace).
namespace: String,
}
#[async_trait]
impl OperationAdapter for FromMCP {
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
}
```
The adapter:
1. Connects to the MCP server's streamable HTTP endpoint using rmcp's
`StreamableHttpClientTransport::from_uri(endpoint)` (the rmcp
`streamable_http.rs` client example shows the pattern: `client_info
.serve(transport).await`, then `client.list_tools()`,
`client.call_tool()`). On connection failure, returns
`AdapterError::DiscoveryFailed`; on 401, `AdapterError::Unauthorized`.
2. Calls `tools/list` → the list of MCP tools (name, description,
`inputSchema`, optional `outputSchema`).
3. For each tool, constructs a `HandlerRegistration`:
- `spec.name` = the tool name (or `namespace/tool_name` if a
namespace prefix is configured — same local-naming sugar as
`from_call`'s `FromCallConfig::namespace_prefix`, ADR-029 §5).
- `spec.namespace` = the configured `namespace`.
- `spec.op_type` = `Mutation` (MCP tools are call/response; the MCP
spec doesn't have a native streaming/tool-subscription distinction
`tools/call` returns a result. If MCP adds a streaming-tool
extension, a `Subscription` mapping would be added.)
- `spec.visibility` = `Internal` (adapter-registered, ADR-015).
- `spec.input_schema` = the tool's `inputSchema` (JSON Schema).
- `spec.output_schema` = the tool's `outputSchema`, or
`Type.Unknown()` if absent (the TS `from_mcp.ts` shows this
fallback).
- `spec.error_schemas` = the MCP tool's error description mapped to
`ErrorDefinition` (ADR-023 — MCP tool definitions carry error
descriptions; the adapter maps them).
- `spec.access_control` = `AccessControl::default()`.
- `handler` = a forwarding handler (see Forwarding Handler below).
- `provenance` = `FromMCP`, `composition_authority: None`,
`scoped_env: None` (leaf — ADR-022).
- `capabilities` = the bearer token for the MCP server (injected by
the assembly layer at registration — see No-Env-Vars below).
4. Returns the bundles. The caller (the assembly layer) registers them
in the `OperationRegistry`.
### Forwarding handler
At call time, the `from_mcp` forwarding handler:
1. Reads the call input (`serde_json::Value` — the tool arguments).
2. Calls `client.call_tool({ name: tool_name, arguments: input })` via
the rmcp client (the `streamable_http.rs` example shows
`client.call_tool(CallToolRequestParams::new(name).with_arguments(...))`).
3. On success: extracts `structuredContent` (if present) or maps the
`content` blocks (the TS `mapMCPContentBlocks` shows the mapping:
text/image/audio/resource/resource_link → `MCPContentBlock`),
wraps in a `ResponseEnvelope`, returns.
4. On `result.isError`: maps to a `CallError` with the MCP error content
(the TS `from_mcp.ts` handler shows the error mapping), returns.
5. The rmcp client connection is maintained for the lifetime of the
registration (the MCP server is a persistent streamable HTTP
endpoint, not a per-call connection).
The handler is opaque to the `CallAdapter``Arc<dyn Handler>` the
registry dispatches. `alknet-call` never sees rmcp.
### to_mcp
```rust
pub fn to_mcp_service(
registry: Arc<OperationRegistry>,
identity_provider: Arc<dyn IdentityProvider>,
) -> StreamableHttpService<...>;
```
`to_mcp` exposes the local registry's `External` operations as MCP tools
over streamable HTTP, using rmcp's `StreamableHttpService` (an
axum-compatible tower service). The rmcp
`simple_auth_streamhttp.rs` server example shows the pattern:
```rust
// From the rmcp example:
let mcp_service: StreamableHttpService<Counter, LocalSessionManager> =
StreamableHttpService::new(
|| Ok(Counter::new()),
LocalSessionManager::default().into(),
StreamableHttpServerConfig::default(),
);
let protected_mcp_router = Router::new()
.nest_service("/mcp", mcp_service)
.layer(middleware::from_fn_with_state(token_store, auth_middleware));
```
`alknet-http`'s `to_mcp` follows the same pattern: the local operations
are exposed as an MCP server (an rmcp `Service` impl that wraps the
`OperationRegistry`), the `StreamableHttpService` nests into the axum
`Router` at `/mcp`, and a Bearer auth middleware gates access (the
`simple_auth_streamhttp.rs` `auth_middleware` + `extract_token` pattern).
The `to_mcp` service:
1. On MCP `tools/list`: returns the local registry's `External`
operations as MCP tools (name, description, `inputSchema`).
2. On MCP `tools/call`: dispatches to the `OperationRegistry::invoke()`
— the same dispatch path the HTTP server uses for HTTP requests
(ADR-036). The MCP tool call becomes a `call.requested` internally.
The result is mapped back to the MCP `tools/call` response shape
(`structuredContent` or `content` blocks).
3. Auth: the Bearer middleware resolves the token via
`IdentityProvider::resolve_from_token()`, same as the HTTP server's
auth (ADR-004). The MCP client authenticates by bearer token; no
`PeerId` (browsers and MCP clients are not alknet peers — ADR-034 §4).
### No-Env-Vars
The `from_mcp` forwarding handler reads the MCP server's bearer token
from `context.capabilities` (the same injection path as `from_openapi`),
not from `std::env::var`. The assembly layer injects the token at
registration; the handler reads it per-call. This is the no-env-vars
invariant (ADR-014, [overview.md](overview.md)).
## Why
MCP is the protocol editors and AI tools use to discover and call tools.
`from_mcp` lets alknet compose external MCP servers (a remote tool
server, a third-party MCP endpoint) into the call protocol — the same
composition pattern as `from_openapi` and `from_call`. `to_mcp` lets
external MCP clients (an editor, an AI tool) discover and call alknet
operations through the MCP protocol, without those clients needing to
speak EventEnvelope.
The streamable-HTTP-only constraint (ADR-037) is a security position:
alknet does not import the MCP stdio RCE vector. The streamable HTTP
path is network-isolated, auth-gatable, and runs under alknet's
auth/identity/capabilities machinery — the same machinery that gates
every other HTTP request.
## Constraints
- **Streamable HTTP only.** Stdio is not built (ADR-037). The `mcp`
feature pulls in rmcp with streamable HTTP transport features only.
- **`from_mcp`-registered ops are `Internal` by default.** Composition
material, not directly callable from the wire (ADR-015).
- **`from_mcp` handlers read credentials from
`OperationContext.capabilities`.** No env vars (ADR-014).
- **`to_mcp` is a pure projection.** Consumes the registry, does not
produce entries. Not an `OperationAdapter`.
- **MCP clients are not alknet peers.** A browser or MCP client
connecting to `to_mcp` authenticates by bearer token, gets no
`PeerId`, is not in the peer graph (ADR-034 §4).
- **The `mcp` feature is optional.** A deployment that doesn't need MCP
doesn't compile rmcp. The default feature set is `h2` + `http1`.
## Design Decisions
| Decision | ADR | Summary |
|----------|-----|---------|
| MCP stdio transport excluded | [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built |
| `from_mcp` is an `OperationAdapter` | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Async trait; produces `HandlerRegistration` bundles |
| `to_mcp` is a projection | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | Consumes the registry, doesn't produce entries |
| Adapter-registered ops are `Internal` | [ADR-015](../../decisions/015-privilege-model-and-authority-context.md) | `from_mcp` ops are composition material |
| `from_mcp` provenance is a leaf | [ADR-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | `composition_authority: None`, `scoped_env: None` |
| Error fidelity | [ADR-023](../../decisions/023-operation-error-schemas.md) | MCP tool errors mapped to `ErrorDefinition`s |
| No-env-vars credential injection | [ADR-014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Handler reads `context.capabilities`, not env vars |
| MCP clients are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Bearer token, no `PeerId` |
## Open Questions
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-40** (open): reqwest client config — the shared `reqwest::Client`
used by `from_mcp` (same client as `from_openapi`).
## References
- [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md) — the
stdio exclusion this document enforces
- [overview.md](overview.md) — adapter location map, feature gates
- [../call/client-and-adapters.md](../call/client-and-adapters.md) —
`OperationAdapter` trait, `AdapterError` variants
- `/workspace/rust-sdk/` — MCP Rust SDK (rmcp v1.8.0); streamable HTTP
transport
- `/workspace/rust-sdk/examples/servers/src/simple_auth_streamhttp.rs`
— streamable HTTP MCP server with Bearer auth (the `to_mcp` pattern)
- `/workspace/rust-sdk/examples/clients/src/streamable_http.rs`
streamable HTTP MCP client (the `from_mcp` pattern)
- `/workspace/@alkdev/operations/src/from_mcp.ts` — TypeScript prior art
(`createMCPClient`, `mapMCPContentBlocks`, the `MCPClientLoader`)

View File

@@ -0,0 +1,292 @@
---
status: draft
last_updated: 2026-06-29
---
# HTTP Server
The `HttpAdapter` — the `ProtocolHandler` for `h2` and `http/1.1` (and
`h3`, covered in [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.
## What
The `HttpAdapter` is constructed by the assembly layer with an
`Arc<dyn IdentityProvider>` (constructor injection, same pattern as
`SshAdapter` — see [auth.md](../core/auth.md)) and an
`Arc<OperationRegistry>` (for dispatching HTTP requests to call-protocol
operations). It implements `ProtocolHandler` for the standard HTTP ALPNs.
```rust
pub struct HttpAdapter {
identity_provider: Arc<dyn IdentityProvider>,
registry: Arc<OperationRegistry>,
/// The default handler for paths that are not registered operations
/// (stealth decoy). Configurable: a static site, a fake 404, a
/// redirect. Two-way-door default (ADR-010).
decoy: DecoyConfig,
}
/// 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.
pub enum DecoyConfig {
/// Serve a fake `404 Not Found` (the default — matches the reference
/// implementation's "fake nginx 404").
NotFound,
/// Serve a static site from a configured directory (the directory
/// path is the payload). For deployments that want a real decoy
/// website.
StaticSite { root: PathBuf },
/// Redirect to a configured URL.
Redirect { to: String },
}
#[async_trait]
impl ProtocolHandler for HttpAdapter {
fn alpn(&self) -> &'static [u8]; // returns the configured ALPN
async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError>;
}
```
The `HttpAdapter` registers for multiple ALPNs (`http/1.1`, `h2`, `h3`).
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)).
## Why
HTTP is the standard external interface. Browsers, curl, axios, API
gateways, and load balancers all speak HTTP. Serving HTTP on the standard
ALPNs means any HTTP client can connect without knowing about alknet —
the TLS handshake negotiates `h2` or `http/1.1` normally. This is the
stealth mapping (ADR-010): the HTTP surface is the decoy for clients that
don't offer alknet ALPNs, and the real external API surface for clients
that do know about alknet.
## Architecture
### Running axum over a QUIC stream
The `HttpAdapter::handle()` method for `h2`/`http/1.1`:
1. Accepts one bidirectional stream from the QUIC connection
(`connection.accept_bi()``(SendStream, RecvStream)`).
2. Wraps the `(SendStream, RecvStream)` pair as a hyper
`TokioIo`-compatible duplex stream — the same byte stream hyper
expects for an HTTP connection.
3. Constructs the axum `Router` (built once at adapter construction,
cloned per connection — axum `Router` is `Clone` and cheap to clone).
4. Hands the duplex stream + the axum router to hyper's connection
driver (`hyper::server::conn::http1::Builder` or
`http2::Builder::serve_connection`), which reads HTTP frames, parses
them, dispatches to axum routes, and writes HTTP responses.
5. Returns when the HTTP connection closes (the client disconnects or
the stream ends).
The axum `Router` is built once at adapter construction with the
`Arc<OperationRegistry>` and `Arc<dyn IdentityProvider>` embedded in its
state; cloning the `Router` per connection clones the `Arc`s (cheap,
shared state), so every request handler has access to the registry and
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).
- `GET /healthz` (raw route, no auth, no call protocol).
- `GET /openapi.json` (serves the `to_openapi` projection).
- The stealth decoy fallback (unknown paths).
- (Feature-gated) `POST /mcp` (the `to_mcp` streamable HTTP service —
[http-mcp.md](http-mcp.md)).
A single HTTP/2 or HTTP/1.1 connection multiplexes multiple requests
over the one bidirectional stream (HTTP/2 multiplexing is native;
HTTP/1.1 is sequential). The axum router handles each request on a
tokio task; the hyper driver manages the connection lifetime.
### HTTP-to-call dispatch (ADR-036)
An HTTP request at `POST /fs/readFile` (or `GET /services/list`, or any
`/{service}/{op}` path matching a registered `External` operation) is
dispatched to the call protocol:
1. The axum route handler extracts the operation name from the path
(`/fs/readFile``fs/readFile`, stripping the leading slash — the
registry form).
2. It resolves the caller's identity from the `Authorization: Bearer`
header via `identity_provider.resolve_from_token(&AuthToken { raw:
token_bytes })`.
3. It parses the request body as the operation input (JSON).
4. It constructs the root `OperationContext` (caller identity, the
registration bundle's capabilities, the connection's env composition)
and dispatches through the `OperationRegistry::invoke()` — the same
dispatch path the `CallAdapter` uses for `alknet/call` wire requests.
5. The response (`ResponseEnvelope`) is serialized as the HTTP response
body (JSON). Errors map to HTTP status codes (see Error Mapping
below).
`Internal` operations (ADR-015) return `404` on the HTTP handler,
matching the call protocol's `NOT_FOUND` for wire calls to Internal
ops — the HTTP handler dispatches only `External` operations.
### Streaming projection (SSE)
A `Subscription` operation served over `h2`/`http/1.1` projects its
`call.responded` stream as Server-Sent Events. The axum route handler:
- Sets `Content-Type: text/event-stream`.
- For each `call.responded` event, writes an SSE `data:` frame (the
event's `output` serialized as JSON).
- On `call.completed`, closes the SSE stream (normal end).
- On `call.aborted`, closes the stream with an SSE error event.
- On HTTP client disconnect (detected as the response writer closing),
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)).
### Auth
Inbound HTTP auth is `Authorization: Bearer <token>`, resolved via
`IdentityProvider::resolve_from_token()` (the auth.md handler table:
`HttpAdapter`, Bearer header, `resolve_from_token`). Bearer-only is the
auth mechanism for the default surface; other HTTP auth schemes (Basic,
API key in query param) are not implemented and would be added as axum
middleware (two-way door). This is recorded in
[ADR-036](../../decisions/036-http-to-call-operation-mapping.md) §Auth;
the resolution mechanism (`resolve_from_token`) is from
[ADR-004](../../decisions/004-auth-as-shared-core.md), and the
connection-level observability (`set_identity`) is OQ-11 (resolved).
- Bearer-only is the auth mechanism. Basic auth, API keys in query
params, and other HTTP auth schemes are not implemented. A deployment
that needs a different auth scheme adds it as axum middleware
(two-way door), but the default surface is Bearer-only.
- The `HttpAdapter` constructor-injects `Arc<dyn IdentityProvider>`,
same pattern as `SshAdapter`.
- An unauthenticated request to an operation with `AccessControl`
restrictions returns `401` (no token) or `403` (token present but
insufficient scopes). The call protocol's `FORBIDDEN` protocol code
maps to `403`; `NOT_FOUND` (Internal op) maps to `404`.
- The HTTP handler stores the resolved identity on the `Connection` for
observability (`connection.set_identity(identity)`), same as the call
protocol handler.
### Error Mapping
Call-protocol `CallError` codes (ADR-023) map to HTTP status codes:
| Call `code` | HTTP status | Notes |
|-------------|-------------|-------|
| `NOT_FOUND` (operation not registered, or Internal op) | `404` | |
| `FORBIDDEN` (insufficient scopes, or unauthenticated) | `401` (no token) / `403` (token present) | |
| `INVALID_INPUT` (schema mismatch) | `422` | |
| `TIMEOUT` | `504` | `retryable: true` |
| `INTERNAL` | `500` | |
| Operation-level domain code with `http_status` (ADR-023) | the declared `http_status` | `from_openapi`-imported ops carry the original status |
| Operation-level domain code without `http_status` | `500` | |
The `retryable` field from `CallError` maps to an HTTP `Retry-After`
hint for `503`/`429`-class errors. The mapping is a two-way-door
default (the exact status for ambiguous codes can be refined
additively); the one-way constraint is that protocol-level and
operation-level codes are distinct (ADR-023) and `from_openapi`-imported
codes are prefixed `HTTP_<status>` to avoid collision with protocol
codes.
### `/healthz` (raw route)
`GET /healthz` is a raw HTTP route outside the call protocol — no auth,
no operation registration, no `OperationContext`. It returns `200 OK`
with a plain-text body (e.g., `"ok"`) if the endpoint is healthy. This
is the infrastructure endpoint load balancers and orchestrators call;
it must work before identity is resolvable.
Other operational endpoints (metrics, dashboard) are call-protocol
operations if built (`/metrics/list`, `/dashboard/view`), not raw HTTP
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`):
- A fake `404 Not Found` (the default — matches the reference
implementation's "fake nginx 404").
- A static site (served from a configured directory).
- A redirect (to a configured URL).
The decoy is the stealth surface: a port scanner or a client that
doesn't offer alknet ALPNs connects on `h2`/`http/1.1` and sees the
decoy. Real services use `alknet/ssh`, `alknet/call`, etc. The decoy
config is a two-way-door default (an operator picks what to serve); the
*existence* of the stealth path is fixed by ADR-010.
## Constraints
- **The HTTP path IS the operation path.** `POST /fs/readFile`
`call.requested` for `fs/readFile`. No second routing table. See
ADR-036.
- **`External` operations only.** `Internal` operations return `404`
on the HTTP handler.
- **Bearer-only auth.** `Authorization: Bearer`
`resolve_from_token`. Other HTTP auth schemes are not implemented.
- **No secret material in HTTP responses.** The call protocol carries no
secret material (ADR-014); the HTTP handler inherits this constraint.
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.
## Design Decisions
| 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` |
| 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 |
| 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 |
| Error mapping (call codes → HTTP status) | [ADR-023](../../decisions/023-operation-error-schemas.md) | Protocol/operation codes distinct; `HTTP_<status>` prefix for imported |
## Open Questions
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-39** (open): `to_openapi` published-spec versioning — the
generated OpenAPI spec is a compatibility contract (ADR-017
Consequences); the versioning strategy needs specifying.
- **OQ-40** (open): reqwest client config and connection pooling —
two-way-door config shape for the outbound HTTP client used by
`from_openapi`/`from_mcp`.
## References
- [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
- [overview.md](overview.md) — crate overview, adapter location map
- [webtransport.md](webtransport.md) — the `h3` ALPN handler
- [http-adapters.md](http-adapters.md) — `from_openapi`/`to_openapi`
- [../core/auth.md](../core/auth.md) — `IdentityProvider`, Bearer →
`resolve_from_token`
- [../core/endpoint.md](../core/endpoint.md) — stealth mode as ALPN
dispatch
- [../call/operation-registry.md](../call/operation-registry.md) —
`OperationRegistry::invoke()`, the dispatch path HTTP requests hit

View File

@@ -0,0 +1,243 @@
---
status: draft
last_updated: 2026-06-29
---
# 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
details are in the sibling documents.
## What
`alknet-http` is the HTTP protocol handler for the ALPN-as-service
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
`to_openapi`/`to_mcp` projections of local call-protocol operations,
the `/healthz` operational endpoint, and the decoy surface for
stealth mode.
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
remote MCP tools over streamable HTTP, using `reqwest`). The reverse
projections `to_openapi` (generate an OpenAPI doc from the local
registry's `External` operations) and `to_mcp` (expose local ops as
MCP tools over streamable HTTP, using `axum`) also live here.
Both directions share the same HTTP dependencies (`axum` for serving,
`reqwest` for calling out), which is why they live in one crate rather
than being split into a server crate and a client crate. See
[ADR-039](../../decisions/039-http-server-and-client-host-colocated.md)
for the full rationale.
## Why
The crate's purpose is to be the HTTP interface library for downstream
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.
The key architectural insight that shapes the crate: **HTTP is both a
server surface and a client transport for adapters.** The server side
serves HTTP to external clients (browsers, curl, axios); the client side
makes outbound HTTP calls to external APIs (OpenAI, Anthropic, vast.ai)
through the `from_openapi`/`from_mcp` forwarding handlers. Both
directions share HTTP dependencies and HTTP-specific concerns (TLS,
headers, streaming, SSE), so they belong in one crate. See
[ADR-039](../../decisions/039-http-server-and-client-host-colocated.md)
for the colocation decision.
## Dependencies
```
alknet-http
├── alknet-core (ProtocolHandler, Connection, AuthContext, IdentityProvider, Capabilities)
├── alknet-call (OperationAdapter, OperationSpec, Handler, HandlerRegistration,
│ OperationRegistry, AdapterError, OperationProvenance)
├── axum (HTTP server — Router, extractors, middleware)
├── 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`)
```
### The `alknet-call` dependency (ADR-003 Amendment 1)
`alknet-http` depends on `alknet-call`. ADR-003's rule is "no handler
crate depends on another handler crate," but `alknet-call` is both a
handler (it implements `ProtocolHandler` on `alknet/call`) *and* the
protocol-foundation crate that `alknet-agent`, `alknet-napi`, and now
`alknet-http` consume. `alknet-http` depending on `alknet-call` is
"HTTP uses the call protocol types" (`OperationSpec`, `Handler`,
`HandlerRegistration`, `OperationAdapter`), not "HTTP depends on SSH."
See [ADR-003 Amendment 1](../../decisions/003-crate-decomposition.md).
`alknet-call` stays lean — it has no `reqwest`, no `axum`, no HTTP
dependencies. The `from_openapi`/`from_mcp` forwarding handlers are
opaque `Arc<dyn Handler>` from the registry's perspective: constructed by
`alknet_http::from_openapi()` at registration time, stored in
`HandlerRegistration`, dispatched by the `CallAdapter` which doesn't
know `reqwest` is involved.
## ALPNs
| 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) |
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.
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.
## Adapter Location Map
The decomposition principle (settled in
[client-and-adapters.md](../call/client-and-adapters.md)): the adapter
trait lives where the types live (`alknet-call`); the adapter
implementations live where their transport dependencies live.
```
alknet-call (lean — no HTTP client, no HTTP server)
├── OperationAdapter trait (the contract — async, ADR-017 §5)
├── from_call (QUIC — discovers remote ops via call protocol)
├── from_jsonschema (pure parse — caller fetches the doc, passes it in)
└── CallClient (outbound connection opener)
alknet-http (owns HTTP server + HTTP client)
├── HttpAdapter (axum server — inbound HTTP on h2/http1.1/h3)
├── 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)
└── to_mcp (feature-gated) (expose local ops as MCP tools over streamable HTTP — axum)
```
`alknet-call` never sees the HTTP client. The `from_openapi`/`from_mcp`
forwarding handlers are opaque `Arc<dyn Handler>` from the registry's
perspective. `alknet-call` stays lean; `alknet-http` owns both HTTP
directions.
## Feature Gates
```toml
[features]
default = ["h2", "http1"] # the non-browser HTTP surface
h3 = ["dep:wtransport"] # HTTP/3 + WebTransport (browser path; X.509 required)
mcp = ["dep:rmcp"] # from_mcp / to_mcp (streamable HTTP only — ADR-037)
```
- `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).
- `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`.
## The No-Env-Vars Invariant
The `from_openapi`/`from_mcp` forwarding handlers are the **credential
injection point** for the no-env-vars architecture. The path (from the
gap analysis):
```
vault → assembly layer → Capabilities → HandlerRegistration.capabilities
→ OperationContext.capabilities → from_openapi handler reads
context.capabilities.get("openai") → injects into HTTP Authorization
header → reqwest request goes out with vault-derived credential
```
This makes aisdk's `std::env::var("OPENAI_API_KEY")` reads unreachable —
the assembly layer never calls `Default::default()` on a provider; it
constructs them with vault-derived credentials, or routes HTTP calls
through `from_openapi` operations that carry the credential in
`Capabilities`.
**This is a spec-level invariant**: no handler reads outbound
credentials from any source other than `OperationContext.capabilities`.
The `from_openapi`/`from_mcp` implementations in `alknet-http` are
verified against this invariant. See ADR-014 and
[client-and-adapters.md](../call/client-and-adapters.md).
## 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-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).
## Design Decisions
| Decision | ADR | Summary |
|----------|-----|---------|
| 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 |
| 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) |
| `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) |
| Adapter-registered ops are `Internal` | [ADR-015](../../decisions/015-privilege-model-and-authority-context.md) | `from_openapi`/`from_mcp` produce `Internal` leaves (settled) |
| `OperationAdapter` trait is async | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | HTTP adapters implement the async trait (settled) |
| `to_*` adapters are projections | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `to_openapi`/`to_mcp` consume the registry, don't produce entries (settled) |
| Error schema fidelity | [ADR-023](../../decisions/023-operation-error-schemas.md) | `from_openapi` maps HTTP status → `HTTP_<status>` codes; `to_openapi` projects back (settled) |
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3`/WebTransport needs X.509 (settled) |
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Browser over WebTransport/HTTPS = bearer token, no `PeerId` (settled) |
## Open Questions
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-13** (resolved): Operation path format `/{service}/{op}` — the
HTTP path.
- **OQ-26** (resolved): `AdapterError` variants — reused by HTTP
adapters; `#[non_exhaustive]` allows extension.
- **OQ-37** (resolved): Browsers are not peers; `h3` hub is a
mixed-fingerprint `PeerEntry`.
- **OQ-38** (open, scope): WebTransport relay-as-proxy — does the proxy
live in `alknet-http` or a separate relay crate?
- **OQ-39** (open): `to_openapi` published-spec versioning — versioning
strategy for generated OpenAPI specs.
- **OQ-40** (open): reqwest client config and connection pooling —
two-way-door config shape.
## References
- `docs/research/alknet-http/phase-0-findings.md` — Phase 0 research
- `docs/research/alknet-call-completion/gap-analysis.md` — adapter
location map, no-env-vars invariant
- `/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

View File

@@ -0,0 +1,232 @@
---
status: draft
last_updated: 2026-06-29
---
# WebTransport — the h3 ALPN handler
The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and
WebTransport. This document covers the WebTransport session/stream
handling, the browser streaming path, and the relationship to the `h2`/
`http/1.1` server. The `h3` support is a first-class transport, not a
deferral (ADR-038).
## What
The `h3` ALPN handler is the same `HttpAdapter` instance that serves
`h2`/`http/1.1`, registered for the `h3` ALPN when the `h3` feature is
enabled. It serves two things on a single `h3` connection:
1. **HTTP/3 requests** — the standard HTTP/3 over QUIC framing. An
HTTP/3 request is dispatched through the same axum `Router` as `h2`/
`http/1.1` requests (ADR-036 — the HTTP path IS the operation path).
From the axum router's perspective, an HTTP/3 request is just
another HTTP request; the framing difference is handled below the
router.
2. **WebTransport sessions** — the browser streaming path. A WebTransport
session is a long-lived connection over which the browser opens
bidirectional and unidirectional streams. A WebTransport stream that
targets the call protocol is handed to the call protocol's dispatch
loop directly — a WebTransport bidirectional stream is a QUIC
bidirectional stream, the same stream type the call protocol already
speaks (ADR-012).
### Why h3 is a first-class transport
WebTransport is the browser streaming transport. QUIC streams are cheap
(multiplexed over one connection, no head-of-line blocking), and
WebTransport is supported in major browsers. The call protocol's
subscription/streaming model maps onto WebTransport streams with no
translation loss — a `call.responded` stream over a WebTransport
bidirectional stream is the native representation, not an SSE
translation (which is the projection for `h2`/`http/1.1` clients per
ADR-036).
The Phase 0 research framing ("defer h3/WebTransport past v1") was a
residual of the "two-way door as deferral" anti-pattern (ADR-009 §"What
this framework is NOT"). WebTransport is in scope, in this crate, as a
first-class transport. See ADR-038.
## Architecture
### The h3 handler entry
The `HttpAdapter::handle()` method for the `h3` ALPN drives two
distinct stream types, distinguished at the HTTP/3 framing layer (not by
peeking application bytes):
1. **HTTP/3 request streams** — standard HTTP/3 GET/POST carrying
`:method`/`:path`. These are the same request model as `h2`/
`http/1.1`, just over HTTP/3 framing. Dispatched through the axum
`Router` (same router as `h2`/`http/1.1`, ADR-036). An HTTP/3 request
is never a WebTransport stream — the stream type is set by the
HTTP/3 frame that opens it.
2. **WebTransport sessions** — opened by a browser's
`new WebTransport(url)` call, which triggers an HTTP/3 extended
CONNECT request. The handler accepts the session (the `wtransport`
crate's `Endpoint::server(config)?.accept().await.await?.accept()
.await?` pattern, or the quinn HTTP/3 endpoint's WebTransport
extension — the exact library is a two-way-door implementation
detail, ADR-038). Within an established session, the browser opens
bidirectional streams via `transport.createBidirectionalStream()`;
the handler accepts each via `session.accept_bi()`.
The two stream types are not disambiguated by "reading the first frame"
— they are distinguished by the HTTP/3 frame type that opens them
(regular request headers vs. extended CONNECT). The "first frame"
routing below applies *within* a WebTransport session, not between an
HTTP/3 request and a WebTransport stream.
### WebTransport session and stream handling
Once a WebTransport session is established (via extended CONNECT), the
browser creates bidirectional streams within it. The handler accepts
each stream (`session.accept_bi()`) and reads the first frame to
determine the sub-protocol:
- **Call-protocol `EventEnvelope`** — the stream is a call-protocol
stream. The handler hands the `(SendStream, RecvStream)` pair to the
call protocol's `Dispatcher` (see [../call/call-protocol.md](../call/call-protocol.md)
for `EventEnvelope` and [../call/client-and-adapters.md](../call/client-and-adapters.md)
§"Shared Dispatcher" for the `Dispatcher` — the same dispatch loop
the `CallAdapter` uses for `alknet/call` connections, ADR-012,
stream-agnostic correlation). The browser speaks the EventEnvelope
wire format directly over the WebTransport stream.
- **Other sub-protocols** — a session may carry other framing
conventions (e.g., a future WT-native RPC framing). The session's
purpose is declared at CONNECT time (by path/origin), so the handler
knows which sub-protocol to expect; the first-frame tag is a
belt-and-suspenders disambiguator for sessions that multiplex
sub-protocols. For the call-protocol session, the first frame is an
`EventEnvelope` JSON object; the handler dispatches accordingly.
The browser's `WebTransport` JS API is the client side of this:
`new WebTransport('https://hub.example.com')`
`transport.createBidirectionalStream()` → write an `EventEnvelope` frame
→ read `call.responded` frames. No SSE translation, no HTTP framing —
the call protocol speaks directly over the WebTransport stream.
### Subscription projection (native, not SSE)
A `Subscription` operation served over WebTransport projects its
`call.responded` stream directly onto the WebTransport bidirectional
stream — each `call.responded` event is a frame on the stream, no SSE
`data:` framing. `call.completed` closes the stream; `call.aborted`
closes the stream with an error frame. This is the native streaming
projection; SSE (ADR-036) is the projection for `h2`/`http/1.1` clients
that don't speak WebTransport.
### The TLS constraint (browsers require X.509)
Browsers do not support RFC 7250 raw public keys (ADR-027, OQ-12). A
WebTransport session from a browser requires an X.509 cert — the `h3`
handler is a domain-hosted-service concern, not a P2P concern. A node
serving WebTransport must have an X.509 identity
(`TlsIdentity::X509` or `TlsIdentity::Acme`).
This is a property of the browser, not a decision this spec makes. It's
recorded so the spec doesn't pretend a raw-key node can serve browsers.
A raw-key node serves `h2`/`http/1.1` (for curl, axios, alknet-native
clients) but not `h3`/WebTransport (for browsers). A browser-facing hub
has a `PeerEntry` with mixed fingerprints (Ed25519 for P2P, X.509 for
browsers — ADR-030, ADR-034 §3).
### Browsers are not alknet peers
A browser connecting to a hub over WebTransport is served by the `h3`
handler. The browser authenticates by bearer token (HTTP `Authorization`
header on the WebTransport session request), 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 (ADR-034 §4): it gets no `PeerId`, does not enter
`PeerCompositeEnv`, and its "ops" are WebTransport streams served by
the `h3` handler, not entries in the call-protocol peer-keyed overlay.
### Stealth on h3
The `h3` handler participates in the same stealth model as `h2`/
`http/1.1` (ADR-010, ADR-036): a client that offers `h3` gets the HTTP
handler. Unknown WebTransport paths and unknown HTTP/3 paths get the
decoy (the same configurable `DecoyConfig` — fake 404, static site,
redirect). Real services use `alknet/ssh`, `alknet/call`, etc.
### Implementation reference: wtransport
The `wtransport` crate (`/workspace/wtransport/`, v0.7.1) is a pure-Rust
WebTransport implementation built on `quinn` + `h3`/`qpack`. Its API:
```rust
// Server (from the wtransport README):
let config = ServerConfig::builder()
.with_bind_default(4433)
.with_identity(&identity) // X.509 identity
.build();
let connection = Endpoint::server(config)?
.accept().await // await connection
.await? // await session request
.accept().await?; // await ready session
let stream = connection.accept_bi().await?;
```
`wtransport` is a candidate dependency for the `h3` feature gate. The
exact WebTransport library choice (wtransport vs a quinn-native HTTP/3
+ WebTransport extension) is a two-way-door implementation detail
(ADR-038); the one-way constraint is that `h3` is served by this crate
as a first-class transport.
## Constraints
- **`h3` requires X.509.** Browsers don't support RFC 7250 (ADR-027).
A node serving `h3` must have an X.509 identity. Raw-key-only nodes
serve `h2`/`http/1.1` but not `h3`.
- **`h3` is behind the `h3` feature gate.** The `wtransport` (or
quinn HTTP/3 extension) dependency is heavier than `h2`/`http/1.1`;
non-browser-facing deployments don't compile it.
- **Browsers are not alknet peers.** A browser over WebTransport
authenticates by bearer token, gets no `PeerId` (ADR-034 §4).
- **WebTransport streams target the call protocol directly.** A
WebTransport bidirectional stream carrying an `EventEnvelope` is
handed to the call protocol's `Dispatcher` — no SSE translation, no
HTTP framing. The browser speaks the call protocol wire format
directly.
- **The HTTP/3 request path uses the same axum `Router` as `h2`/
`http/1.1`.** An HTTP/3 request is just another HTTP request from
the router's perspective (ADR-036).
- **WebTransport is a draft standard.** The `wtransport` README notes
the protocol is not yet standardized; the API may change. The `h3`
feature gate isolates the risk.
## Design Decisions
| 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 |
| 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` |
| WebTransport streams → call protocol directly | [ADR-012](../../decisions/012-call-protocol-stream-model.md) | Stream-agnostic; WebTransport stream = QUIC bidirectional stream |
| Stealth on h3 | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Unknown paths get the decoy |
| HTTP path = operation path (for HTTP/3 requests) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | Same axum `Router` as h2/http1.1 |
## Open Questions
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-38** (open, scope): WebTransport relay-as-proxy — a proxy that
terminates the browser's WebTransport connection and forwards to a
P2P hub's Ed25519 endpoint (so the hub need not expose a public
X.509 cert). Recorded in ADR-034 §5. Does the proxy live in
`alknet-http` or a separate relay crate? This is a genuine scope
question (the proxy use case is not yet concrete enough to decide the
crate boundary), not a deferral.
## References
- [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md)
— the decision that `h3` is in scope
- [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) —
the HTTP-to-call mapping (the HTTP/3 request path uses the same
axum `Router`)
- [overview.md](overview.md) — crate overview, feature gates
- [http-server.md](http-server.md) — the `h2`/`http/1.1` companion
- `/workspace/wtransport/` — pure-Rust WebTransport reference
implementation (the `h3` feature's candidate dependency)