--- id: review-001-gateway-publish-semantics name: /publish + /batch gateway fixes (GW-01, GW-06, GW-08..GW-11, HY-13) status: pending depends_on: [] scope: narrow risk: medium impact: component level: implementation tags: [gateway, review-001] --- ## Description Review 001 findings on `src/gateway/routes.rs`'s `/publish` and `/batch` routes: - **GW-01 (major)**: `/publish` (`routes.rs:257-276`) feeds parsed NDJSON straight to `invoke_sink`; `publish_schema` validation lives only in alkcall's wire `Dispatcher` — so a Pub op registered with `publish_schema` receives arbitrary attacker-controlled JSON over HTTP while the same op over the call protocol aborts invalid chunks. Handlers written against the validated-wire guarantee get a transport-dependent invariant. Fix: validate in the route (or move validation into the shared `invoke_sink` spine so both transports enforce it — prefer the spine if alkcall's surface allows, else the route). - **GW-10**: a first line missing `chunk` silently publishes `Value::Null` (`routes.rs:213`) — indistinguishable from intent since null is a legitimate payload. Reject with `INVALID_INPUT` (the route already rejects a missing `operation` this way). - **GW-06**: `/publish` buffers the whole NDJSON body (2 MiB-capped) before dispatch, contradicting ADR-068 step 4. Either stream the body (axum `Body` → framed stream) — the real fix — or amend ADR-068 to document the buffered 2 MiB semantics. Decide, then implement or amend. - **GW-08**: no cap on batch operation count; the 2 MiB body is the only bound and the 30 s deadline is unenforced (GW-05). Cap batch size (constant, e.g. 100). - **GW-09**: internal-op batch entries emit `request_id: null` while dispatched entries carry a UUID — one response body, two envelope shapes. Generate request ids for internal-op entries. - **GW-11**: `/publish` runs four registry lookups + ACL checks that `invoke_sink` then repeats — drop the redundant pre-checks (mirror `/call`/`/batch`, which deliberately rely on the registry). - **HY-13**: the vacuous test at `routes.rs:1572-1583` (`publish_body_is_fully_consumed_before_dispatch_not_required`) cites a socket-level test that doesn't exist — wire the early-disconnect test or delete the stub. ## Acceptance Criteria - [ ] `/publish` test with a `publish_schema`-registered Pub op rejects an invalid chunk (review's gate for this unit) - [ ] First line without `chunk` → `INVALID_INPUT`, not a null publish (test) - [ ] GW-06 decision landed: true streaming or ADR-068 amended; consistent tests + docs - [ ] Batch size capped (test); mixed-shape batch envelopes fixed - [ ] Redundant `/publish` pre-checks removed (dispatch still enforces) - [ ] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass ## References - docs/reviews/001-initial-implementation-review.md (Part C, GW-01, GW-06, GW-08..GW-11; HY-13) - docs/architecture/decisions/068-gateway-publish-endpoint.md - docs/architecture/decisions/023-operation-error-schemas.md ## Notes > Agent fills during implementation. If the GW-01 fix goes into > alkcall's `invoke_sink` spine, coordinate the alkcall change (small, > additive) and note it in the summary. ## Summary > Filled on completion.