- 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
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.
- 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
- 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.