- OQ-05: deferred → resolved (2026-09-04, review 006 Unit 2+3); the
consumer-set reframe recorded (WS is also the native-client fallback
behind hostile NAT/firewall; OQ-04 does not block the wiring).
- ADR-067: status amendment + the v1-cut blockquote gains the Wired
(2026-09-04) note — per-session-fork shape, openable surface, gates.
- ADR-048: landed-state amendment — §4's hub→browser direction has its
object (op/register → connection overlay, hub composes via the
retained Arc<CallConnection>); the op/register ACL posture (UP-02,
SRV-10 precedent) recorded.
- websocket.md: the step-7 deferral note and the §"Data channels for
browsers" status block removed (the section now documents the landed
surface: with_ws_openable_alpns, the OpenableAlpns fallback, cap
policy, discovery, gates); idle-knob deployment note for silent data
channels (semantics unchanged; the 60 s default bites more often —
set None at assembly for long-lived interactive channels).
- Review 003 status → remediated (all findings closed; log in review
006); its Unit-4 section marked landed.
- alknet-ADR-044 §5 pointer checked: not stale.
Verification: cargo test 454 / 0; --all-features 582 / 0; clippy
(both) clean; fmt clean; doc clean.
Commits the re-point edits left uncommitted in the working tree
(5b62307's follow-through; alkcall review 005 verified these lines at
that tree), updated to the post-review-005 state:
- ADR-048 reconciliation note gains the 2026-09-04 update: alkcall
review 005 remediated the landed mechanisms (serving-loop
concurrency G-01/G-02, op/register collision policy G-03, spec
round-trip G-04, bootstrap-list alignment G-05) and alkcall 0.3.0
shipped them; alkhttp now consumes 0.3. The ADR-022 collision
sub-amendment binds here: a peer-announced op never shadows the
serving side's own registrations — the WS session's op/register
handler gates on the session fork.
- OQ-05 resolution gains the same dated update and extends the
cross-references to alkcall reviews 004-005.
What remains here is still alkhttp-side wiring only (review 003
Unit 2).
Split the feature graph so consumers pulling only the import adapters
(from_openapi / from_jsonschema / from_mcp) no longer compile the axum
/ hyper server stack, and server-only deployments no longer compile
reqwest. One crate, one import path — sides cut by features, not by a
crate split.
Feature graph:
- server (default): axum host, gateway, WS upgrade, to_openapi, to_mcp
- client (default): client host, forward, from_jsonschema, from_openapi
- openapi: shared OpenAPISpec model (implied by both sides)
- mcp: from_mcp needs client, to_mcp needs server
- wss: tungstenite transport (from_wss); tungstenite half of the
shared WS↔byte-stream adapter
- h2/http1: hyper protocol features; imply server
Wire-contract neutral: gateway endpoints, ALPNs, and all public API
shapes unchanged; defaults keep both sides on.
Supporting changes:
- forward.rs drops its axum::body::Bytes type leak (bytes crate types)
- bounded_join + error-echo caps move to input_validation (usable by
both sides; openapi_spec no longer imports from forward)
- byte_adapter: axum flavor compiles under server, tungstenite under
wss; the generic pumps stay shared (WS-11)
- input_validation / openapi_spec import-only internals gated to the
side that consumes them
- http-body-util moves to dev-dependencies (was test-only)
- integration-test required-features updated for the new sides
- from_wss unit tests (axum producer harness) gated to server
Verified: cargo test (defaults, 453) and --all-features (575) pass;
lean side builds (client / server / client,mcp / client,wss /
server,wss / openapi-only) build clean with zero warnings;
clippy -D warnings clean across all feature combinations; fmt clean.
Bump the alkcall dependency to 0.2 (with the gateway feature) and
converge on the promoted shared pieces:
- The local dispatch spine (gateway/dispatch.rs, 721 lines) is deleted;
GatewayDispatch, schema_disclosure_denial, and DEFAULT_DEADLINE are
re-exported from alkcall::gateway (alkcall ADR-048). The 30 s default
deadline preserves the previous behavior exactly.
- gateway/schema_cache.rs (PublishSchemaCache) is deleted: alkcall CF-003
compiles publish_schema at registration time and exposes
OperationRegistry::publish_validator; the /publish chunk stream
resolves against it. Un-compilable schemas are now rejected at
registration, so the two end-to-end fail-closed tests were reworked
into a registration-rejection test (a stronger guarantee).
- schema_disclosure_denial consumers (to_mcp, routes) use alkcall's
promoted implementation; the alkhttp-local copy is gone (ADR-071
updated: the guard stays as defense-in-depth, the implementation no
longer forks).
- CF-001: from_wss drop monitor and the WS overlay tests use
CallError::connection_closed; the review-001-ws-eof-signal race tests
now assert retryable CONNECTION_CLOSED on both resolution paths (the
tolerated non-retryable INTERNAL write-failure outcome is gone).
- Added CHANGELOG.md (Keep a Changelog), Unreleased section records the
bump and convergence.
Verification: cargo test default 453 ok, wss 470 ok, mcp 526 ok,
all-features 575 ok; clippy -D warnings clean (default + all-features,
all-targets); fmt clean; cargo doc warning-free.
Net: -1093 lines.
Decision: advertise == enforce. The key allowlist (OAI-02) stays as the
first gate with its established unknown-key message; a compiled leaf
validator now runs second, so required/type/enum/pattern/bounds
violations surface as INVALID_INPUT 422 naming the keyword — not as
upstream round-trips.
- new src/adapters/input_validation.rs: CompiledInputSchema compiles an
op's input_schema once at import with the jsonschema crate (same
2020-12 dialect publish_schema uses) and validates peer input at call
time; the compile-time copy is hardened closed-by-default
(additionalProperties: false injected when absent) so the validator
reproduces the allowlist's unknown-key semantics; explicit
additionalProperties:true catch-all and schema values are preserved;
the original spec value is never mutated
- from_openapi/from_jsonschema import(): compile per registration,
capture the validator in the handler closure (re-import recompiles —
the closure capture is the invalidation story); a non-compilable
input schema fails import loudly (AdapterError::SchemaParse naming
the operation), matching the publish_schema fail-closed precedent
- from_openapi generated input schemas now carry
additionalProperties:false explicitly, so the /schema advert states
the enforced rule and external schema-driven validators reach the
same verdicts
- forward/forward_stream/build_request gain an
Option<&CompiledInputSchema> parameter; enforcement runs after the
allowlist
- round-trip test (review 002 Test-gap 10): the /schema-exported
input_schema is compiled with the same validator and driven against
build_request over a 10-input violation matrix — accept-sets exactly
equal in both directions; the chain-test that lets advertise/enforce
drift surface as a CI failure
- ADR-066: new decision section (advertise==enforce) with the trust-
boundary reasoning and the rejected option (b) rationale
- module + enforce_input_schema docs updated to the two-gate shape
cargo test --all-features 596 pass; clippy --all-features/-D warnings,
fmt, doc gates clean.
docs(tasks): mark review-002-fu-oai18-decision done
Internal/ACL-restricted op specs were readable through
POST /call {"operation":"services/schema","input":{"name":...}}
(the MCP call/batch tools identically): the outer-name pre-checks pass
(services/schema is External) and alkcall's services_schema_handler
projects any registered spec with no visibility/ACL check of its own
(alkcall CF-004 is the complete fix there).
- GatewayDispatch.invoke/invoke_streaming now apply the GET /schema
route's is-internal + access-control checks to the meta-op's inner
name input before dispatch (404 Internal / FORBIDDEN ACL), one
interception point covering /call, /batch, /subscribe and the MCP
call/batch tools; /publish cannot reach the Query-typed meta-op
- the visibility+ACL check is one shared fn (schema_disclosure_denial)
used by the HTTP /schema route, the dispatch guard, and the MCP
schema tool, so transports cannot drift
- when CF-004 lands, this guard remains as defense-in-depth (ADR-071)
Tests: dispatch-spine guard unit tests; /call 404 + 401/403 matrix,
/batch NOT_FOUND entry, /subscribe error event; MCP call/batch tools
via services/schema with an Internal inner name (mcp feature).
Verify: cargo test (405), --all-features (523), clippy default and
--all-features --all-targets -D warnings, fmt --check — all pass.
§5 records the post-OAI-12 from_yaml contract: duplicate keys rejected
loudly on YAML (with the empirical correction that serde_json 1.0.151's
Value path last-wins rather than errors — the YAML side is the stricter
one), non-finite floats rejected with JSON-pointer context, merge keys
applied via apply_merge (shallow, referencing keys win; the one
deliberate YAML 1.2 deviation), scalar-key stringification matching the
core schema, and the no-new-bounds note (the walk stays inside
yaml_serde's parse-time limits).
Verification: cargo doc --no-deps clean; module-doc cross-check in
openapi_spec.rs matches this contract.
Documents the CON-18 disposition in the v1 session-lifetime section
(teardown handle still future work, but a dead import no longer leaves
a permanent monitor task) and adds the consequence pair: dead imports
self-clean after EOF + bounded grace; registrations landing past the
grace window fall back to the 30s sweeper deadline.
SSE payload contract (non-JSON frames carry {data, event}; JSON frames
surface as themselves), the placeholder routing rule (placeholder keys
never double-emit as query; structural path values are INVALID_INPUT),
and the literal-percent trade-off (% in values always encoded; % in
assembly-supplied template text survives — the assembly owns the
upstream-semantics choice, per the ADR-066 trust boundary).
Record the v1 cut for browser-opened data channels over WS (review-001
WS-03): the design (ADR-067) stands; only the wiring is deferred.
- open-questions.md: add OQ-05 (deferred(scope: v1 cut)) with gap
detail and deferred scope
- ADR-067: dated status amendment + v1-cut note at the data-channel
step; References point to OQ-05
- ADR-048: dated reconciliation note — overlay bidirectionality is
decided design, not a v1 implementation commitment
- websocket.md: status notes on §"Data channels for browsers" and
upgrade step 7
- task review-001-ws-data-channel-decision: completed, Summary filled
Verification: taskgraph validate (38 tasks OK); cargo doc --no-deps
(pre-existing warnings only).
- apply bearer_auth_middleware route_layer AFTER the extra_routes merge,
so assembly-layer custom routes resolve the bearer token by default
(ADR-046 §4); per-route opt-out via the route's own layer remains
- enforce RESERVED_PATHS per-method at build time: a probe MethodRouter
occupied on all methods is pre-merged against extras, so a custom
POST /search panics like a same-method overlap (ADR-046 §3)
- tests: auth resolves through an extra route; an extra route with its
own layer opts out; reserved-path merge panics; non-reserved
different-method merge stays legal; MCP bearer-gate test stays green
- ADR-046 §3: one sentence restating the per-method rejection rule
Verification: cargo test (215) ok, cargo test --all-features (260 +
integration) ok, clippy -D warnings (default + all-features) ok,
cargo fmt --check ok.
- GW-01: /publish validates every NDJSON chunk against the op's
publish_schema (incl. the first-line chunk) via NdjsonChunkStream —
terminal Err(INVALID_INPUT)/422 on violation, matching the wire
dispatcher's per-chunk contract. Route-level fix; the alkcall spine
was explored and rejected (wire validation is pump-side by design).
- GW-06: the body is streamed, not buffered — Body::into_data_stream()
-> newline-framed BufferedLines -> lazily parsed chunk stream.
ADR-068 documents the streamed semantics and the 2 MiB per-line cap.
- GW-08: /batch capped at 100 operations (INVALID_INPUT 400).
- GW-09: internal-op batch entries now carry generated UUID request ids.
- GW-10: first publish line missing `chunk` is rejected INVALID_INPUT.
- GW-11: redundant /publish pre-checks removed; enforcement rides on
invoke_sink via the shared dispatch spine.
- HY-13: the vacuous stub test was replaced by a body-cut-short test.
- Adjacent: INVALID_OPERATION_TYPE now maps 422 (with identity) / 401
(without) in error.rs — the route relies on the shared mapper since
the pre-checks are gone (GW-03's finding; was a 500 fall-through).
Verification: cargo test 211 passed; cargo clippy --all-targets -- -D
warnings clean; cargo fmt --check clean.
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.
Spike against alkcall source resolved ADR-067 assumptions:
- write_chunk issues header+payload as separate write_alls; channel
0's write_frame issues prefix+body separately — a logical write can
surface as multiple chunks, so the WS adapter must parse outgoing
chunk boundaries (byte-stream treatment both directions), not assume
write-per-chunk or message-per-chunk
- MAX_CHUNK_LEN is 16 MiB; the WS path needs a practical message cap
with oversized chunks split across messages
- install_channel_zero + run_loop_single_stream confirmed as the exact
server-side seam; EOF/teardown invariants already specified by
alkcall (REQ-CH-01/02)
Corrections applied to websocket.md, ADR-067, OQ-01.
docs/plans/implementation.md: scoped plan guiding task decomposition —
spike findings, 4-phase build order, OQ dispositions, task conventions.