- enforce the same 100-operation cap the HTTP /batch endpoint enforces;
over-cap \x60calls\x60 reject with a structured INVALID_INPUT (retryable:
false, matching CallError::invalid_input) before any dispatch
- hoist MAX_BATCH_OPERATIONS to gateway/mod.rs and reuse it in routes,
to_openapi (removing a pre-existing duplicate literal), and to_mcp
- state the limit in the batch tool description and add maxItems to the
input schema (doc previously advertised no limit)
- GatewayDispatch gains a per-instance invoke_count spy accessor so the
over-cap test proves zero dispatches (process-global counters raced
under the parallel test runner)
- tests: over-cap -> INVALID_INPUT + invoke_count()==0; at-cap -> 100
results + invoke_count()==100
verification: scripts/verify.sh (352 passed) and scripts/verify.sh
--all-features (468 passed); cargo clippy --all-targets -D warnings and
cargo fmt --check clean
- AxumFraming arms were exercised only indirectly via tungstenite
(shared generic pumps); drive the axum message types directly with
an in-process fake WebSocket (futures mpsc-backed Sink+Stream
stand-in for the split halves)
- text test: read pump maps a text message to the WriteMsg close
carrying 1002 + the text reason
- cap-trip test: an above-MAX_CHUNK_LEN header through the write pump
closes with 1011 naming the violation
Verification: scripts/verify.sh OK (345 passed), test-support suite
ok, clippy -D warnings clean, fmt clean
- WsTimeouts { idle, write } request extension mirrors ChannelsPolicy:
a deployment layers it on a WS route (bare-registry routes included)
to set the pump knobs per route
- precedence: extension present replaces the router state entirely;
absent falls back to SessionState (adapter-configured idle) and the
crate default write window — a Default impl never clobbers the
adapter-configured idle knob
- upgrade.rs module + handler docs now state the real defaults for
bare-registry routes (60 s idle + 60 s write, 64-session semaphore,
handler-private WsSessions) and the extension surface
- split_ws_to_bytes_idle_with_write exposes the WS-18 write window to
run_channels_session; acceptance test drives a bare-registry route
with a 150 ms extension idle window (1001 eviction observed)
Verification: scripts/verify.sh OK (343 passed), test-support suite
ok, clippy -D warnings clean, fmt clean
- DEFAULT_WS_WRITE_TIMEOUT (60 s, WS-01 knob family): one outbound WS
send that stays unsent past the window (peer stopped reading)
evicts the connection — the send path was slot-bounded but
time-unbounded
- bound is per send call: a slow-but-draining peer resets it with
every message emitted; only a fully stalled sink trips it
- on timeout the pump signals the stream error (InvalidData naming
the write stall) and ends WITHOUT a close frame — a clogged socket
cannot receive one and the close send would park on it
- test-support split helper with both knobs explicit backs the
scaled eviction test (clogged duplex, eviction well inside the
5 s slack)
Verification: scripts/verify.sh OK (343 passed), clippy -D warnings
clean, fmt clean
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.
The review-001 CON-02 monitor swept the pending map once EOF held but
never exited: every fire-and-forget import whose peer died left a
spawned task + pending map + watch receiver alive for the process
lifetime, and once the read pump ended the watch sender dropped so the
old select's 'changed()' error branch spun.
- replaced the 1s never-exiting sweep with a 50ms post-EOF drain
(fast-fail) and a bounded grace window: the monitor ends after
PENDING_SWEEP_MAX_POST_EOF (8) consecutive empty drains (~400ms)
- post-EOF registrations now resolve in ~50ms with retryable
CONNECTION_CLOSED instead of waiting up to 1s for the next sweep
- removed the silent busy-spin: the loop now only wakes on the sweep
tick or the session-close signal (close path still fails all + exits)
- retained the monitor JoinHandle on WssDropMonitor (test builds) and
added a lifecycle test asserting the monitor joins after EOF+grace
- tightened call_registered_after_eof test: 500ms bound + CONNECTION_
CLOSED code assertion (was a pre-sleep + 5s timeout)
Verification:
- cargo test --features wss: 358 passed, 0 failed
- cargo test --all-features: 454 passed, 0 failed
- cargo clippy --all-targets -- -D warnings (default, wss, all): clean
- cargo fmt --check: clean
- scripts/verify.sh + verify.sh --all-features: VERIFY OK
The eager placeholder-binding check in spec_name_references_undeclared
short-circuited on an empty properties set, letting
FromJsonSchema::new accept a template whose placeholder could never be
bound (every later call failed INTERNAL, or INVALID_INPUT if a peer
supplied the key). Drop the early return so any placeholder against a
declared-nothing (or properties-less) input_schema is unbound, matching
the from_openapi unbound_placeholders path.
- add construction-time test: no-properties schema + {id} template
fails with the placeholder error; no-properties schema + placeholder-
free template still constructs and imports as Visibility::Internal
Verification: scripts/verify.sh (342 passed), scripts/verify.sh
--all-features (454 passed); clippy -D warnings and fmt --check clean
- WriteMsg::CloseWith now carries (code, reason); the write pump sent
the hardcoded "text messages not supported" string for the idle
(1001) and inbound-frame (1011) closes as well as the text (1002)
close
- close_reason module: canonical per-cause reason strings, shared by
the close frames and the stream-error diagnostics (they already
matched for idle/oversize; now single-sourced)
- reason asserts added to the idle-stall and forever-dribble 1001
tests (reason names the no-chunk-progress cause) and a new text
frame test asserts 1002 + the distinct text reason
Verification: scripts/verify.sh OK (342 passed), clippy -D warnings
clean, fmt clean
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).
- byte-cap check moved ahead of poll_ready/try_send so the mux sees
the InvalidData stream error at the failing call instead of the
write pump emitting a mid-stream 1011 close (the chunk was already
committed to the wire path)
- pump-side over-cap arm replaced by a debug_assert (defense-in-depth
invariant; the error was unreachable for single-call writes once the
pre-send check exists)
- write_tx_mut renamed write_tx_ref with an updated doc contract
- WS-05's over-cap pump test replaced by a pre-send rejection test
(error surfaces at the first write, names the cap) + a cap-edge
test (exactly PENDING_BUFFER_CAP still flows)
Verification: scripts/verify.sh OK (341 passed), clippy -D warnings
clean, fmt clean
Module doc gains three sections: the FWD-18 routing rule (placeholder
keys never double-emit as query; structural path values are
INVALID_INPUT), the FWD-19 percent trade-off (% in values always
encoded; % in assembly-supplied template/base text survives verbatim —
documented instead of rejected, per the ADR-066 assembly trust
boundary), and the FWD-17 streaming payload contract. FWD-19 behavior
pinned by a wire-level template test.
The routing half of FWD-18 was already in place (placeholder keys skip
query routing before value-shape inspection); this pins it with a test
and decides the structural-value outcome: an object/array value under a
placeholder key now fails with INVALID_INPUT instead of splicing the
minified JSON into the path segment. Scalars (string/number/bool/null)
render as before.
Option (b) of the FWD-17 decision: payloads decode as JSON when valid
(number stays number, quoted string stays string); non-JSON payloads
surface as {"data": <raw>, "event": <name|null>} instead of
silently degrading to a JSON string. The parser captures the frame's
event: field (WHATWG last-wins) and resets all pending state on every
blank-line dispatch, so an event:-only frame (no data) cannot leak its
name into a later frame. JSON frames surface as themselves even under
a named event; the implicit 'message' default never wraps a payload.
Verification: cargo test (33 forward tests incl. 4 new FWD-17
contract/parser pins), clippy, fmt.
Review 002 GW-15 [major]: /publish lost both claimed memory bounds in
the GW-06 streaming rewrite. Unauthenticated POST /publish with chunked
'a'-forever (no newline) grew the heap with the upload until OOM, and
each poll re-scanned the whole buffer (O(n^2) on top).
- BufferedLines: cap the unterminated tail against
MAX_PUBLISH_LINE_BYTES immediately after every chunk read (checked
before yielding, even with no \n seen) and re-check on the
trailing-EOF mem::take path; a breach aborts the whole reader
(pending lines included) with the same terminal INVALID_INPUT
LineCap error. Complete lines keep the baseline at-cap semantics.
- Whole gateway router: explicit request-body-limit layer
(GATEWAY_BODY_LIMIT = 2 MiB + 64 KiB framing headroom). A raw-Body
handler never consults axum's DefaultBodyLimit (that is an extension
extractors read), so /publish had no whole-body cap at all. The layer
pre-rejects oversized declared Content-Length and wraps chunked
uploads in a counting stream; both answer plain-text 413. It
deliberately sits above the per-line cap so a single over-cap line
still surfaces the semantic line-cap error. Upstream body read
failures are not flagged as limit-exceeded (disconnects are not 413).
- New wire tests: streamed never-newline over-cap rejections (both the
streamed multi-chunk and trailing-EOF shapes), 413 on chunked
over-limit uploads, 413 on oversized declared Content-Length, cap
breach batched with complete lines, at-cap line still round-trips,
declared-length over-limit pre-rejection.
Module status mapping note (GW-16 tracks the drift): hand-rolled
pre-dispatch rejections use 400/INVALID_INPUT while mid-stream chunk
errors map 422 through gateway::error; normalizing is GW-16, not GW-15.
Verification: cargo test 305+5 pass, cargo test --all-features pass,
cargo clippy --all-targets -- -D warnings pass (--all-features too),
cargo fmt --check pass.
- path-item-level `parameters` parse into PathItem and merge into every
operation's input schema; operation-level entries override shared
name+in duplicates (last-insert wins)
- success sweep accepts `2XX` after concrete 2XX keys and before
`default` (SSE detection + output schema; concrete outranks wildcard)
- `4XX`/`5XX` error keys project to their class representative status
(`HTTP_400`/`HTTP_500`) instead of dropping silently; `default`
still drops loudly (no implied range)
- top-level `webhooks` fails import naming the feature (inbound
callbacks are outside the single-endpoint outbound adapter model)
- unbound-placeholder error names the parameter-merge state so the
diagnosis no longer dead-ends
Verification: cargo test (174 lib tests), cargo fmt
Mirrors the WS-13 progress semantics onto the axum upgrade path with
three integration tests over the real HttpAdapter surface (also the
COV-11b dark-knob gate — with_ws_idle_timeout had no test caller):
the forever-dribble client is evicted with 1001 despite arriving
messages; a productive session round-trips calls across many windows
without eviction; the None knob never evicts a dribbling client.
The router-wide bearer_auth_middleware route_layer wraps every route
registered before the call (axum 0.8 RouteManager semantics, verified
against the vendored axum 0.8.9 source), so it double-wrapped the WS
upgrade route (own ws_bearer_auth layer) and the /mcp nest (own inner
bearer layer) — the token resolved twice per request on both, and the
SRV-10 comment claimed the opposite.
Restructure build_router: /mcp is merged and the WS upgrade route is
registered after the router-wide route_layer, so each keeps exactly one
auth layer. The WS MethodRouter now carries the decoy 405 fallback
explicitly (MethodRouter::route_layer wraps method endpoints, not the
fallback), preserving the SRV-07 decoy shape for wrong-method probes on
/alk/channels. Comments state axum's actual route_layer semantics.
Tests: a counting IdentityProvider pins one resolution per WS upgrade
request and per /mcp initialize (verified to fail with left: 2/3 under
the pre-fix ordering); the 401 enforcement and the WS-path decoy 405
are pinned.
Verification: cargo test (304), cargo test --all-features (376 + 5
integration suites), clippy -D warnings (default + all-features),
cargo fmt --check, cargo doc --no-deps
Decides the WS-13 legitimate-silence question as option (b): 60s of no
chunk progress is an intentional eviction line even for silent
subscriptions; no WS ping/pong keepalive is added because a keepalive
can only rescue app-silence by re-arming the deadline, which reopens
the dribble hole the knob exists to seal. Documented in the byte_adapter
module doc, on DEFAULT_WS_IDLE_TIMEOUT, on the unchanged
HttpAdapter::with_ws_idle_timeout knob, and in websocket.md (new
'Idle-read timeout' section, including the FWD-15 SSE-keepalive
layering note). Deployment posture for long-lived silent sessions:
with_ws_idle_timeout(None) + WsSessions abort + write-side caps.
The WS-01 idle timer reset on WS message arrival, so the forever-dribble
stall (declare a chunk, deliver its payload one byte per message) reset
the deadline forever while the demux stayed parked on the partial chunk;
conversely the reset-on-message rule was the only thing slow-but-alive
sessions survived on.
Semantics: the deadline now resets on demux progress — bytes actually
forwarded into read_tx that complete inbound chunks (the frames the
demux routes), tracked byte-for-byte in line with alkcall's parse walk
(8-byte header -> payload skip). Message arrival without a completed
chunk resets nothing, so the dribble hits the deadline; every completed
chunk (even one per message, slowly) re-arms the window.
Tests (tungstenite path): the forever-dribble eviction with 1001 despite
arriving messages; a productive-progress session that survives across
many windows; the knob-disabled (None) arm; the stall and text/cap arms
unchanged.
http-adapters.md: forwarding-handler step 5 documents the time/bytes
split for Sub forwards (stream client without the total request
timeout, total streamed-bytes cap, read timeout as the staleness
guard); the HTTP Client section documents the two derived clients
(request vs stream), the reqwest 0.13 per-request-override limitation
that forces the derived-client design, and that only the total timeout
differs between them.
http-server.md: the per-endpoint dispatch deadline paragraph now covers
the outbound half — unbounded time, bounded bytes on both the gateway
and the forwarding side (ADR-049/021 note: unbounded time by design for
subscriptions, bounded bytes per subscription).
Verified: cargo doc --no-deps clean, full suite green.
FWD-15: forward_stream now sends through SharedHttpClient::stream_client
— a client derived from the same config with the total request timeout
removed and connect + read timeouts retained. reqwest 0.13's per-request
override can lengthen a client-level total timeout but never clear it
(request-scoped None falls back to the client default), so the derived
client is the only correct mechanism. Both clients rebuild-and-swap
together atomically (FWD-12). A healthy >30s subscription survives; the
read timeout stays as the staleness guard, matching the gateway's
deadline: None dispatch contract (alkcall ADR-021).
FWD-14: the streaming branch enforces a total streamed-bytes cap per
subscription (HttpClientConfig::stream_total_byte_cap, default 1 GiB),
accumulated across every chunk fed to the SSE parser; exceeding it
terminates with a single terminal HTTP_413 error envelope. The SSE
line-cap check moved before extend_from_slice so the reassembly buffer
can never exceed the cap. Removing the total timeout without this cap
would open an unbounded-memory window, so both land together.
Wire tests: keepalive trickle past a scaled total-timeout deadline keeps
delivering; over-cap stream terminates with exactly one terminal error;
parser boundary tests for pre-extend cap checks.
Verified: cargo test (302+5), --all-features (373+41), clippy
--all-targets -D warnings (default + all-features), fmt --check,
doc --no-deps clean.
method_not_allowed_fallback(decoy_method_not_allowed) was registered on
the default router before the extras merge; axum applies the 405
fallback only to MethodRouters present at call time, so wrong-method
probes on extra routes returned axum's bare 405 (no body, no
Server: nginx) — the exact stealth probe SRV-07 neutralized for the
default surface.
Re-apply the fallback after the extras merge (idempotent for routers
the earlier call covered — axum 0.8.9 replaces only Fallback::Default).
Tests pin both shapes: decoy 405 on an extra route, and no regression
of the default-surface 405 after the merge.
Verification: cargo test, cargo clippy --all-targets -- -D warnings,
cargo fmt --check
Replace rmcp's unbounded Peer::list_all_tools with a bounded walk over
list_tools: hard cap of 100 pages (MCP_MAX_TOOLS_LIST_PAGES) plus a 60 s
overall deadline (MCP_TOOLS_LIST_DEADLINE). Tripping either budget fails
loudly with AdapterError::DiscoveryFailed naming pages fetched and tools
accumulated — no silent truncation, no partial registration (import
fails closed, as before). A slow or hung page is cut off by a per-page
tokio timeout sized to the remaining budget.
Bounds are documented in the module doc. Integration test added: a
cycling-cursor server (next_cursor always Some("a")) terminates with
the clean budget error inside an outer 10 s guard; the existing 3-page
pagination test is unchanged and passes.
Verification: cargo test (304 pass), cargo test --features mcp,
cargo test --all-features (412 pass), clippy -D warnings both default
and --all-features --all-targets, cargo fmt --check, cargo doc --no-deps.
- build_error_schemas: default/wildcard response keys dropped with a
warn instead of emitting a dead HTTP_0 ErrorDefinition — /search
never advertises a code that can't match (the runtime mapper already
synthesizes HTTP_<actual> for unmapped statuses)
- check_parameter_style: non-default style/explode parameter forms
(spaceDelimited, pipeDelimited, deepObject, form+explode:false,
simple+explode:true) fail import with a feature-naming SchemaParse;
wire-equivalent defaults (form, simple) import unchanged — no more
silent "[1,2]" array mis-serialization
- servers overrides rejected at import at all three levels (document,
path, operation) — the adapter pins one base_url at assembly time
- trace-only paths: skip is now logged (warn naming path + methods),
documented-as-inert instead of silent
- detect_op_type + build_output_schema sweep 2XX/default keys for
text/event-stream — a default-declared SSE stream classifies as Sub
instead of returning one giant text body
Tests: 11 new (error-drop, style rejections + default accept, servers
3-level rejections + baseline, trace skip, SSE default/2XX detection).
Verified: cargo test (299), --all-features (370 + suites), clippy
--all-targets -D warnings (default + all-features), fmt --check.
Tasks: review-001-openapi-loud-degradation
- new gateway::schema_cache — PublishSchemaCache: compile the op's
publish_schema once per registration (value-keyed invalidation for
hot reload), cache compile failures (logged once at error level,
never retried per request)
- /publish compile failure is now fail-closed: the chunk stream
terminates with INTERNAL (500), the error text stays in the log
(no schema internals on the wire) — the per-request warn-and-skip
unvalidated ingest path is removed
- schema resolution is lazy (first chunk poll, after invoke_sink's
404/403/422 pre-checks — GW-11 order preserved) and keyed by schema
value, so re-registration/hot reload is picked up (test)
- NdjsonChunkStream: first Err item is terminal (done + stream end),
mirroring the wire pump's send(Err) + break — Ok chunks can never
follow an error on the HTTP path either (found by spy-handler test)
Verified: cargo test (308), cargo test --all-features, clippy
--all-targets -D warnings (default + all-features), fmt --check.
Tasks: review-001-publish-schema-validation-robust
Found in the sweep of completed review-001 remediation:
- /publish schema validation fails open on compile error + recompiles
per request (remediation-introduced, routes.rs:254-268)
- OAI-06 loud-degradation unblocked and still open (HTTP_0 marker)
- HY-06 ExponentialBackoff in public API + COV-02 mTLS success path
both unblocked post client-config rework
- HY-02/04/11 publish-prep docs gate (104 missing-docs warnings
re-measured)
Also flagged, not tasked here: WS-12 (alkcall demux 4 GiB discard
alloc) was never actually filed in alkcall's consumer-findings-ledger —
only CF-001 is there. File it when next touching alkcall.
taskgraph: validate clean (42), no cycles
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).
- build_request takes the op's input_schema and rejects undeclared
input keys (INVALID_INPUT) before any outbound request is built;
explicit `additionalProperties: true` opts into catch-all input;
non-object inputs rejected (OAI-02)
- in: header parameters are stamped `wire: header` in the generated
input schema and sent as upstream request headers, not query params;
in: cookie fails import with a clear error (OAI-03)
- a spec parameter named `body` is rejected at import unconditionally
(OAI-07)
- FromJsonSchema::new returns Result and validates method/path template/
base_url at construction; registered visibility forced to Internal
like from_openapi; module doc corrected (OAI-09)
Verified: cargo test, cargo test --all-features, clippy (both feature
sets, -D warnings), cargo fmt --check
- /search, /schema: document the envelope wrapper and the real item/spec
fields (PRJ-01/02); /search drops unreachable 401/403, documents 404
(PRJ-15)
- error statuses: 422 for dispatch-path INVALID_INPUT /
INVALID_OPERATION_TYPE; extractor 400s documented as the plain-text
gap they are (PRJ-03); operation-declared errors projected by
http_status with x-runtime-behavior: 500 on non-HTTP_* codes (PRJ-04
project-honest decision) — no runtime changes
- /subscribe: 200+SSE only; event:error terminal contract documented
(PRJ-05, GW-12)
- components for requests/responses; CallRequest no longer inlined
per-path (PRJ-14); all library expect() paths removed (PRJ-11)
- error projections folded into BTreeMaps: same registry =>
byte-identical doc, sorted enums (PRJ-12)
- components.securitySchemes.bearerAuth + top-level security (PRJ-15)
- info.version 1.1.0 -> 1.2.0 (ADR-045 minor: additive documentation of
the settled runtime contract)
- 31 unit tests incl. golden print-level assertions mirroring the routes
tests' actual bodies and a determinism test
Verification: cargo test (288), clippy --all-targets -D warnings, fmt
--check, cargo doc --no-deps, cargo test --all-features (all green in a
clean worktree at HEAD; shared tree carries parallel agents' edits).
- index components/parameters + requestBodies in OpenAPISpec; resolve
bare $ref parameter and requestBody entries through the cycle-guarded
resolver; unresolvable refs abort spec parse loudly
- reject path placeholders with no matching input-schema property at
registration (no more silently percent-encoded literal placeholders)
- reject duplicate operationIds and path+method routes in one import
batch instead of silent last-write-wins registration
- also reject a parameter named 'body' shadowed by requestBody (OAI-07
adjacency, same code path)
Verified: cargo test (243), --all-features (322), clippy -D warnings
(default + all-features), fmt --check, doc --no-deps
- percent-encode path-parameter values with a WHATWG path-segment
superset (/, %, ?, #, \\, controls): traversal values, query/fragment
structure, and later-placeholder strings can no longer alter the
request line (FWD-01)
- single-pass template rendering; rendered values are never
re-substituted; unbound or unterminated placeholders error loudly
- append the request path to the base URL directory (https://host/v1
+ /chat/completions keeps /v1) instead of Url::join semantics,
with a post-assembly origin-equality check (FWD-02)
- base_url validation: https/http-only scheme allowlist, explicit
host required, userinfo rejected (credentials flow via
Capabilities only); request_path is never empty
Verification: cargo test (238 lib tests incl. 8 new FWD-01/02 tests),
cargo clippy --all-targets -- -D warnings, cargo fmt --check
Add tests for the /mcp body cap and complete the task file.
- oversized POST /mcp with declared Content-Length > 8 MiB -> 413
before any body read
- oversized chunked POST /mcp -> 413 (counting-stream cut mid-body;
rmcp maps body errors to 500, so the middleware sources the status)
- normal-size initialize round-trip unchanged
- task file: status completed, Summary filled
Verified: cargo test (219), cargo test --features mcp --lib (257),
cargo test --all-features (269 + integration), clippy default and
--all-features (-D warnings), cargo fmt --check.
- Replace the axum/tungstenite pump paths' Notify-based read-EOF signal
with a retained tokio watch channel: a late subscriber (monitor
spawned after session setup, or pump EOF before the receiver is
taken) still observes EOF (WS-02).
- from_wss drop monitor: on EOF (or session close) fail all pendings
retryable, then keep sweeping the pending map every 1 s — calls
registered after the initial fail_all (the forgotten-session import
path) resolve instead of hanging (CON-02).
- Tests: drop-during-registration race variants (forget + held
session) and a post-EOF registration resolved via the sweep; the
existing no-hang test stays green.
cargo test (219), cargo test --features wss (231, 3x for flake check),
cargo clippy --all-targets -- -D warnings, cargo fmt --check
Replace the axum/tungstenite pump paths' Notify-based read-EOF signal
with a retained tokio watch channel so a late subscriber observes EOF
regardless of when it fired. Extend the from_wss drop monitor to sweep
the pending map (1 s interval) once EOF is observed, so calls
registered after the initial fail_all also resolve retryable instead
of hanging.
cargo test; cargo clippy --all-targets -- -D warnings (default +
all-features); cargo fmt --check
The /mcp nest (to_mcp streamable HTTP service) collects the raw body
itself, so axum's extractor-based DefaultBodyLimit never applies and a
multi-GB chunked POST buffers entirely in memory (review-001 SRV-03).
Wrap the nest with an explicit counting-body middleware: 8 MiB cap
(headroom over the gateway's 2 MiB for JSON-RPC batch payloads),
Content-Length-declared oversizes rejected before reading, streaming
oversizes cut off mid-read and answered 413 (rmcp maps body-read errors
to 500, so the middleware sources the status itself).
Verification: cargo check (default + mcp + all-features); mcp test
suite pending tree unstuck from parallel agent's WIP.
Replace per-chunk parse_sse_frames with SseParser holding raw bytes
across chunks: reassembles frames split at TCP boundaries (the review's
silently-losing case), decodes UTF-8 per complete line so multi-byte
chars split across chunks survive, caps the buffer at 1 MiB
(SSE_EVENT_BUFFER_CAP) and dispatches a pending event at EOF.
forward_stream threads the parser through its unfold state and emits a
terminal error envelope on cap overflow. Existing single-chunk SSE test
assertions preserved; added multi-chunk, split-UTF-8, EOF-dispatch, and
cap tests.
Verified: cargo test (219 pass), clippy -D warnings, fmt --check.
- 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.
resolve_refs_recursive recursed with no cycle detection and no depth
budget; a self-referential OpenAPI component stack-overflowed and
aborted the process (uncatchable, kills import()).
- add branch-scoped visited set on the JSON-pointer ref path: a ref
re-entering its own expansion chain errors cleanly with
AdapterError::SchemaParse naming the offending ref (OAI-01)
- add depth budget (MAX_REF_RESOLUTION_DEPTH = 64) bounding $ref hop
chains and schema nesting height; over-deep specs error cleanly
instead of exhausting the stack (OAI-01)
- clean loud error over depth-limited expansion: recursive schemas
(trees, linked lists, cursor pagination) fail import rather than
expand unboundedly
- shared refs to a common schema (diamond/repeated) still resolve —
visited set is branch-scoped, not global
- OAI-08: replace the two guarded expects in from_value
("paths is object", "schemas is object") with if-let paths
Verification: cargo test 188 passed / 0 failed; clippy
--all-targets -D warnings clean; fmt --check clean
21 review-001 tasks across server/adapters/client/gateway/websocket/infra,
chunked from the 7-unit remediation plan in
docs/reviews/001-initial-implementation-review.md.
- Scope split by mechanism, not one-per-finding: 15 tasks in generation 1
(parallelizable), 6 sequenced after their file-sharing precursors
- Deliberately deferred until dependent fixes land: projection/doc
fidelity partial (Unit 6 beyond dependency hygiene), coverage backfills
(COV-01..07 via in-task acceptance for forward.rs), and per-finding
minors (OAI-06/07, HY-02/04/06/10/11, CON-08)
- Cross-crate WS-12 (alkcall demux 4 GiB discard alloc) noted for filing
in alkcall, not here
taskgraph: validate clean, no cycles, 6 generations
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.
Ported the alknet-http overlay verification to the channels-over-WS
session (tests/ws_overlay_ops.rs, test-support feature, 8 tests):
- overlay mechanism: browser-registered ops land in the connection's
Layer 2 overlay (register_imported), exposed via overlay_env() —
no PeerIds (browsers are not peers); PeerRef::Specific to a browser
id routes to nothing (NOT_FOUND)
- hub→browser call through compose_root_env's attached overlay
- AccessControl on browser ops gates hub calls (scope match allows,
missing scope FORBIDDEN)
- overlay dies with the connection; no leak between connections;
in-flight calls to browser ops resolve on close
- wire-level: 10 interleaved concurrent calls across two WS sessions
— no cross-correlation, no deadlock; disconnect mid-call resolves
and a fresh session works (no listener wedge)
byte_adapter: read_eof Notify now gated to the wss feature (its only
consumer is from_wss) so a test-support-only build is warning-free.
Verified: cargo test (182 lib), --all-features (227 lib + 5 MCP + 8
overlay + 10 WS integration), clippy -D warnings (default,
test-support, all-features), fmt.