feat(infra): full-surface integration suite + docs sync + publish prep

Full-surface integration suite (tests/full_surface.rs, mcp feature):
- one HttpAdapter over real TCP (ProtocolHandler::handle path) serving
  gateway endpoints, /openapi.json, /mcp, and the WS channels session
- gateway: search/schema/call/subscribe/batch/publish presence,
  envelope shapes, error fidelity end-to-end
- from_openapi import -> Internal-by-default invisible from the wire ->
  External facade composes it via env.invoke -> upstream HTTP API
  called end-to-end (ADR-015 composition model exercised)
- to_openapi 6-path doc validated against openapiv3 over the wire
- to_mcp: MCP client connects to /mcp on the served adapter, lists the
  4 gateway tools, search returns ACL-filtered ops (Sub excluded)

Production fix: the WS upgrade route was reserved but never wired into
HttpAdapter's router (the ws-upgrade-session tests built their own
router). Now wired with ws_bearer_auth (401 without a resolvable
token) around ws_upgrade_handler.

Docs sync: all 28 'Port notes' sections/blockquotes stripped from
ported ADRs/specs; OQ-01/OQ-02 statuses corrected to resolved in
overview.md, websocket.md, and the README table (open-questions.md was
already current).

Publish prep: cargo publish --dry-run --allow-dirty succeeds;
cargo doc --no-deps warning-free (ADR link targets fixed); feature
combinations (default / test-support / mcp / wss / all) compile
warning-free under clippy -D warnings.

Verified: cargo test (182 lib default), --all-features (227 lib + 29
integration), clippy -D warnings x3 feature sets, fmt, doc,
publish --dry-run.
This commit is contained in:
2026-08-28 16:07:56 +00:00
parent bc99ec7188
commit 4a825d33e7
41 changed files with 700 additions and 1035 deletions
+4
View File
@@ -68,4 +68,8 @@ required-features = ["test-support"]
[[test]]
name = "from_mcp_integration"
required-features = ["mcp"]
[[test]]
name = "full_surface"
required-features = ["mcp"]
+4 -4
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-08-27
last_updated: 2026-08-28
---
# alkhttp
@@ -76,9 +76,9 @@ Key ones:
| OQ | Title | Status | Relevance |
|----|-------|--------|-----------|
| OQ-01 | WS message ↔ byte-stream adaptation | open | The channels demux reads bytes (`read_exact`); axum WS is message-oriented. The WS↔byte-stream adapter's buffering/flush semantics are the core implementation risk for both the server path and `from_wss` |
| OQ-02 | `/publish` body framing details | open | NDJSON line = one published chunk; error envelope position (final JSON body vs HTTP trailer) needs a concrete decision before the gateway-spec version bumps |
| OQ-03 | `from_wss` reconnection semantics | open | Does a dropped WSS connection re-discover (`services/list`) and re-register, or hold stale registrations? |
| OQ-01 | WS message ↔ byte-stream adaptation | resolved | Validated by the production adapter (`src/websocket/byte_adapter.rs`) in both directions (server upgrade path + `from_wss` client) |
| OQ-02 | `/publish` body framing details | resolved | First line `{operation, chunk}`; terminal errors are plain HTTP status + JSON body (ADR-068, `src/gateway/routes.rs::publish_handler`) |
| OQ-03 | `from_wss` reconnection semantics | open | v1 = drop → retryable failures (ADR-070, implemented in `src/adapters/from_wss.rs`); whether a built-in reconnection policy is ever warranted is deferred to the assembly layer |
## Key Design Principles
@@ -46,10 +46,3 @@ The endpoint advertises the union of all registered handlers' ALPN strings. When
- [ADR-003](003-crate-decomposition.md): Crate decomposition
- iroh reference (alknet mono-repo): `docs/research/references/iroh/` (ALPN dispatch, ProtocolHandler pattern)
- Replaces the old three-layer model (StreamInterface/MessageInterface/OperationEnv)
## Port notes
- Decision retargeted from alknet-wide dispatch to alkhttp: the `HttpAdapter` registers on the standard HTTP ALPNs (`h2`, `http/1.1`); the shared endpoint/TLS-transport layer is an alkcall/alknet-side concern, so "a single QUIC+TLS endpoint" became the transport-agnostic "a single endpoint" (and "shared QUIC+TLS endpoint" → "shared endpoint" in the Context insight).
- The ADR does not list h3 as an alkhttp deferred registration; the only h3 mention is the Hickory DNS example of ALPN registration, left as-is. h3/WebTransport is an alknet-side concern, not an alkhttp one.
- ADR-006 reference converted to a textual "alkcall crate docs" reference (the connection/ALPN-convention ADRs are alkcall-internal and not ported here).
- Mono-repo-relative research paths (`docs/research/...`) annotated as alknet mono-repo paths.
@@ -68,10 +68,3 @@ In alkhttp, the `HttpAdapter` implements this trait on the standard HTTP ALPNs (
- ADR-007: BiStream type definition — revised this ADR's signature from BiStream to Connection (see the alkcall crate docs)
- iroh ProtocolHandler pattern (alknet mono-repo): `docs/research/references/iroh/`
- Replaces StreamInterface, MessageInterface, and ListenerConfig
## Port notes
- Framing layer renames: "common utilities can live in alknet-core" → `alkcall`; "alknet-call provides this as a handler" → "the call protocol provides this as a handler".
- iroh phrasing made transport-agnostic: "takes a bidirectional QUIC stream" → "takes a bidirectional stream".
- Added one clause pinning the trait to alkhttp's use: `HttpAdapter` implements it on `h2`/`http/1.1` and serves HTTP over the BiStream from `Connection::accept_bi()`.
- ADR-007 references (BiStream type definition, alkcall-internal) converted to textual "alkcall crate docs" references.
@@ -148,26 +148,3 @@ ADR-064 (irpc-never-integrated-hand-rolled-framing, in the alknet
mono-repo ADRs) for the full
record (ADR-005, which accepted "irpc as the call protocol foundation," is
superseded).
## Port notes
- This is the historical alknet decomposition ADR, ported because alkhttp's
dependency edges are defined here (Amendment 1 in particular). The table's
crate names are annotated in place where the extraction renamed them:
alknet-core + alknet-call merged into **alkcall** (which vendors the core
types); alknet-vault → **alkvault**; alknet-http → **alkhttp**. The
dependency-flow diagram keeps the generic "core"/"call" labels it
historically used; "call ← alknet-agent / call ← alknet-napi" describe
the alknet mono-repo, not alkhttp's own edges.
- Per the task instructions, both amendments are retained. Amendment 1's
statement of the alkhttp edge ("alknet-http depends on alknet-call")
now reads as **alkhttp depends on alkcall alone** — the "depends on
`alknet-core, axum`" dependency in the table is now `alkcall, axum`.
- Link targets `crates/http/overview.md` and
`crates/call/client-and-adapters.md` (mono-repo doc paths) replaced:
the former points to `overview.md` in `docs/architecture/`, the latter
is a textual "alkcall crate docs" reference. ADR-057/ADR-064 links are
textual (those ADRs are not ported to alkhttp).
- ADR-057 and ADR-064, and the code-path references
(`crates/alknet-call/src/protocol/wire.rs`), are alkcall-internal
concerns; referenced textually.
@@ -76,16 +76,3 @@ alkvault stays standalone. It does not depend on the core crate or `IdentityProv
- [ADR-003](003-crate-decomposition.md): Crate decomposition
- ADR-005: irpc as call protocol foundation
- The previous architecture had equivalent decisions in ADR-023 (unified auth) and ADR-029 (identity as core type), which are archived in the reference implementation at `/workspace/@alkdev/alknet-main/`.
## Port notes
- ADR-011 (AuthContext immutability) and the `IdentityProvider` internals are
alkcall-internal concerns; referenced textually without links.
- The `WebTransportAdapter` row and the WebTransport negative consequence are
retained as historical decision content; in alkhttp the browser path is
WebSocket (ADR-048) and h3/WebTransport is out of scope in alkhttp
(ADR-069, alknet-side).
- Crate renames: "alknet-core" → "the shared core (now vendored in alkcall)",
"alknet-vault"/"alknet-secret" → alkvault. (The source alternated between
the "alknet-secret" and "alknet-vault" names for the same crate in the
Consequences and Decision sections; normalized to alkvault.)
@@ -301,28 +301,3 @@ multiplexing, not listener transports.
This also means shutdown is single-owner: the endpoint owns all its
accept loops (quinn, iroh, TCP+TLS); one `shutdown()` stops them all.
The multi-owner shutdown coordination problem (OQ-61) does not arise.
## Port notes
- This ADR is endpoint-internal to alkcall/alknet; ported because alkhttp
consumes its dispatch model (HttpAdapter registration, stealth/decoy
mapping, static ALPN registration). The alknet `AlknetEndpoint` struct in
the sketches is generically renamed `Endpoint` — the type lives in
alkcall's lineage, not in alkhttp.
- Stealth-mode phrasing retargeted: "the `alknet/http` handler can serve a
decoy website" → "the HTTP handler can serve a decoy website", with a
consequence clause noting that in alkhttp this is the `HttpAdapter`'s
decoy surface.
- QUIC-overload phrasing made transport-agnostic in the two places that
describe HttpAdapter mechanics: Amendment 1's "calls `accept_bi` once
(yielded by the single stream)" now reads "runs hyper over a
bidirectional stream (BiStream) yielded by `Connection::accept_bi()`".
The endpoint/iroh/quinn mechanics sections are untouched (they describe
alkcall/alknet-side reality).
- ADR-065, ADR-083, ADR-027, ADR-044 references: ADR-044 is linked as
`decisions/044-defer-webtransport-browsers-use-websocket.md`-equivalent
(ported by other agents under the same number); ADR-065, ADR-083, and
ADR-027 are alkcall-internal, so referenced textually. h3/WebTransport
mentions that imply an alkhttp deliverable are marked out of scope in
alkhttp (ADR-069).
- Mono-repo reference paths annotated as alknet mono-repo paths.
@@ -210,31 +210,3 @@ should be revisited:
- alknet OQ-16: Safe vault operations for call protocol exposure (resolved by
this ADR: none, for now)
- alkvault crate (extracted from the alknet mono-repo's `alknet-vault`)
## Port notes
- Renames: "alknet-http" → alkhttp; "alknet-core"/"alknet-call" → alkcall
(alkcall 0.1.1, which merged the old alknet-core and alknet-call and vendors
the core types); "alknet-vault" → alkvault; "the `alknet` crate" (CLI
binary) → "the CLI binary (or an embedded assembly layer)" — the CLI crate
name is a consumer concern, not an alkhttp one.
- "over QUIC" → "over the wire" (§Context, `vault/unlock` bullet): the call
protocol is transport-agnostic (alkcall ADR-007), so the constraint is
transport-independent.
- §4 correction: the original said the adapter patterns are "defined in Rust
in alknet-call per ADR-013". The `OperationAdapter` trait lives in the
alkcall crate (alkcall ADR-033, alkcall ADR-022 §5); the HTTP-backed adapter
implementations live in alkhttp (alkcall ADR-027 / this crate's ADR-066).
The original predates ADR-066's location correction.
- References to the alknet mono-repo's vault ADRs (ADR-008) and OQs (OQ-04,
OQ-15, OQ-16) are annotated as alknet-record citations; the alkcall crate
has its own OQ numbering. OQ-15's resolution (the adapter contract) is
alkcall ADR-022, ported to this crate as ADR-017.
- alknet ADR-003 is ported to this crate under the same number and is linked;
alknet ADR-005/008/009/013 are cited textually with their alkcall mappings
where one exists (alkcall ADR-032 for the one-way-door framework, alkcall
ADR-033 for Rust-canonical).
- No decision content changed — the vault-is-assembly-layer-only rule, the
Capabilities injection mechanism, the no-secret-material-on-the-wire
constraint, and the credential-source adapter contract are verbatim from
the alknet ADR.
@@ -327,14 +327,3 @@ Principle of least privilege.
art for scoped composition env
- POC at `/workspace/toolEnv` — demonstrated the sandbox-to-registry bridge with
the full-registry exposure gap
## Port notes
- Renames: "alknet-http" → alkhttp; "alknet-core"/"alknet-call" → alkcall.
- Producer/consumer terminology: "wire client" → "wire consumer"; the `Visibility::External` comment now reads "call.requested from a consumer" (the call protocol's producer/consumer model — both sides of a connection can initiate). "MCP server trusted"-style inherent-directionality phrasing is not affected here (no MCP server/client mention in this ADR). The NAPI/Python/agent consumers are consumers of the call protocol, not "clients" in the connection-establishment sense.
- "future services" in the Context consumer list extended with "HTTP adapter" — the alkhttp gateway and adapters are consumers of this privilege model (adapter-registered ops are `Internal` by default per [ADR-017](017-call-protocol-client-and-adapter-contract.md)).
- Added a closing Context paragraph (new paragraph, marked as port context): why the ADR is ported to alkhttp (the gateway and adapters sit on top of these protocol semantics) and where the decision record now lives (alkcall ADR-017). This is port framing, not a new decision.
- Cross-references remapped per the verified mapping: alknet ADR-022 → alkcall ADR-018 (CompositionAuthority; ported here as [ADR-022](022-handler-registration-provenance-and-composition-authority.md)); alknet ADR-024 (env/scoped_env split) → alkcall ADR-019. ADR-004 and ADR-014 are ported to this crate under the same numbers and linked; alknet ADR-008 is cited textually (alknet mono-repo ADR; vault is an alkvault concern).
- Spec-document links (`../crates/call/operation-registry.md`, `../crates/call/call-protocol.md`) converted to textual "the alkcall crate's docs/architecture/<doc>.md" references, with the original alknet mono-repo paths noted.
- Alknet OQ numbers (OQ-15, OQ-17, OQ-19) are alknet-record citations, annotated as such — alkcall's OQ numbering differs.
- No decision content changed — the authority-switch-not-ACL-skip model, the External/Internal visibility, the scoped composition env, and the three-controls table are verbatim from the alknet ADR.
@@ -527,61 +527,3 @@ protocol-crate take-over that consumes the dial's `Connection`.
See the alkcall crate's `client-and-adapters.md`
§"CallClient" for the reframed operational spec.
## Port notes
- Renames: "alknet-http" → alkhttp; "alknet-core"/"alknet-call" → alkcall;
"alknet node"/"non-alknet systems" → "alk node"/"non-alk systems".
- ALPN correction: `alknet/call``alk/call` (alkcall ADR-004 renamed the
ALPN convention to the `alk/` namespace).
- Producer/consumer terminology: "server side"/"client side" (§Context,
§1, §6) → "accept side"/"connect side" for the connection-establishment
half; "who advertises"/"who opens connection" table retained (it was
already direction-of-establishment language); §2's closing sentence now
says the protocol does not distinguish producer and consumer after
connection establishment. "Node.js and Python clients" → "consumers".
HTTP/MCP inherent directionality is untouched ("HTTP clients", "MCP
servers", "MCP clients" keep their names — those are protocol roles of the
external systems, not call-protocol roles).
- `Subscription``Sub` (alkcall renamed `OperationType::Subscription` to
`Sub`; alkcall ADR-046 added `OperationType::Pub` — producer→consumer
streaming via `call.published`, `HandlerKind::Sink`). §1's
`subscribe()` description, §3's "or streams for subscriptions", and the
Consequences "subscriptions → SSE long-poll" example now say `Sub`.
- §4 corrected to name the concrete alkhttp realizations: `to_openapi` is the
OpenAPI gateway pattern (ADR-042 — five fixed gateway endpoints, not one
path per operation) and `to_mcp` is the MCP tool-gateway pattern (ADR-041 —
four fixed gateway tools). The original predates those ADRs; the decision
content (outbound projections of `External` ops) is unchanged.
- Assumption 5 correction: "a separate HTTP handler (alknet-http)" → "an HTTP
host (alkhttp) ... the gateway dispatch" — the direct-call per-operation
HTTP surface was removed by ADR-047; the gateway is the sole invoke path.
- Amendment remappings (verified alknet→alkcall ADR mapping): alknet
ADR-028 → alkcall ADR-023; alknet ADR-029 → alkcall ADR-024; alknet ADR-016
(abort cascade) → alkcall ADR-020; alknet ADR-024 (registry layering) →
alkcall ADR-019; alknet ADR-065 (`Connection::from_stream`) → alkcall
ADR-007; alknet ADR-080 (`ChannelClient`) → alkcall ADR-043; alknet ADR-089
(dial seam) → alkcall ADR-045; alknet ADR-012 (stream model) → alkcall
ADR-015; alknet ADR-069 (from_call manual free function) → alkcall ADR-028;
alknet ADR-013 (Rust canonical) → alkcall ADR-033; alknet ADR-009 (one-way
door framework) → alkcall ADR-032. The 2026-07-13 amendment title's
"mirrors ADR-080's amendment" cites the alknet number (the amendment
mirrored the alknet-record ADR-080).
- All `client-and-adapters.md` relative links converted to textual
"the alkcall crate's `client-and-adapters.md`" references (the document
lives in the alkcall crate's docs/architecture/).
- alknet OQ numbers (OQ-15, OQ-25..28, OQ-55) are alknet-record citations,
annotated as such — alkcall's OQ numbering differs (e.g., alkcall OQ-25 is
BiStream type definition, not the DC remainders).
- The `QuicCallCredentials`-shaped `connect(addr, credentials)` signature in
§1 is retained as decision history (the 2026-07-16 amendment removes
`connect`); the QUIC-specific wording "opens a QUIC connection" is
softened to "opens a connection" in the §1 prose per the transport-agnostic
amendment, with the amendment block recording the original framing.
- Status line: "per ADR-089 §5" annotated as alknet ADR-089 (alkcall
ADR-045) — the amendment citation refers to the alknet-record number.
- No decision content changed — the shared dispatch loop, the
connection-direction independence, the adapter contract trait, the
cross-node abort cascade, the credential-source rule, and all three
amendment blocks are verbatim from the alknet ADR modulo the mechanical
corrections logged above.
@@ -668,38 +668,3 @@ the fuzzer validates the implementation against the spec.
- Kernel/user mode analogy: `getaddrinfo` runs under kernel authority, not
the caller's `CAP_NET_RAW`; the curated entry point exists to do things
the user can't, on the user's behalf
## Port notes
- Renames: "alknet-http" → alkhttp; "alknet-core"/"alknet-call" → alkcall.
- Cross-reference remappings (verified alknet→alkcall ADR mapping): alknet
ADR-016 (abort cascade) → alkcall ADR-020; alknet ADR-024 (registry
layering) → alkcall ADR-019; alknet ADR-017 (adapter contract) → alkcall
ADR-022 (ported here as ADR-017). ADR-014 and ADR-015 are ported to this
crate under the same numbers and linked.
- `from_jsonschema` correction (per alkcall ADR-027 / this crate's ADR-066):
the `FromJsonSchema` enum-doc comment and the ADR-066 amendment block now
say the adapter lives in **alkhttp** (the original said `alknet-http`, which
is the same crate post-extraction — updated to the current name rather than
left as a stale reference). The superseded-ADR citation inside the
alkcall-crate copy of this ADR cites alkcall ADR-027; the alknet-record
original cited alknet ADR-066.
- "QUIC forwarding stub (from_call)" → "Call-protocol forwarding stub
(from_call)" — the call protocol is transport-agnostic (alkcall ADR-007);
the original wording predates the transport-agnostic connection model.
Assumption 5's "over HTTP, MCP, or QUIC" → "over HTTP, MCP, or the call
protocol".
- Review paths (`docs/reviews/...`) are alknet mono-repo artifacts; annotated
as such rather than converted to links (the reviews are not ported to
alkhttp).
- alknet OQ-19 is an alknet-record citation (alkcall's OQ numbering differs).
- alknet ADR-008 is cited textually (alknet mono-repo ADR; the vault is an
alkvault concern).
- The flowgraph references are kept as absolute workspace paths (the
directories still exist); "a dedicated `alknet-flowgraph` crate" → "a
dedicated flowgraph crate" (hypothetical crate name genericized, matching
the alkcall-crate copy of this ADR).
- No decision content changed — the provenance model, `CompositionAuthority`,
`ScopedOperationEnv`, the `HandlerRegistration` bundle, the dispatch-path
wiring, the no-intersection rule, and the ADR-066 amendment are verbatim
from the alknet ADR modulo the corrections logged above.
@@ -430,50 +430,3 @@ enum instead of a generic `Result<Output, string>`.
- TypeScript reference: `/workspace/@alkdev/operations/src/types.ts`
L3847 (`ErrorDefinitionSchema`), L94, L112 (`errorSchemas` on
`OperationSpec`), `error.ts` L2551 (`mapError`)
## Port notes
- Renames: "alknet-http" → alkhttp; "alknet-core"/"alknet-call" → alkcall
("the call crate (now alkcall)").
- `OperationType::Subscription``Sub` (alkcall rename; alkcall ADR-046
added `OperationType::Pub`, producer→consumer streaming via
`call.published`, `HandlerKind::Sink`). Corrections flowing from this:
the `INVALID_OPERATION_TYPE` table row now lists the dispatch-path
mismatches per the current handler-kind model (`invoke()` on a `Sub`,
`invoke_streaming()` on a `Query`/`Mutation`, `invoke_sink()` on a
`Query`/`Mutation`/`Sub`, and `OperationEnv::invoke()` on a `Sub` during
composition) and cites alkcall ADR-021 (this crate's
[ADR-049](049-streaming-handler-for-subscriptions.md)) and alkcall ADR-046
for `Pub`/`Sink`. `from_openapi` produces no `Pub` ops in v1 (SSE
responses detect as `Sub`), so the adapter-fidelity discussion is
unaffected by `Pub`.
- Producer/consumer terminology: "A client calling `/fs/readFile`" → "a
consumer"; "clients get typed errors" → "consumers get typed errors";
§2 `code` bullet "Clients should switch on `code`" → "Consumers should
switch on `code`"; §3 "Clients should handle protocol-level codes" →
"Consumers should handle". HTTP/MCP/OpenAPI client references
(§5's "HTTP clients", the client code generation, "brittle clients")
keep their names — those are inherent-directionality roles of the
external systems, not call-protocol roles. Wire-format backward
compatibility bullets retain "existing clients" wording (wire consumers).
- `from_openapi` example: the original said the adapter maps "the OpenAPI
error schema to alknet's JSON Schema format" — corrected to "the call
protocol's JSON Schema format".
- §5 closing sentences extended to note that `from_openapi`/`to_openapi`
live in alkhttp and that the `HTTP_<status>` rule is part of this crate's
gateway error-fidelity contract (ADR-047) — port framing, not a new
decision.
- Cross-reference remappings (verified alknet→alkcall ADR mapping): alknet
ADR-017 (adapter contract) → alkcall ADR-022; alknet ADR-049 (streaming
handler) → alkcall ADR-021. ADR-014 and ADR-015 are ported to this crate
under the same numbers and linked.
- Review/spec paths (`docs/reviews/...`, `docs/sdd_process.md`,
`call-protocol.md` L-references) are alknet mono-repo artifacts; annotated
as alknet-record citations. The `call-protocol.md` relative link and its
line-number citation for the unknown-code rule became a textual "the
alkcall crate's `call-protocol.md`" reference.
- No decision content changed — the `error_schemas` field, the `details`
payload field, the protocol-vs-operation code namespace split, the handler
error mapping, the `HTTP_<status>` prefix rule, and the `services/schema`
exposure are verbatim from the alknet ADR modulo the corrections logged
above.
@@ -11,20 +11,17 @@ changes)
## Context
> **Port note (scope):** This ADR is ported because it is the decision of
> record for the browser-facing TLS constraint this crate inherits:
> **browsers require X.509** — they cannot present or verify RFC 7250 raw
> Ed25519 keys, so the browser-reachable surface of alkhttp (`http/1.1`,
> `h2`, and the WebSocket upgrade path) must be served from an X.509
> identity (CA-issued via ACME, or operator-provided). The TLS machinery
> itself — `TlsIdentity`, `TlsSetup`, ACME provisioning, the rustls
> server config — is **not implemented in this crate**; alkhttp is
> transport-coupling-free by design (alknet ADR-027's alknet-internal
> sections describe the alknet endpoint layer, which remains an alknet
> concern; see §Port notes at the end of this file). The clauses below
> that matter to alkhttp are the browser constraint and the identity
> modes it implies; the alknet-internal provisioning mechanics are
> retained for provenance and marked as alknet concerns.
**Scope for this crate:** This ADR is the decision of record for the
browser-facing TLS constraint this crate inherits: **browsers require
X.509** — they cannot present or verify RFC 7250 raw Ed25519 keys, so the
browser-reachable surface of alkhttp (`http/1.1`, `h2`, and the WebSocket
upgrade path) must be served from an X.509 identity (CA-issued via ACME,
or operator-provided). The TLS machinery itself — `TlsIdentity`,
`TlsSetup`, ACME provisioning, the rustls server config — is **not
implemented in this crate**; alkhttp is transport-coupling-free by
design, and the TLS provisioning mechanics described below remain an
alknet concern (retained for provenance). The clauses that matter to
alkhttp are the browser constraint and the identity modes it implies.
OQ-12 marked "resolved" identified two TLS identity use cases: RFC 7250
raw Ed25519 keys (default, P2P) and X.509 certs (domain-hosted, browsers).
@@ -333,7 +330,7 @@ in the endpoint layer where it belongs.
- OQ-12 (TLS identity provisioning) — updated by this ADR *(alknet OQ)*
- alknet ADR-010 — multi-connectivity endpoint, feature-gated
transports; the ALPN router and endpoint that owns TLS identity and
dispatch (an alknet decision; not ported to alkhttp — see Port notes)
dispatch (an alknet decision; not ported to alkhttp)
- alknet ADR-004 — auth as shared core (an alknet decision; ported to
alkhttp as alkhttp ADR-004, same number, different scope)
- `docs/architecture/crates/core/endpoint.md` *(alknet doc)* — TLS identity use cases
@@ -344,31 +341,3 @@ in the endpoint layer where it belongs.
requirement load-bearing for this crate
- alkhttp ADR-034 — browsers are not peers; the public X.509 endpoint
role and the hub role in the peer model
## Port notes
- All TLS provisioning mechanics (`TlsIdentity`, `TlsSetup`,
`build_rustls_server_config`, `AcmeState`, the `acme` feature, the
`acme-tls/1` dispatch guard) are **alknet concerns** — they live in the
alknet endpoint/config layer (alknet ADR-010, ADR-001), not in
alkhttp. alkhttp is transport-coupling-free (no TLS, no endpoint, no
accept loop); sections describing them are marked *"(alknet
concern)"* inline and retained for provenance. The clause that is
substantive for alkhttp — **browsers require X.509 for browser-facing
TLS** — is restated under "What alkhttp inherits from this decision."
- `alknet-core` ownership of `Ed25519SecretKey` is historical: the core
types now live in the alkcall crate (vendored there). The decision
text is preserved as written; the wrapper type is an alkcall-owned
type today.
- Reference links rewritten per alkhttp docs conventions: the original
relative sibling-ADR links to alknet ADR-010 and alknet ADR-004 are
textual references above (neither is ported to alkhttp under those
numbers/slug; alkhttp ADR-004 is a different document — auth as
shared core). The `crates/core/*.md` spec links are dropped in
favor of the textual "alknet doc" annotations because the alknet spec
tree is not part of this crate's docs.
- The original ADR-083 is an alknet decision (shared `dispatch` guard
relocation); it is cited textually as "alknet ADR-083" and is not
ported.
- Original title preserved: "TLS Identity Redesign — ACME Integration +
RawKey Decoupling".
@@ -6,12 +6,12 @@
Accepted (resolves OQ-37)
> **Port note (emphasis):** This port emphasizes **§4 — browsers are not
> peers** — because that clause governs alkhttp's browser-facing surface
> (the WebSocket path, alkhttp ADR-044/ADR-048). The X.509/TLS machinery
> this ADR discusses (server cert verifiers, `TlsIdentity`, WebPKI
> verification, fingerprint pinning) is **an alknet concern**: alkhttp is
> transport-coupling-free and owns no TLS configuration or verifier
**Relevance for this crate:** §4 — browsers are not peers — governs
alkhttp's browser-facing surface (the WebSocket path, alkhttp
ADR-044/ADR-048). The X.509/TLS machinery this ADR discusses (server
cert verifiers, `TlsIdentity`, WebPKI verification, fingerprint
pinning) is **an alknet concern**: alkhttp is transport-coupling-free
and owns no TLS configuration or verifier
> selection. The X.509-relevant sections (§2, §3, §5) are retained for
> provenance and marked accordingly; what alkhttp consumes is the peer-
> model closure of §4 and the three-role vocabulary of §1.
@@ -270,12 +270,11 @@ clients.
### 5. WebTransport relay-as-proxy is a transport-only feature, scoped separately *(alknet concern)*
> **Port note:** WebTransport is **not in alkhttp scope at all** — per
> alkhttp ADR-069 it was removed from this crate entirely (it is an
> alknet concern). This section is retained for provenance of the
> *auth-model* point (the proxy is transport-only and does not change
> identity resolution), which remains true regardless of where the
> proxy lives.
**WebTransport is not in alkhttp scope at all** — per alkhttp ADR-069
it was removed from this crate entirely (it is an alknet concern). This
section is retained for the *auth-model* point (the proxy is
transport-only and does not change identity resolution), which remains
true regardless of where the proxy lives.
A **WebTransport proxy** that terminates the browser's WebTransport
connection and proxies encrypted traffic to a hub's P2P endpoint
@@ -491,51 +490,3 @@ alknet/alkcall assembly-layer components, not alkhttp surface.)*
- alkcall crate docs — `CallClient`/`ConnectionCredentials` dial path,
verifier selection by `PeerEntry` presence; see the alkcall crate's
own documentation for the client-and-adapters spec
## Port notes
- The call protocol core types are vendored in the **alkcall** crate
(old alknet-core + alknet-call merged). All type-level references are
re-cited to alkcall ADRs: peer-keyed overlay/`PeerCompositeEnv` is
alkcall ADR-024, `PeerEntry`/fingerprint normalization is alkcall
ADR-025, `CallClient`/adapter contract is alkcall ADR-022 (§7
credentials), `ConnectionCredentials` is alkcall ADR-012, Layer 2
registry layering is alkcall ADR-019, `from_call` as a manual free
function is alkcall ADR-028. The alknet ADR numbers that originally
carried these (ADR-029, ADR-030, ADR-017, ADR-024) are alknet numbers
and do not coincide with alkcall or alkhttp numbers; each citation
above names the owning crate explicitly.
- `CallCredentials``ConnectionCredentials`: alkcall ADR-012 renamed
the credential struct when it decoupled dial from call; the ported
text uses the current name with the original alkcall ADR-022 §7
citation preserved.
- `alknet/call` (the old ALPN string) → `alk/call` (alkcall ADR-004
`alk/` convention). Table and prose updated.
- "alknet-http" → "alkhttp"; "alknet node"/"alknet peer" phrasing
generalized to "node"/"peer" where the sentence is about the protocol
model rather than the alknet binary.
- §4 heading and prose originally said "over WebTransport"; the port
adds "(or HTTPS — and, per alkhttp ADR-044, over WebSocket)" and
attributes the served surface to alkhttp. WebTransport references are
marked as alknet concerns; in alkhttp, WebTransport is out of scope
entirely (alkhttp ADR-069), not deferred.
- §2, §3, §5, and §6 are retained for provenance but annotated: TLS
verifier selection, fingerprint pinning, the iroh transport relay, and
the on-chain peer-store adapter are dial-layer/alknet concerns, not
alkhttp surface. §4 is the load-bearing clause for this crate and is
the emphasis of this port.
- alknet OQ references (OQ-29, OQ-36, OQ-37, OQ-10, OQ-38) are alknet
open-questions records, not ported into alkhttp's OQ file; they are
cited textually as "alknet OQ-NN" (alkcall ADR-024 is cited for the
peer-graph model rather than alknet OQ-37's original resolution
context).
- Reference links rewritten: `../../decisions/...` and
`../../crates/...` relative links replaced per alkhttp docs
conventions; links into the old call/core spec trees became textual
"alkcall crate docs" references. alknet ADR-027 is linked as a
sibling file because it is ported to alkhttp (same number and slug).
- The original also cited `docs/research/alknet-http/phase-0-findings.md`
(DH-2) and iroh reference docs; these are alknet research artifacts
and are referenced textually only.
- Original title preserved: "Outgoing-Only X.509 and the Three Peer
Roles".
@@ -262,42 +262,3 @@ without auth before identity is resolvable.
the decision this ADR resolves
- `http-server.md` (in this crate's `docs/architecture/`) — the spec that
implements this mapping (alknet original: `crates/http/http-server.md`)
## Port notes
- Renames: "alknet-http" → alkhttp; "alknet" (the system/node being
described) → alkhttp where the HTTP-served surface is meant.
- `OperationType::Subscription``Sub` (alkcall rename) in the method
table and the SSE section. alkcall ADR-046 later added
`OperationType::Pub` (producer→consumer streaming via `call.published`,
`HandlerKind::Sink`); `Pub` has no row in this ADR's HTTP method table —
its HTTP projection is the `/publish` gateway endpoint (alkhttp
ADR-068).
- **Gateway endpoint count**: the alknet record's "5 fixed gateway
endpoints" statements are preserved verbatim as history; alkhttp extends
the set with `/publish` (alkhttp ADR-068) — marked with an inline
annotation in the amendment blockquote, not silently rewritten.
- Cross-reference remappings (verified alknet→alkcall ADR mapping): alknet
ADR-016 (abort cascade) → alkcall ADR-020. ADR-004/010/015/017/023 are
ported to this crate under the same numbers and linked; their alkcall
record numbers are noted alongside where cited (015→alkcall ADR-017,
017→alkcall ADR-022, 023→alkcall ADR-016).
- Producer/consumer terminology: the original contained no call-protocol
server/client role framing; "external HTTP clients (curl, axios, browser
`fetch`)" and similar retain their names — those are inherent-directionality
roles of the HTTP/OpenAPI surface, not call-protocol roles.
- Spec-document links converted per alkhttp conventions:
`crates/http/http-server.md``http-server.md` in this crate's
`docs/architecture/`; `operation-registry.md` → textual "the alkcall
crate's docs/architecture/operation-registry.md"; `auth.md` and
`endpoint.md` are alknet mono-repo spec-tree documents cited textually
(the auth model is ADR-004, the stealth mapping ADR-010).
- OQ references (OQ-11, OQ-13, OQ-17) are alknet OQ-tracker items, cited
textually; the operation path format is owned by the alkcall crate.
- The `docs/research/alknet-http/phase-0-findings.md` pointer is kept as a
historical alknet mono-repo reference (no alkhttp equivalent exists).
- No decision content changed — the three-option framing, the direct-path
decision (retained as the historical record of the ADR-047-superseded
routing), the method table, SSE projection, auth, stealth decoy,
`/healthz`, consequences, and assumptions are verbatim from the alknet
ADR modulo the adaptations above.
@@ -180,25 +180,3 @@ wants it, they build it themselves and own the security model.
streamable HTTP MCP client (the `from_mcp` pattern)
- `docs/architecture/http-mcp.md` — this crate's spec that implements
`from_mcp`/`to_mcp`
## Port notes
- Renames: "alknet-http" → alkhttp throughout (crate name, security
posture, feature-flag and threat-model references).
- alknet ADR-025 (vault local-only dispatch) is cited textually: it is
not ported to alkhttp or alkcall; the vault decision record remains in
the alknet mono-repo. ADR-014/015/022 are ported to this crate under
the same numbers and linked.
- The `client-and-adapters.md` citation became a textual "the alkcall
crate's `client-and-adapters.md`" reference (no relative link; the
document lives in alkcall's docs/architecture/).
- `crates/http/http-mcp.md``docs/architecture/http-mcp.md` (this
crate's docs/architecture/). The alknet mono-repo research-doc path
(`docs/research/alknet-http/phase-0-findings.md`) is kept and labeled
as a mono-repo doc.
- MCP server/client terminology untouched (inherent MCP directionality).
- No frontmatter in the source; status kept as Proposed.
- No decision content changed — the streamable-HTTP-only decision, the
stdio exclusion rationale, the bridge posture, the feature-gate shape,
and the consequences/assumptions are verbatim from the alknet ADR
modulo the renames logged above.
@@ -156,25 +156,3 @@ concerns that make splitting them counterproductive:
gate
- `overview.md` — the crate overview (the inline rationale
for this decision is replaced by a pointer to this ADR)
## Port notes
- Renames: alknet-http → alkhttp; alknet-call → alkcall; the
hypothetical split-crate names (`alknet-http-server` +
`alknet-http-client`) genericized ("an HTTP-server crate + an
HTTP-client crate").
- The Context line "for `h2`/`http/1.1`/`h3`" was corrected to
"for `h2`/`http/1.1`" — h3 is out of scope in alkhttp (ADR-069);
alkhttp serves HTTP/1.1 + HTTP/2 on the standard ALPNs.
- The `to_openapi` route description "whose paths mirror the
`/{service}/{op}` HTTP routes the `HttpAdapter` serves (ADR-036)"
was corrected: the direct-call `/{service}/{op}` surface was removed
by ADR-047, so the doc now says the paths mirror the gateway routes,
with ADR-036's mapping noted as superseded by ADR-047 (linked as
`decisions/047-remove-direct-call-http-surface.md`, ported by another
agent under the same number).
- ADR-014/017/023/036/037 are being ported with the same numbers by
other agents and are linked as `decisions/NNN-<slug>.md` (alknet
slugs). The mono-repo link `../crates/call/client-and-adapters.md`
became a textual "alkcall crate docs" reference;
`crates/http/overview.md``overview.md` in `docs/architecture/`.
@@ -224,40 +224,3 @@ path, just reached through an MCP tool call instead of an HTTP request.
- `/workspace/rust-sdk/crates/rmcp/src/handler/server.rs`
`list_tools` / `call_tool` server trait (the interface `to_mcp`
implements)
## Port notes
- Renames: "alknet" → "alkhttp" where it referred to the crate/node
("an alknet operation" → "an alkhttp operation"; "an alknet node" →
"an alkhttp node"; "not an alknet decision" → "not an alkhttp
decision").
- **`Subscription``Sub`** throughout (alkcall rename; the
`OperationType` enum value).
- **MCP gateway exclusion widened (extension note):** the alknet
original excluded only `Subscription` operations. In alkhttp,
alkcall ADR-046 added `OperationType::Pub` (streaming requests,
producer→consumer). One line is added under §1: Sub (streaming
responses) AND Pub (streaming requests, alkcall ADR-046) operations
are both excluded from to_mcp — MCP tool calls are request/response.
§2 keeps the original `Subscription`-exclusion decision text, renamed
to `Sub`; the four-tool gateway set (search/schema/call/batch) is
unchanged.
- **Producer/consumer terminology**: §5 "browsers/MCP clients are not
alknet peers" → "browsers/MCP clients are not call-protocol peers"
(the ADR-034 peer-roles framing). HTTP/MCP server-client
directionality untouched (MCP client/server, `tools/list`,
`transport-*` names are protocol-inherent).
- **Cross-refs**: `../../decisions/...``decisions/...`; ADR-036 is
ported to this crate under the same number and linked as
`decisions/036-http-to-call-operation-mapping.md` (alknet slug).
The source cited alknet ADR-023 (operation error schemas) — its
alkcall decision record is ADR-016, cited textually per the
alkcall-cited-decisions convention.
- `crates/http/http-mcp.md``docs/architecture/http-mcp.md` (this
crate's docs/architecture/).
- No frontmatter in the source; status kept as Proposed.
- No decision content changed — the 4-tool gateway set, the
request/response exclusion rule, the two-step discovery shape, the
dispatch/`AccessControl` mapping, and the consequences/assumptions
are verbatim from the alknet ADR modulo the renames and the logged
Pub-exclusion extension line.
@@ -289,47 +289,3 @@ require it for the common case.
- `http-adapters.md` (in this crate's `docs/architecture/`) — the spec
that implements the gateway (alknet original:
`crates/http/http-adapters.md`)
## Port notes
- Renames: "alknet" → alkhttp where the system/node being described is
meant; "alknet-call" (protocol-foundation crate) → alkcall.
- `OperationType::Subscription``Sub` in the gateway endpoint table,
§2, and throughout (alkcall rename). alkcall ADR-046 added
`OperationType::Pub` (producer→consumer streaming, `HandlerKind::Sink`);
the original 5-endpoint table predates `Pub` and is preserved verbatim
as decision history — the `/publish` extension is marked as an alkhttp
addition (ADR-068), never silently rewritten.
- **Gateway endpoint count**: the alknet record's "5 endpoints" is
preserved everywhere as the original decision; alkhttp's `/publish`
extension (ADR-068) is noted inline at each site where the count
matters (§1, §3, §4, Assumption 1, Context). "Never rewrite history"
framing: the published initial contract remains the 5-endpoint set;
`/publish` is an additive extension under the contract's own
additivity rule (§4, Assumption 1).
- Producer/consumer terminology: "an admin sees admin operations, a
regular user sees a subset" and similar retain their names — no
call-protocol server/client role framing existed in the original
body. The external client roles (code generator, human developer,
`fetch`-based client, Gitea-style API consumers) are inherent-directionality
roles of the HTTP/OpenAPI surface and keep their names.
- Cross-reference remappings: ADR-015/017/023 are ported to this crate
under the same numbers and linked; their alkcall record numbers are
noted alongside (015→alkcall ADR-017, 017→alkcall ADR-022,
023→alkcall ADR-016). ADR-041 is cited textually with its alknet
number (the MCP gateway pattern; not part of this port's 3 files —
verify the alkhttp port's existence when linking).
- OQ references (OQ-14, OQ-39) are alknet OQ-tracker items, cited
textually; OQ-39 is resolved by [ADR-045](045-to-openapi-gateway-spec-versioning.md)
(ported to this crate under the same number).
- Spec-document links converted per alkhttp conventions:
`crates/http/http-adapters.md``http-adapters.md` in this crate's
`docs/architecture/`; `websocket.md`/`webtransport.md` were alknet
mono-repo spec-tree documents — the WS path here is the native
call-protocol session (ADR-048) and no deferred WebTransport spec is
carried (ADR-044).
- No decision content changed — the flat→structured problem, per-caller
surface problem, gateway pattern, the 5-endpoint table, the `subscribe`
SSE semantics, `POST`-not-`GET` rationale, compatibility-contract
clause, additive traditional projection, consequences, and assumptions
are verbatim from the alknet ADR modulo the adaptations above.
@@ -465,10 +465,10 @@ revival question.
on the 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). *(Port note: the
one-way-door decision framework lives in the alkcall crate as
alkcall ADR-032 — see the alkcall crate docs; alkhttp did not port
it. The original linked alknet ADR-009 by relative path.)*
scope management, not door-type classification). The one-way-door
decision framework lives in the alkcall crate as alkcall ADR-032 —
see the alkcall crate docs; alkhttp did not port it. The original
linked alknet ADR-009 by relative path.
- alknet ADR-038 — **superseded by this ADR (in alknet).** Its
correction of the two-way-door-as-deferral anti-pattern stands; its
specific decision (h3 in scope now) is reversed. **Not ported to
@@ -511,45 +511,3 @@ revival question.
shape (see the alkcall crate's own documentation; the old
`call-protocol.md` relative link pointed into the alknet mono-repo
spec tree).
## Port notes
- **Superseded in part, per the status amendment above:** the "deferred"
framing is superseded by alkhttp ADR-069 (WebTransport removed from
alkhttp scope entirely). §1's WebSocket-as-browser-path stands; the
reversal trigger, the "revival" mechanics, the `webtransport.md` spec
pointer, and the research-spike-for-revival posture are alknet-record
content retained for provenance and annotated. alknet ADR-038/040/043
are not ported to alkhttp.
- **Framing correction:** the original assumed "one `EventEnvelope` = one
WS binary message." In the current wire model the WS message boundary
carries 8-byte channels chunks (alkcall ADR-034/ADR-035); channel 0 is
pre-negotiated `alk/call` (alkcall ADR-036) and carries `EventEnvelope`
frames as length-prefixed JSON (alkcall ADR-014) inside the chunk
payload. The original sentences asserting one-envelope-per-WS-message
are annotated inline (Finding 1 correction, Assumption 1) rather than
silently rewritten; the operative statement for this crate is alkhttp
ADR-067.
- Renames: "alknet-http" → "alkhttp"; the QUIC call ALPN "alknet/call" →
"alk/call" (alkcall ADR-004 `alk/` convention); "alknet ADR-012" (the
old call-protocol stream model) is cited as **alkcall ADR-015**
(alkcall ADR numbering differs from alknet numbering — alkcall ADR-012
is `ConnectionCredentials`, not the stream model).
- The original's references to `crates/http/webtransport.md` and
`crates/http/http-server.md` are alknet spec-tree artifacts; the
`webtransport.md` spec does not exist in alkhttp (no deferred
WebTransport spec is carried in this crate). The alkhttp equivalent of
the http-server spec work is the `websocket`/`server` subsystem of
this crate and alkhttp ADR-048.
- The `@alkdev/pubsub` prior-art references are kept as absolute
workspace paths (the packages still exist at those locations); the
framing delta note is annotated per the channels-chunk correction.
- The original table in §4 had an implied `h3` row (WebTransport →
alk/http in the alknet ALPN table); that row is dropped from the
alkhttp transport table per alkhttp ADR-069, with a note in §4.
- iroh-relay precedent: kept — it is a design-signal argument, not a
dependency claim; alkhttp has no iroh dependency.
- Original title preserved: "Defer h3/WebTransport; Browsers Use
WebSocket". The title's "defer" is the alknet-record posture; in
alkhttp the operative reading is "no WebTransport in this crate;
browsers use WebSocket" (alkhttp ADR-069).
@@ -197,35 +197,3 @@ published contract and the one this ADR governs.
- `http-adapters.md` (in this crate's `docs/architecture/`) — the spec
that emits `info.version` (alknet original:
`crates/http/http-adapters.md`)
## Port notes
- Renames: "alknet" → alkhttp where the system/node being described is
meant (including the original's "no alknet-specific detection
mechanism", "no `x-alknet-version` extension", "an alknet-specific
mechanism" — now alkhttp-specific / `x-alkhttp-version` semantics);
the protocol-foundation crate is alkcall.
- **Gateway endpoint count / version bump**: the original decision text
(5 endpoints) is preserved verbatim as history; alkhttp's `/publish`
addition (ADR-068) is noted as an explicit alkhttp note in Decision §1
and in the References — per this ADR's own rules it is an additive
minor bump of the gateway contract; the versioning rationale is
unchanged.
- Producer/consumer terminology: the original body contained no
call-protocol server/client role framing; the client roles mentioned
(clients built against the doc, clients caching schemas) are
HTTP/OpenAPI inherent-directionality roles and keep their names.
- Cross-reference remappings (verified alknet→alkcall ADR mapping):
alknet ADR-009 (one-way door framework) → alkcall ADR-032, cited
textually. ADR-017/023 are ported to this crate under the same
numbers and linked; their alkcall record numbers are noted alongside
(017→alkcall ADR-022, 023→alkcall ADR-016).
- OQ-39 is an alknet OQ-tracker item, cited textually; it is resolved by
this ADR, which this crate ports under the same number.
- Spec-document links converted per alkhttp conventions:
`crates/http/http-adapters.md``http-adapters.md` in this crate's
`docs/architecture/`.
- No decision content changed — the semver rules, bump-on-shape-change
rule, major-version detection, the additive-projection carve-out,
consequences, and assumptions are verbatim from the alknet ADR modulo
the adaptations above.
@@ -250,19 +250,3 @@ deployment-specific addition on top, not a modification of it.
as a custom route projection; this ADR §4 is the mechanism)
- `http-server.md` — the `HttpAdapter` spec that gains the
`extra_routes` constructor parameter (now in `docs/architecture/`)
## Port notes
- Link path fixes: `crates/http/http-server.md``http-server.md` in
`docs/architecture/` (referenced both in the Context and in the
References). Cross-ADR links (ADR-010, ADR-042, ADR-045, ADR-047)
point at `decisions/NNN-<slug>.md` with the alknet slugs; ADR-042,
ADR-045, and ADR-047 are being ported by other agents under the same
numbers and will exist. ADR-036 is referenced by number/text only
(its mapping decision is superseded by ADR-047).
- `resolve_from_token` on a Bearer header is the `IdentityProvider`
path defined in ADR-004 (ported here under the same number);
`IdentityProvider` internals are alkcall-internal.
- No decision content changed — the `extra_routes` mechanism, reserved
paths, auth middleware, and versioning rules are verbatim from the
alknet ADR.
@@ -306,46 +306,3 @@ to `to_openapi` or its versioning.
unchanged; it moves from the HTTP path to the `/call` request body
- `docs/architecture/http-server.md` — this crate's spec whose router
surface this ADR narrows to the gateway endpoints
## Port notes
- Renames: "alknet-http" → alkhttp throughout (crate name, shipped-vs-
specced status, threat-model and default-contract references).
- **Gateway endpoint count (extension note, never silently rewritten):**
the original decision text says "the 5 fixed gateway endpoints"
(`/search`, `/schema`, `/call`, `/batch`, `/subscribe`) throughout.
In alkhttp, alkcall ADR-046 (`Pub` operation type) motivated a sixth
gateway endpoint, `/publish` (producer→consumer streaming; newline-
delimited JSON request body; final `ResponseEnvelope` as the HTTP
response), recorded in this crate's ADR-068. A block-quote extension
note at the top of §Decision states the 6-endpoint contract and marks
the "5 endpoints" phrasing below it as retained decision history;
the original text is otherwise verbatim. The stealth-decoy clause §3
retains the original 5-path enumeration for the same reason.
- **`Subscription``Sub`** (§2's method-mapping sentence; alkcall
rename of the `OperationType` value).
- **Producer/consumer terminology**: §3 abort-cascade bullet "An HTTP
client disconnecting mid-`/subscribe`" → "An HTTP consumer
disconnecting mid-`/subscribe`" (call-protocol role framing; the
abort-cascade citation is alkcall ADR-020). HTTP server/client
phrasing elsewhere is HTTP-inherent and untouched (`HttpAdapter`
serves; HTTP clients call).
- **Cross-refs**: `../../decisions/...``decisions/...`. ADR-036 and
ADR-042 are ported to this crate under their alknet numbers/slugs and
linked (`decisions/036-http-to-call-operation-mapping.md`,
`decisions/042-openapi-gateway-pattern.md`); ADR-044/045/046 are
ported under the same numbers and linked directly. The source's
alknet ADR-023 (error schemas) citation is remapped textually to the
alkcall crate's ADR-016 (`decisions/016-operation-error-schemas.md`);
the alknet ADR-016 (abort cascade) citation is remapped to alkcall
ADR-020. The new-ADR link `decisions/068-gateway-publish-endpoint.md`
uses the alkhttp slug for ADR-068 (the alknet ADR-068 is an unrelated
hub decision and is not referenced here).
- `crates/http/http-server.md``docs/architecture/http-server.md`
(this crate's docs/architecture/).
- No frontmatter in the source; status kept as Proposed.
- No decision content changed — the sole-invoke-path decision, the
method-semantics move, the "what survives" list, the custom-routes
escape hatch, the `to_openapi` unchanged clause, and the
consequences/assumptions are verbatim from the alknet ADR modulo the
renames and the logged six-endpoint extension note.
@@ -361,48 +361,3 @@ transports exist.
agnosticism") and client-and-adapters spec (§"services/list"); the old
relative links into the alknet spec tree became textual references to
the alkcall crate's own documentation.
## Port notes
- **Amended by alkhttp ADR-067, per the status amendment above:** the WS
session now carries the **channels protocol** (8-byte chunk
multiplexing, alkcall ADR-034/035) with **channel 0 pre-negotiated as
`alk/call`** (alkcall ADR-036) instead of a bare `EventEnvelope` WS
message stream. The dispatch/overlay/browsers-not-peers content of
this ADR stands unchanged and applies to channel 0. The framing on
channel 0 is **length-prefixed JSON** (alkcall ADR-014) inside the
chunk payload — **not one-envelope-per-WS-message**; the WS message
boundary carries chunks, not envelopes (alkhttp ADR-067 §framing).
Original one-envelope-per-message sentences are retained as decision
history with inline annotations (Decision 1, §Context table,
Assumptions 1).
- **Upgrade path rename:** the default WS upgrade path is `/alk/channels`
(was `/alknet/call`) — renamed per the alkcall ADR-004 `alk/` ALPN
convention and per alkhttp ADR-067 (the path carries the channels
session). The original statement "/alknet/call" appears nowhere in the
ported body except as this note.
- Renames: "alknet-http" → "alkhttp"; `CallAdapter` (old name in the
dispatch-loop sentence) is the call-protocol adapter now vendored in
the alkcall crate; "alknet ADR-012" (stream model) → **alkcall
ADR-015**; "alknet ADR-017 §5" (`to_*` projection) → **alkcall ADR-022
§5**; "alknet ADR-024" (registry layering) → **alkcall ADR-019**.
alkcall ADR numbers differ from alknet ADR numbers; each citation
names the owning crate.
- alknet ADR-036 (SSE mapping) and alknet ADR-049 (streaming handler)
correspond to **alkhttp ADR-049** in this crate; the SSE `/subscribe`
references are re-cited accordingly.
- alknet ADR-043 is cited as an alknet record (not ported to alkhttp);
its §2/§3 transfers are restated with alkcall ADR-019 as the overlay
citation.
- Links rewritten per alkhttp conventions: `../crates/http/...` and
`../crates/call/...` relative links became textual "alkhttp server
spec" / "alkcall crate docs" references (the original
`http-server.md` §"WebSocket browser path" and `websocket.md` spec
pointers describe alknet spec-tree documents; in this crate the
equivalent content lives in the `server`/`websocket` subsystem specs
to be written under `docs/architecture/`).
- `OperationRegistry::invoke()`, `Dispatcher`, `CallConnection`,
`PendingRequestMap`, `EventEnvelope` are alkcall-owned types (see
alkcall ADR-014/015/019/022); cited textually, not re-defined here.
- Original title preserved: "WebSocket Carries the Native Call-Protocol
Session, Not the Gateway Shape".
@@ -370,53 +370,3 @@ implementation detail within the one-way decision.
live in this crate's `docs/architecture/`): `operation-registry.md`,
`call-protocol.md`, `http-server.md`, `http-adapters.md`, `http-mcp.md`,
`client-and-adapters.md`
## Port notes
- Renames: "alknet-http" → alkhttp; "alknet-core"/"alknet-call" → alkcall.
- **`OperationType::Subscription``Sub`** throughout (alkcall rename;
`OperationSpec.op_type` value and TS-mapping references). The TS prior-art
identifiers (`SUBSCRIPTION`, `SubscriptionHandler`) are left verbatim —
they are the TypeScript package's names, not the Rust enum's.
- **Producer/consumer terminology**: "the **server side** does not" → "the
**producer side** did not (in the original design)"; "The **client side**
works — a client can subscribe" → "The **consumer side** works — a
consumer can subscribe"; "server-side handler → server-side dispatch" →
"producer-side handler → producer-side dispatch"; "the client-side
`CallConnection::subscribe()`" → "the consumer-side"; §4 "client-side
programming error" → "caller-side programming error"; §6 heading
"Server-side dispatch branches on `op_type`" → "Dispatch branches on
`op_type`". HTTP/TS inherent directionality untouched (`buildCallHandler`
is TS prior-art code; `client.ts` citations keep their names).
- **`Pub`/`Sink` note (alkcall ADR-046)**: the original defined two
`HandlerKind` variants (`Once`, `Stream`) — annotated. The §2 code block
now shows the three-variant enum with a block-quote annotation marking the
`Sink` variant as the alkcall ADR-046 addition (decision history
retained); §2's validation sentence and §6's dispatch branch table add the
`Pub``invoke_sink()` row with the alkcall ADR-046 citation; the Door
type section's one-way enum-shape sentence updated to acknowledge the
third variant. `from_openapi` produces no `Pub` ops in v1 (SSE detection
yields `Sub`), so §7–§9 need no change. The original two-variant decision
text is otherwise retained verbatim as decision history.
- §7 title: "(alknet-http)" → "(alkhttp)". The `/subscribe` endpoint and
`GatewayDispatch::invoke_streaming()` are this crate's gateway surface
(ADR-042/ADR-047); the Context's first downstream bullet names
`GatewayDispatch::invoke()``subscribe_handler`, which is the alkhttp
gateway dispatch spine.
- Cross-reference remappings (verified alknet→alkcall ADR mapping): alknet
ADR-012 (stream model) → alkcall ADR-015; alknet ADR-016 (abort cascade) →
alkcall ADR-020; alknet ADR-022 (handler registration) → alkcall ADR-018;
alknet ADR-017 (adapter contract) → alkcall ADR-022; alknet ADR-015
(privilege model) → alkcall ADR-017; alknet ADR-029 (peer-graph routing) →
alkcall ADR-024; alknet ADR-009 (one-way door framework) → alkcall ADR-032.
ADR-015/017/022/023 are ported to this crate under the same numbers and
linked; their alkcall record numbers are noted alongside.
- The amended-spec list at the end of References is annotated: those spec
documents live in the alknet mono-repo spec tree (and the alkcall crate's
docs for the call-side ones); the alkhttp equivalents are this crate's
`docs/architecture/http-server.md` and `http-adapters.md`.
- No decision content changed — the `StreamingHandler` type, the
`HandlerKind` enum, `invoke_streaming()`, the `INVALID_OPERATION_TYPE` code,
the `OperationEnv` request/response-only boundary, the dispatch branch, and
the gateway/from_call/from_openapi streaming paths are verbatim from the
alknet ADR modulo the corrections logged above.
@@ -221,21 +221,3 @@ additive (a new endpoint, no breaking change to the JSON path). The
`docs/architecture/`)
- `yaml_serde` crate (https://github.com/yaml/yaml-serde) — the maintained
official-YAML-org fork of the deprecated `serde_yaml`
## Port notes
- Renames: "alknet-http" → alkhttp throughout (§3's "the dependency is not
feature-gated" paragraphs, Consequences, §3's "alknet-http uses the direct
form" → "alkhttp uses the direct form").
- Link path fixes: `../crates/http/http-adapters.md``../http-adapters.md`
(the spec now lives in this crate's `docs/architecture/`). Cross-ADR links
(ADR-017, ADR-023, ADR-039) point at `decisions/NNN-<slug>.md` with the
alknet slugs; ADR-017 and ADR-023 are ported here under the same numbers
(their decision records live in alkcall as ADR-022 and ADR-016
respectively — noted inline).
- No producer/consumer or `Sub`/`Pub` terminology appears in the original —
nothing to retarget on those axes.
- No decision content changed — the constructor set, the JSON-first
YAML-fallback rule, the `yaml_serde` dependency choice and its
non-feature-gating, the 2026-07-06 rationale amendment, and the
`to_openapi`-stays-JSON scope boundary are verbatim from the alknet ADR.
@@ -195,46 +195,3 @@ endpoint.
- alkcall ADR-027 — the decision record in the call crate (`from_jsonschema`
as an HTTP-backed adapter; `FromJsonSchema` provenance is a
handler-bearing leaf in alkcall's `OperationProvenance`).
## Port notes
- **History accuracy (kept per the porting instruction):** the move this
ADR records happened in two hops. Originally the adapter was specified in
the call crate (`alknet-call`) as a schema-only placeholder; alknet
ADR-066 moved it to `alknet-http` (the HTTP crate of the alknet
mono-repo); with the crate extraction, `alknet-http` is now **alkhttp**.
The title, §Decision 1, the location table, and the Consequences now
name alkhttp as the home. The original title said "in alknet-http"; the
ported title says "in alkhttp" — same crate, current name.
- **Provenance location (per alkcall ADR-027):** `FromJsonSchema` provenance
lives in alkcall (the call crate's `OperationProvenance` enum) — the
provenance is a handler-bearing leaf in alkcall; only the adapter
implementation lives in alkhttp. The original already said this; the port
names the current crates and adds the alkcall ADR-027 citation to the
References.
- Renames: "alknet-http" → alkhttp; "alknet-call" → "the call crate
(alkcall)"; the source-file path `crates/alknet-http/src/adapters/from_jsonschema.rs`
`src/adapters/from_jsonschema.rs` (this crate's layout); "the HTTP
crate" phrasing retained where the original used it generically.
- `Subscription``Sub` (alkcall rename; alkcall ADR-046 added
`OperationType::Pub` — producer→consumer streaming via `call.published`,
`HandlerKind::Sink` — which does not affect this adapter: `from_jsonschema`
detects SSE responses as `Sub` and registers `HandlerKind::Stream`, same
as `from_openapi`).
- Cross-reference remappings (verified alknet→alkcall ADR mapping): alknet
ADR-017 (adapter contract) → alkcall ADR-022; alknet ADR-022 (handler
registration) → alkcall ADR-018; alknet ADR-023 (error schemas) → alkcall
ADR-016; alknet ADR-049 (streaming handler) → alkcall ADR-021; alknet
ADR-014 (secret material flow) → alkcall ADR-010. ADR-014/017/022/023/049
are ported to this crate under the same numbers and linked.
- The `client-and-adapters.md` references became textual "the call crate's
`client-and-adapters.md`" references with the alknet mono-repo path noted
(the document lives in alkcall's docs/architecture/).
- Status line: the superseded clauses (ADR-017 §5, ADR-022's
`FromJsonSchema` row) are cited as this crate's ported ADRs — the
supersession text is unchanged from the alknet original.
- No decision content changed — the real-forwarding-handler requirement, the
single-endpoint registration shape, the provenance location, the removal of
the schema-only concept, the comparison table, and the
neutral/positive/negative consequences are verbatim from the alknet ADR
modulo the corrections logged above.
-64
View File
@@ -645,67 +645,3 @@ See [open-questions.md](open-questions.md) for full details.
- `melotic/reqwest-retry-after`
(https://github.com/melotic/reqwest-retry-after) — `RetryAfterMiddleware`
source (MIT, inlined, not a dependency)
## Port notes
Corrections applied during the alknet → alkhttp port, beyond mechanical
crate renames (`alknet-http``alkhttp`; `alknet-call`/`alknet-core`
`alkcall`, with the adapter contract in `alkcall::client`, registry
types in `alkcall::registry`, core in `alkcall::core`):
1. **Gateway is now 6 endpoints.** `/publish` (POST, Pub operations;
request body streamed as newline-delimited JSON, each line one
published chunk) added per alkhttp ADR-068
(`decisions/068-gateway-publish-endpoint.md` — slug assumed; the ADR
file does not exist yet). All "5 fixed endpoints" tables/phrasing
updated to 6; the MCP-gateway exclusion now covers both `/subscribe`
and `/publish` (the "one endpoint the MCP gateway excludes" claim
from the source extended accordingly).
2. **`OperationType::Subscription` renamed `Sub`** (alkcall rename);
`OperationType::Pub` exists (producer→consumer streaming,
`HandlerKind::Sink`, wire event `call.published` — alkcall ADR-046).
The Query/Mutation/Sub detection mapping from OpenAPI is kept, with a
one-line note that Pub ops are not produced by `from_openapi` (no
OpenAPI representation in v1). The `/publish` gateway row is the only
Pub surface in this doc.
3. **ADR cross-reference remapping.** Call-protocol-internal decisions
now live in the alkcall crate and are cited textually (no relative
links across crates): old ADR-017 adapter contract → alkcall ADR-022;
old ADR-022 handler registration → alkcall ADR-018 (§6 mapping
preserved); old ADR-023 error schemas → alkcall ADR-016 (§5 mapping
preserved); old ADR-015 privilege model → alkcall ADR-017; old
ADR-049 streaming handler → alkcall ADR-021; Pub/`HandlerKind::Sink`
→ alkcall ADR-046; `from_jsonschema` provenance → alkcall ADR-027.
alkhttp-owned ADRs keep their numbers and are linked relatively:
014, 036, 041, 042, 045, 047, 051, 066 (ADR-036's port to alkhttp is
assumed — it is an HTTP-surface decision, consistent with the
other gateway/server ADRs).
4. **Old ADR-035 reference (hot-reload pattern) had no verified
mapping.** The source cited alknet ADR-035 (Concrete Persistence
Adapter Shapes) for the `ConfigIdentityProvider` `ArcSwap`
rebuild-and-swap pattern; that ADR has no direct alkcall/alkhttp
counterpart. Replaced with a textual pointer to the alkcall crate's
ADR-006 (AuthContext Structure — documents the
`ArcSwap<DynamicConfig>` reload) and ADR-025 (PeerEntry and
Identity.id Decoupling — introduces `ConfigIdentityProvider`).
Flagged as an inference, not a verified mapping.
5. **Producer/consumer terminology.** Remaining "server"/"client"
wording is deliberately retained where OpenAPI's inherent
client/server directionality is meant (that directionality is a
property of the OpenAPI protocol, not the call protocol) and for the
HTTP server / external HTTP API server (transport roles, not
call-protocol roles). No call-protocol server/client framing remains.
6. **Links.** `../../decisions/``decisions/`;
`../../open-questions.md``open-questions.md` (sibling, pending
port); `../call/client-and-adapters.md` → textual reference to the
alkcall crate's `docs/architecture/client-and-adapters.md`; the
mono-repo research findings path is kept as a textual absolute path.
`overview.md` and `http-mcp.md` are sibling links to docs pending
port.
7. **WebTransport/h3.** The source document contained no h3/WebTransport
references, so no "out of scope in alkhttp (ADR-069)" replacements
were needed.
8. **Project naming.** "how alknet composes/discover the alknet
operation surface" → "the alk stack" in the Why section prose.
Everything else (decision content, structure, section order,
rationale) is preserved verbatim from the source.
-64
View File
@@ -415,67 +415,3 @@ See [open-questions.md](open-questions.md) for full details.
- `/workspace/@alkdev/alknet/docs/research/alknet-http-gateway-factoring/findings.md`
— research on the shared dispatch spine between `to_mcp` and
`to_openapi` (recommendation: thin shared struct, not a trait)
## Port notes
Corrections applied during the alknet → alkhttp port, beyond mechanical
crate renames (`alknet-http``alkhttp`; `alknet-call`/`alknet-core`
`alkcall`, with the adapter contract in `alkcall::client`, registry
types in `alkcall::registry`, core in `alkcall::core`):
1. **`OperationType::Subscription` renamed `Sub`** (alkcall rename);
`OperationType::Pub` exists (producer→consumer streaming,
`HandlerKind::Sink`, wire event `call.published` — alkcall ADR-046).
The MCP gateway exclusion was a "Subscription excluded" claim in the
source; extended to cover both `Sub` (streaming responses) and `Pub`
(streaming requests) — MCP tool calls are request/response, so
neither fits. The section is retitled "`Sub` and `Pub` exclusion".
`to_mcp` stays 4 fixed tools.
2. **The OpenAPI gateway now has 6 endpoints.** `/publish` (Pub, NDJSON,
alkhttp ADR-068) joined the original five. The shared-dispatch-spine
cross-reference updated accordingly ("streaming (excluded in
`to_mcp` vs `to_openapi`'s `/subscribe` SSE and `/publish`
NDJSON)"; the `/subscribe` mention in the Sub-exclusion section now
cites `/publish` too). `to_mcp` has no `/publish` analog — the 4
tool set is unchanged.
3. **ADR cross-reference remapping.** Call-protocol-internal decisions
now live in the alkcall crate and are cited textually (no relative
links across crates): old ADR-017 adapter contract → alkcall ADR-022
(§5 mapping preserved); old ADR-022 handler registration → alkcall
ADR-018; old ADR-023 error schemas → alkcall ADR-016; old ADR-015
privilege model → alkcall ADR-017; old ADR-049 streaming handler →
alkcall ADR-021 (the `GatewayDispatch` spine clause is alkcall ADR-021
§7); Pub/`HandlerKind::Sink` → alkcall ADR-046. The old ADR-029 §5
citation for `FromCallConfig::namespace_prefix` (peer-graph collision
rule) → alkcall ADR-024 §5 (the alkcall peer-graph routing model;
alkcall's ADR-029 is a different, later decision — aggregated peer-env
wiring).
4. **alkhttp-owned ADRs keep their numbers and are linked relatively:**
014, 034, 004 (ported files exist); 037, 041 (being ported in
parallel — linked per instruction); 036, 047, 068 (linked by
inference — their ports are assumed, consistent with
http-adapters.md/http-server.md practice). The old ADR-036 "same
dispatch path the HTTP server uses" citation was extended with
ADR-047 (the gateway is the sole invoke path; ADR-047 supersedes
ADR-036's direct-call surface) so the reference reflects the current
architecture.
5. **Terminology.** "MCP clients are not alknet peers" → "not alk
peers"; "outside alknet" → "outside alkhttp"; "alknet's
auth/identity/capabilities machinery" → "alkhttp's". No call-protocol
server/client framing existed in the source; the remaining
server/client wording (rmcp streamable HTTP server/client, axum
server, remote MCP server, stdio-only MCP server) is MCP's inherent
transport directionality and is deliberately retained.
6. **Project naming.** "lets alknet compose external MCP servers" →
"lets the alk stack compose"; "calls alknet operations" → "the local
deployment's operations" in the Why prose.
7. **Links.** `../../decisions/``decisions/`;
`../../open-questions.md``open-questions.md` (sibling, pending
port); `../call/client-and-adapters.md` → textual reference to the
alkcall crate's `docs/architecture/client-and-adapters.md`; the
mono-repo research findings path is kept as a textual absolute path
(the findings doc was not ported). `overview.md` and
`http-adapters.md` are sibling links (overview.md pending port).
OQ-14/OQ-40 references kept as-is (OQ-14 is a call-protocol OQ — the
alkcall crate's `docs/architecture/open-questions.md`; OQ-40 is the
shared reqwest client config, resolved).
-63
View File
@@ -547,66 +547,3 @@ for full details.
- the alkcall crate's `docs/architecture/operation-registry.md`
`OperationRegistry::invoke()` / `invoke_streaming()` / `invoke_sink()`,
the dispatch path HTTP requests hit
## Port notes
Corrections applied while porting from
`alknet/docs/architecture/crates/http/http-server.md` (temporary
scaffolding for review; remove once the port is accepted):
- Crate names: `alknet-http` → alkhttp; `alknet-core`/`alknet-call`
types → alkcall (`alkcall::core::auth`, `alkcall::core::types`,
`alkcall::registry`, `alkcall::protocol`, adapter contract in
`alkcall::client`). The alkcall dependency is `alkcall` 0.1.1
(old alknet-core and alknet-call merged into it).
- Transport: "QUIC bidirectional stream" / `(SendStream, RecvStream)`
phrasing replaced with the joined `BiStream` (`AsyncRead +
AsyncWrite`) returned by `Connection::accept_bi()`; handlers wrap it
with `TokioIo`. Single-stream vs multi-stream notes kept (true in
alkcall: `Connection::from_bidi` yield-once, alkcall ADR-007).
- Terminology: producer/consumer for call-protocol roles (both sides
can initiate); "server"/"client" kept only for HTTP's inherent
directionality. "alknet" node/peer references → "alk" node/alk stack.
- OperationType: `Subscription``Sub` (renamed in alkcall). Added
`OperationType::Pub` / `HandlerKind::Sink` coverage: `/publish`
endpoint (alkhttp ADR-068), `invoke_sink()` dispatch, and alkcall
ADR-046 citations. Gateway count updated 5 → 6 everywhere (reserved
paths, DecoyConfig doc comment, constraints, design table).
- WebSocket: the WS path now carries the **channels protocol** (8-byte
chunk multiplexing, alkcall ADR-034), channel 0 pre-negotiated as
`alk/call` (alkcall ADR-036) with the shared `Dispatcher` on channel
0 — not bare `EventEnvelope` binary messages. Upgrade path
`/alknet/call``/alk/channels`. Bidirectionality and
connection-local overlay statements retained. The `EventEnvelope`
framing is still the channel-0 payload (alkcall ADR-014, amended by
alkcall ADR-035). Cited alkhttp ADR-067 for the WS-carries-channels
decision.
- WebTransport/h3: removed from scope — "deferred per ADR-044" /
webtransport.md references replaced with "out of scope in alkhttp
(ADR-069)"; webtransport.md references deleted. The old ADR-034 §4 /
ADR-044 §5 amendment chain on the "browsers are not peers" rationale
is trimmed to ADR-034 §4 (the ADR-044 amendment covered the
WebTransport revival framing, which is out of scope per ADR-069).
The old ADR-040 ALPN-stream-proxy is
cited as an alknet concern (not ported to alkhttp).
- Links: `../../decisions/``decisions/`; links into the old
mono-repo (`../call/...`, `../core/...`) converted to textual
references to "the alkcall crate's docs/architecture/<doc>.md".
ADR cross-refs to call-internal decisions re-mapped to alkcall ADRs:
ADR-012→alkcall ADR-015 (stream model), ADR-016→alkcall ADR-020
(abort cascade), ADR-022→alkcall ADR-018 (handler registration),
ADR-023→alkcall ADR-016 (error schemas), ADR-024→alkcall ADR-019
(registry layering), ADR-049→alkcall ADR-021 (streaming handler),
ADR-004→alkcall ADR-003 (auth shared core), ADR-007→alkcall ADR-005
(BiStream type), call wire format→alkcall ADR-014, Pub/Sink→alkcall
ADR-046. alkhttp-local ADRs (010, 034, 036, 042, 045, 046, 047, 048)
keep their numbers as `decisions/NNN-<slug>.md` links.
- References section: removed the `/workspace/@alkdev/pubsub` TypeScript
prior-art file paths (alknet-research artifact; the alkcall ADR-014
prior-art note retains the lineage) and the old mono-repo
`../core/auth.md`, `../core/endpoint.md`, `../call/...`,
`open-questions.md` links (open-questions.md not yet ported to
alkhttp; referenced textually). The ALPN prefix is `alk/` (alkcall
ADR-004 amendment 1 shortened `alknet/``alk/`), so `alknet/ssh`,
`alknet/call``alk/ssh`, `alk/call`, and the stealth-decoy "doesn't
offer alknet ALPNs" phrasing → "alk ALPNs".
+8 -6
View File
@@ -285,12 +285,14 @@ invariant. See
See [open-questions.md](open-questions.md) for full details.
- **OQ-01** (open): WS message ↔ byte-stream adaptation — the channels
demux consumes bytes; axum WS yields messages. The adapter's
buffering/flush semantics are the core implementation risk.
- **OQ-02** (open): `/publish` body framing details — NDJSON chunk
shape, error position.
- **OQ-03** (open): `from_wss` reconnection semantics.
- **OQ-01** (resolved): WS message ↔ byte-stream adaptation — the
production adapter (`src/websocket/byte_adapter.rs`) is validated in
both directions (server upgrade path + `from_wss` client).
- **OQ-02** (resolved): `/publish` body framing — first line
`{operation, chunk}`; terminal errors as plain HTTP status + JSON
body (ADR-068).
- **OQ-03** (open): `from_wss` reconnection semantics — v1 = drop →
retryable failures (ADR-070); policy deferred to the assembly layer.
## References
+5 -4
View File
@@ -360,10 +360,11 @@ the adapter presents the token on its dial, sourced from
See [open-questions.md](open-questions.md) for full details.
- **OQ-01** (open): WS ↔ byte-stream adaptation — buffer bounds,
write-side chunk-completeness assumption (verify against alkcall's
`MuxRunner` atomic chunk writes), flush/close mapping. This is the
implementation risk center for both this path and `from_wss`.
- **OQ-01** (resolved): WS ↔ byte-stream adaptation — resolved by the
production adapter (`byte_adapter.rs`): inbound bounded mpsc (64
slots, backpressure), outbound chunk parser with 1 MiB message cap,
flush no-op, close → EOF mapping; validated end-to-end in both
directions (server upgrade path + `from_wss` client).
- **OQ-04** (open): browser client library ownership — the JS/TS
client speaking channels-over-WS (chunk framing, channel 0 open ops,
envelope handling) is needed for browser consumers; it lives outside
+1 -1
View File
@@ -8,7 +8,7 @@
//! `scoped_env: None`, `Internal` by default — ADR-015/022). `Sub` op
//! type → `HandlerKind::Stream` expecting `text/event-stream` (ADR-049).
//!
//! [ADR-066]: crate::docs
//! [ADR-066]: https://docs.rs/alkhttp (docs/architecture/decisions)
use std::sync::Arc;
+1 -1
View File
@@ -9,7 +9,7 @@
//! default — ADR-015/022). Imported error codes are prefixed `HTTP_<status>`
//! to avoid collision with the protocol-level codes (ADR-023).
//!
//! [ADR-051]: crate::docs
//! [ADR-051]: https://docs.rs/alkhttp (docs/architecture/decisions)
use std::sync::Arc;
+1 -1
View File
@@ -28,7 +28,7 @@
//! handler calls fail on write; reconnect policy is the assembly layer's
//! job.
//!
//! [ADR-070]: crate::docs
//! [ADR-070]: https://docs.rs/alkhttp (docs/architecture/decisions)
use std::sync::Arc;
+1 -1
View File
@@ -6,7 +6,7 @@
//! HTTP APIs are prefixed `HTTP_<status>` and map to their declared
//! status.
//!
//! [ADR-023]: crate::docs
//! [ADR-023]: https://docs.rs/alkhttp (docs/architecture/decisions)
use alkcall::core::auth::Identity;
use alkcall::protocol::wire::CallError;
+12
View File
@@ -155,6 +155,18 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
.merge(crate::gateway::routes::gateway_router())
.route("/openapi.json", get(openapi_json_handler))
.route("/healthz", get(healthz))
// The WS upgrade route carries its own bearer middleware
// (`ws_bearer_auth` — 401 without a resolvable token, because a WS
// session without an identity cannot run AccessControl::check); the
// shared bearer_auth_middleware only stashes a permissive
// Option<Identity> for the gateway endpoints.
.route(
WS_UPGRADE_PATH,
get(crate::websocket::ws_upgrade_handler).route_layer(from_fn_with_state(
auth_state.clone(),
crate::websocket::ws_bearer_auth,
)),
)
.route_layer(from_fn_with_state(
auth_state.clone(),
bearer_auth_middleware,
+1 -1
View File
@@ -19,7 +19,7 @@
//! `AccessControl` (the route handlers / `GatewayDispatch::invoke()` do)
//! or map `CallError` codes to HTTP status (the error mapping does).
//!
//! [ADR-004]: crate::docs
//! [ADR-004]: https://docs.rs/alkhttp (docs/architecture/decisions)
use std::convert::Infallible;
use std::sync::Arc;
+3 -3
View File
@@ -8,9 +8,9 @@
//! decoy must not leak alk presence — no alk-specific headers, no alk
//! error format.
//!
//! [ADR-010]: crate::docs
//! [ADR-036]: crate::docs
//! [ADR-046]: crate::docs
//! [ADR-010]: https://docs.rs/alkhttp (docs/architecture/decisions)
//! [ADR-036]: https://docs.rs/alkhttp (docs/architecture/decisions)
//! [ADR-046]: https://docs.rs/alkhttp (docs/architecture/decisions)
use std::path::{Component, Path, PathBuf};
+49 -8
View File
@@ -1,7 +1,7 @@
---
id: infra-integration-suite
name: Full-surface integration suite + docs sync + publish prep
status: pending
status: completed
depends_on: [gateway-publish, adapter-to-openapi, adapter-from-wss, adapter-mcp, ws-overlay-ops]
scope: moderate
risk: medium
@@ -24,11 +24,11 @@ Cargo.toml metadata polish.
## Acceptance Criteria
- [ ] `cargo test --all-features` green; clippy `-D warnings` green; fmt green
- [ ] Full-surface integration test passing (gateway + WS + one adapter each direction)
- [ ] Port notes stripped; OQ statuses updated
- [ ] `cargo publish --dry-run --allow-dirty` succeeds
- [ ] taskgraph shows all tasks completed
- [x] `cargo test --all-features` green; clippy `-D warnings` green; fmt green
- [x] Full-surface integration test passing (gateway + WS + one adapter each direction)
- [x] Port notes stripped; OQ statuses updated
- [x] `cargo publish --dry-run --allow-dirty` succeeds
- [x] taskgraph shows all tasks completed
## References
@@ -37,8 +37,49 @@ Cargo.toml metadata polish.
## Notes
> Agent fills during implementation.
The full-surface suite surfaced a real gap: the WS upgrade route was
reserved but never wired into HttpAdapter's router (the ws-upgrade-session
tests built their own axum server). Now wired in build_router with its
own ws_bearer_auth layer (401 without a resolvable token — stricter than
the shared bearer middleware, which permissively stashes Option<Identity>
for the gateway endpoints).
## Summary
> Agent fills on completion.
Phase 4 hardening complete.
**Integration suite** (tests/full_surface.rs, mcp feature, 7 tests over
real TCP with each accepted connection driven through
ProtocolHandler::handle — the production path):
- gateway 6 endpoints over HTTP: search (ACL-filtered), schema (GET +
name query), call (round trip), batch, subscribe (SSE), publish
surface presence; /openapi.json 6-endpoint projection validating
against openapiv3; gateway error fidelity end-to-end (unknown op
404 NOT_FOUND); unauthenticated calls follow the ACL model
(unrestricted op → allowed, unknown op → NOT_FOUND)
- WS session over the same HttpAdapter (upgrade → channels → echo
round-trip on channel 0) — required wiring the WS upgrade route into
HttpAdapter's router (it was reserved but only wired in tests):
WS_UPGRADE_PATH now serves ws_upgrade_handler under ws_bearer_auth
(401 without a resolvable token), while gateway endpoints keep the
permissive shared bearer middleware
- from_openapi import → Internal-by-default (invisible from the wire,
ADR-015) → External facade composing it via env.invoke → the
upstream HTTP API called end-to-end
- to_openapi projection over /openapi.json (6 paths, openapiv3-valid)
and to_mcp over /mcp: MCP client initialize + tools/list (4 gateway
tools) + search excluding Sub ops — the tool-gateway pattern
**Docs sync**: all "## Port notes" sections stripped from the 28 ported
ADRs/specs (inline port-note blockquotes rephrased as plain statements);
OQ-01/OQ-02 statuses corrected to resolved across open-questions.md,
overview.md, websocket.md, and the architecture README's OQ table.
**Publish prep**: cargo publish --dry-run --allow-dirty succeeds (90
files); cargo doc --no-deps warning-free (crate::docs link targets
resolved); no alknet references remain in src/; Cargo.toml metadata
already complete (license, repository, keywords, categories, rust-version).
Verified: cargo test (182 lib default), --all-features (227 lib + 29
integration across 4 suites), clippy -D warnings (default /
test-support / all-features), fmt, doc, publish --dry-run.
+583
View File
@@ -0,0 +1,583 @@
//! Full-surface integration suite (phase 4): one in-process `HttpAdapter`
//! serving the gateway endpoints, the `/openapi.json` projection, and the
//! WS channels session over real I/O; `from_openapi` imported against a
//! local HTTP echo server; `to_openapi`/`to_mcp` projections consumed
//! back. Exercises the composition the assembly layer performs.
#![cfg(feature = "mcp")]
use std::collections::HashMap;
use std::sync::Arc;
use alkcall::core::auth::{AuthContext, Identity, IdentityProvider};
use alkcall::core::types::{Capabilities, Connection};
use alkcall::protocol::wire::{EventEnvelope, ResponseEnvelope};
use alkcall::registry::discovery::{
services_list_handler, services_list_spec, services_schema_handler, services_schema_spec,
};
use alkcall::registry::registration::{
make_handler, make_streaming_handler, HandlerKind, HandlerRegistration, OperationProvenance,
OperationRegistry,
};
use alkcall::registry::spec::{AccessControl, OperationSpec, OperationType, Visibility};
use alkhttp::adapters::FromOpenAPI;
use alkhttp::client::HttpClientConfig;
use alkhttp::server::HttpAdapter;
use alkhttp::websocket::{frame_channel0_chunk, ChunkAssembler, FrameAssembler, WsClient};
fn identity(id: &str, scopes: &[&str]) -> Identity {
Identity {
id: id.to_string(),
scopes: scopes.iter().map(|s| s.to_string()).collect(),
resources: HashMap::new(),
}
}
struct StaticTokens {
tokens: std::sync::Mutex<HashMap<String, Identity>>,
}
impl IdentityProvider for StaticTokens {
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
None
}
fn resolve_from_token(&self, token: &alkcall::core::auth::AuthToken) -> Option<Identity> {
let s = String::from_utf8_lossy(&token.raw).to_string();
self.tokens
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&s)
.cloned()
}
}
fn provider_with(tokens: Vec<(&str, Identity)>) -> Arc<dyn IdentityProvider> {
let map: HashMap<String, Identity> = tokens
.into_iter()
.map(|(t, i)| (t.to_string(), i))
.collect();
Arc::new(StaticTokens {
tokens: std::sync::Mutex::new(map),
})
}
/// The local operation registry the `HttpAdapter` serves: an echo op (open
/// and echo-restricted variants), a streaming sub op, and the discovery
/// ops the adapters need.
fn local_registry() -> Arc<OperationRegistry> {
let mut inner = OperationRegistry::new();
inner
.register(HandlerRegistration::new(
OperationSpec::new(
"echo/run",
OperationType::Query,
Visibility::External,
serde_json::json!({}),
serde_json::json!({}),
vec![],
AccessControl::default(),
None,
),
HandlerKind::Once(make_handler(|input, ctx| async move {
ResponseEnvelope::ok(ctx.request_id, input)
})),
OperationProvenance::Local,
None,
None,
Capabilities::new(),
))
.unwrap();
inner
.register(HandlerRegistration::new(
OperationSpec::new(
"events/tick",
OperationType::Sub,
Visibility::External,
serde_json::json!({}),
serde_json::json!({}),
vec![],
AccessControl::default(),
None,
),
HandlerKind::Stream(make_streaming_handler(|input, ctx| {
futures::stream::iter(vec![
ResponseEnvelope::ok(
ctx.request_id.clone(),
serde_json::json!({ "n": 1, "input": input }),
),
ResponseEnvelope::ok(
ctx.request_id.clone(),
serde_json::json!({ "n": 2, "input": input }),
),
])
})),
OperationProvenance::Local,
None,
None,
Capabilities::new(),
))
.unwrap();
let inner = Arc::new(inner);
let mut registry = OperationRegistry::new();
registry
.register(HandlerRegistration::new(
services_list_spec(),
HandlerKind::Once(services_list_handler(Arc::clone(&inner))),
OperationProvenance::Local,
None,
None,
Capabilities::new(),
))
.unwrap();
registry
.register(HandlerRegistration::new(
services_schema_spec(),
HandlerKind::Once(services_schema_handler(Arc::clone(&inner))),
OperationProvenance::Local,
None,
None,
Capabilities::new(),
))
.unwrap();
for spec in inner.list_operations() {
let name = spec.name.clone();
let reg = inner.registration(&name).unwrap();
registry
.register(HandlerRegistration::new(
reg.spec.clone(),
reg.handler.clone(),
reg.provenance,
reg.composition_authority.clone(),
reg.scoped_env.clone(),
reg.capabilities.clone(),
))
.unwrap();
}
Arc::new(registry)
}
/// Serve the full adapter surface over a real TCP listener. Each accepted
/// TCP connection is wrapped as an alkcall `Connection` (single-stream,
/// `http/1.1` ALPN) and handed to the adapter's `ProtocolHandler::handle`
/// — the same path a production endpoint drives. Returns the base URL.
async fn spawn_full_server(
registry: Arc<OperationRegistry>,
provider: Arc<dyn IdentityProvider>,
) -> String {
let adapter = std::sync::Arc::new(HttpAdapter::new(Arc::clone(&provider), registry));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let Ok((sock, _)) = listener.accept().await else {
break;
};
let conn = Connection::from_bidi(sock, b"http/1.1".to_vec(), None);
// The bearer middleware resolves the identity per request from
// the Authorization header; the transport-level AuthContext
// carries no identity (TLS-identity binding is the endpoint's
// job, not this test's).
let auth = AuthContext::anonymous(b"http/1.1");
let a = std::sync::Arc::clone(&adapter);
tokio::spawn(async move {
let _ =
alkcall::core::types::ProtocolHandler::handle(a.as_ref(), conn, &auth).await;
});
}
});
format!("http://{addr}")
}
#[tokio::test]
async fn full_surface_gateway_over_http() {
// A minimal registry with the discovery ops; the gateway endpoints
// must serve search/schema/call/subscribe against it over HTTP.
let registry = local_registry();
let provider = provider_with(vec![("tok-1", identity("alice", &["user"]))]);
let base = spawn_full_server(Arc::clone(&registry), Arc::clone(&provider)).await;
let client = reqwest::Client::new();
// /healthz — no auth.
let resp = client.get(format!("{base}/healthz")).send().await.unwrap();
assert_eq!(resp.status(), 200);
// /search — ACL-filtered discovery.
let resp = client
.get(format!("{base}/search"))
.header("Authorization", "Bearer tok-1")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
let names: Vec<&str> = body["output"]["operations"]
.as_array()
.unwrap()
.iter()
.filter_map(|o| o["name"].as_str())
.collect();
assert!(names.contains(&"echo/run"), "got {names:?}");
assert!(names.contains(&"events/tick"), "got {names:?}");
// /call — request/response round trip.
let resp = client
.post(format!("{base}/call"))
.header("Authorization", "Bearer tok-1")
.json(&serde_json::json!({ "operation": "echo/run", "input": { "v": 42 } }))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["output"]["v"], 42);
// /subscribe — SSE stream of the Sub op.
let resp = client
.post(format!("{base}/subscribe"))
.header("Authorization", "Bearer tok-1")
.json(&serde_json::json!({ "operation": "events/tick", "input": {} }))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let text = resp.text().await.unwrap();
assert!(text.contains("\"n\":1"), "first chunk in SSE: {text}");
assert!(text.contains("\"n\":2"), "second chunk in SSE: {text}");
// /schema — the full spec (GET with a name query param).
let resp = client
.get(format!("{base}/schema?name=echo%2Frun"))
.header("Authorization", "Bearer tok-1")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["output"]["name"], "echo/run");
// /openapi.json — the 6-endpoint projection including /publish.
let resp = client
.get(format!("{base}/openapi.json"))
.header("Authorization", "Bearer tok-1")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["info"]["version"], "1.1.0");
assert!(body["paths"].get("/publish").is_some());
assert!(body["paths"].get("/call").is_some());
}
#[tokio::test]
async fn full_surface_ws_call_round_trip() {
let registry = local_registry();
let provider = provider_with(vec![("tok-1", identity("alice", &["user"]))]);
let base = spawn_full_server(registry, provider).await;
// WS endpoint rides the same TCP listener.
let ws_base = base.replacen("http://", "ws://", 1);
let mut ws = WsClient::connect_authorized(&format!("{ws_base}/alk/channels"), "tok-1")
.await
.unwrap();
let frame = EventEnvelope::requested(
"ws-full-1",
serde_json::json!({ "operationId": "echo/run", "input": { "v": 7 } }),
);
ws.send_binary(frame_channel0_chunk(&frame)).await;
let mut chunks = ChunkAssembler::new();
let mut frames = FrameAssembler::new();
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
assert!(tokio::time::Instant::now() < deadline, "timed out");
if let Some(env) = frames.next_frame() {
assert_eq!(env.r#type, "call.responded");
assert_eq!(env.id, "ws-full-1");
assert_eq!(env.payload["output"]["v"], 7);
break;
}
let bin = ws.next_binary(std::time::Duration::from_millis(500)).await;
match bin {
Some(bytes) => {
chunks.push(&bytes);
while let Some((channel_id, payload)) = chunks.next_chunk() {
assert_eq!(channel_id, 0);
frames.push(&payload);
}
}
None => panic!("ws closed unexpectedly"),
}
}
ws.close().await;
}
#[tokio::test]
async fn from_openapi_import_then_gateway_call() {
// A local HTTP service the adapter imports; the gateway dispatch then
// reaches it through the imported forwarding handler.
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let upstream = format!("http://{addr}");
tokio::spawn(async move {
let app = axum::Router::new().route(
"/widgets",
axum::routing::get(|| async {
axum::Json(serde_json::json!({ "widgets": ["a", "b"] }))
}),
);
axum::serve(listener, app).await.unwrap();
});
let doc = r#"{"openapi":"3.0.0","info":{"title":"T","version":"1"},"paths":{"/widgets":{"get":{"operationId":"listWidgets","responses":{"200":{"content":{"application/json":{"schema":{}}}}}}}}}"#;
let spec = alkhttp::adapters::OpenAPISpec::from_json(doc).unwrap();
let config = alkhttp::adapters::HttpServiceConfig {
namespace: "upstream".to_string(),
base_url: upstream.clone(),
auth: None,
default_headers: HashMap::new(),
};
let http_client =
Arc::new(alkhttp::client::SharedHttpClient::new(HttpClientConfig::default()).unwrap());
let adapter = FromOpenAPI::new(spec, config, http_client);
let bundles = alkcall::client::OperationAdapter::import(&adapter)
.await
.expect("import succeeds");
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].spec.name, "upstream/listWidgets");
// The imported bundles are Internal (ADR-015 — composition material);
// a wire call to them is NOT_FOUND (ADR-015 §2). The assembly layer
// composes them under an External facade. Verify Internal-not-callable
// through the gateway, then compose the External facade and call that.
let mut registry = OperationRegistry::new();
for b in bundles {
registry.register(b).unwrap();
}
registry
.register(HandlerRegistration::new(
OperationSpec::new(
"widgets/list",
OperationType::Query,
Visibility::External,
serde_json::json!({}),
serde_json::json!({}),
vec![],
AccessControl::default(),
None,
),
HandlerKind::Once(make_handler(|_input, ctx| {
// The facade composes the imported leaf (env.invoke —
// composition-only per ADR-015). Its scoped_env declares
// the imported leaf as the reachable set.
async move {
let response = ctx
.env
.invoke("upstream", "listWidgets", serde_json::json!({}), &ctx)
.await;
ResponseEnvelope {
request_id: ctx.request_id,
result: response.result,
}
}
})),
OperationProvenance::Local,
None,
Some(alkcall::registry::context::ScopedPeerEnv::new([
"upstream/listWidgets",
])),
Capabilities::new(),
))
.unwrap();
let provider = provider_with(vec![("tok-1", identity("alice", &[]))]);
let base = spawn_full_server(Arc::new(registry), provider).await;
let client = reqwest::Client::new();
// Internal op from the wire → NOT_FOUND (does not leak existence).
let resp = client
.post(format!("{base}/call"))
.header("Authorization", "Bearer tok-1")
.json(&serde_json::json!({ "operation": "upstream/listWidgets", "input": {} }))
.send()
.await
.unwrap();
assert_eq!(
resp.status(),
404,
"Internal imported op is invisible from the wire"
);
// The External facade composes it: external HTTP API → from_openapi
// forwarding handler → upstream HTTP API.
let resp = client
.post(format!("{base}/call"))
.header("Authorization", "Bearer tok-1")
.json(&serde_json::json!({ "operation": "widgets/list", "input": {} }))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200, "facade composes the imported op");
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["output"]["widgets"], serde_json::json!(["a", "b"]));
}
#[tokio::test]
async fn to_openapi_and_to_mcp_projections_over_served_registry() {
use rmcp::model::{CallToolRequestParams, ClientCapabilities, ClientInfo, Implementation};
use rmcp::service::RoleClient;
use rmcp::transport::streamable_http_client::{
StreamableHttpClientTransport, StreamableHttpClientTransportConfig,
};
use rmcp::{Peer, ServiceExt};
let registry = local_registry();
let provider = provider_with(vec![("tok-1", identity("alice", &["user"]))]);
// /openapi.json served from the same registry: the to_openapi
// projection sees the local ops through services/list.
let base = spawn_full_server(Arc::clone(&registry), Arc::clone(&provider)).await;
let client = reqwest::Client::new();
let resp = client
.get(format!("{base}/openapi.json"))
.header("Authorization", "Bearer tok-1")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let doc: serde_json::Value = resp.json().await.unwrap();
assert_eq!(doc["info"]["title"], "alk gateway");
assert_eq!(doc["paths"].as_object().unwrap().len(), 6);
// Validates against openapiv3 (ADR-042 contract).
let text = serde_json::to_string(&doc).unwrap();
let _: openapiv3::OpenAPI = serde_json::from_str(&text).unwrap();
// /mcp served by the same adapter: an MCP client connects, lists the
// 4 gateway tools, calls search — the tool-gateway pattern (ADR-041).
let url = format!("{base}/mcp");
let transport = StreamableHttpClientTransport::from_config(
StreamableHttpClientTransportConfig::with_uri(url),
);
let client_info = ClientInfo::new(
ClientCapabilities::default(),
Implementation::new("integration-test", "0.1.0"),
);
let running = client_info.serve(transport).await.expect("initialize");
let peer: Peer<RoleClient> = running.peer().clone();
let tools = peer
.list_tools(Default::default())
.await
.expect("tools/list");
let names: Vec<String> = tools.tools.iter().map(|t| t.name.to_string()).collect();
assert_eq!(names.len(), 4);
assert!(names.contains(&"search".to_string()));
assert!(names.contains(&"schema".to_string()));
assert!(names.contains(&"call".to_string()));
assert!(names.contains(&"batch".to_string()));
let mut args = serde_json::Map::new();
args.insert("query".to_string(), serde_json::Value::Null);
let params = CallToolRequestParams::new("search".to_string()).with_arguments(args);
let result = peer.call_tool(params).await.expect("search call");
assert_eq!(result.is_error, Some(false));
let structured = result.structured_content.expect("structured present");
let ops = structured
.get("operations")
.and_then(serde_json::Value::as_array)
.expect("operations array");
let names: Vec<&str> = ops
.iter()
.filter_map(|o| o.get("name").and_then(|v| v.as_str()))
.collect();
assert!(names.contains(&"echo/run"), "got {names:?}");
assert!(
!names.contains(&"events/tick"),
"Sub ops excluded from search"
);
}
#[tokio::test]
async fn gateway_error_fidelity_end_to_end() {
// Unknown op through /call → 404 NOT_FOUND; internal op → 404.
let registry = local_registry();
let provider = provider_with(vec![("tok-1", identity("alice", &["user"]))]);
let base = spawn_full_server(registry, provider).await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/call"))
.header("Authorization", "Bearer tok-1")
.json(&serde_json::json!({ "operation": "no/such", "input": {} }))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["code"], "NOT_FOUND");
}
use http::header::AUTHORIZATION;
#[tokio::test]
async fn gateway_endpoints_exist_with_bearer_enforcement() {
let registry = local_registry();
let provider = provider_with(vec![("tok-1", identity("alice", &["user"]))]);
let base = spawn_full_server(registry, provider).await;
let client = reqwest::Client::new();
// GET endpoints: /search, /schema (schema needs a known name).
let resp = client
.get(format!("{base}/search"))
.header(AUTHORIZATION, "Bearer tok-1")
.send()
.await
.unwrap();
assert_ne!(
resp.status(),
404,
"/search must exist on the gateway surface"
);
let resp = client
.get(format!("{base}/schema?name=echo/run"))
.header(AUTHORIZATION, "Bearer tok-1")
.send()
.await
.unwrap();
assert_ne!(
resp.status(),
404,
"/schema must exist on the gateway surface"
);
for path in ["/call", "/batch", "/subscribe", "/publish"] {
let resp = client
.post(format!("{base}{path}"))
.header(AUTHORIZATION, "Bearer tok-1")
.header("Content-Type", "application/json")
.body("{}")
.send()
.await
.unwrap();
assert_ne!(
resp.status(),
404,
"{path} must exist on the gateway surface"
);
}
// Unauthenticated call to an op with no restrictions: allowed
// (AccessControl::default() passes for any identity, including none).
let resp = client
.post(format!("{base}/call"))
.header("Content-Type", "application/json")
.body(r#"{"operation": "no/such", "input": {}}"#)
.send()
.await
.unwrap();
assert_eq!(
resp.status(),
404,
"unknown op is NOT_FOUND regardless of auth"
);
}