jcp-gateway: everything a gated request carries to the server is sealed and flow-checked #339

Merged
dcharlot merged 1 commit from fix/jcp-gateway-flow-checks-whole-request into main 2026-10-19 21:21:56 -04:00

2026-10-19

Transaction Science
jcp-gateway: everything a gated request carries to the server is sealed and flow-checked
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 7s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 56s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 5m36s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 6m57s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m39s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m14s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m40s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m16s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m38s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m3s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m35s
The gateway decided a gated request over its arguments alone, but it
forwarded the whole request. So a `_meta` key, a `params` member the
method does not define, or a member of the JSON-RPC message beside
`params` reached the server without passing the flow ceiling. A value
from an untrusted read could leave by any of them.

Each of those is now sealed and flow-checked along with the arguments.
Two things are left out, and the code documents why:

- The protocol's own `_meta` keys: `progressToken`, and keys under a
  prefix MCP reserves for itself, such as `io.modelcontextprotocol/`.
  These are machinery written by the client's runtime, and a server
  names them back in every reply. The flow store admits every payload it
  labels, so folding these keys would make the first tainted request
  taint every request after it.
- This gateway's own namespace and its legacy witness key.

A request that carries nothing else is decided over exactly the bytes
it was before.

This also fixes a latent false refusal. A `resources/read` was gated
over its whole `params`, `_meta` included. Under MCP 2026-07-28 the
protocol keys in every request matched the server's keys in every
reply, so once one reply had been admitted, every later read under a
flow ceiling was refused as tainted.

A request's parts (arguments, answers to an interim result, what it
carries beside them) are now scanned joined by a separator unique to
that request, not wrapped in one JSON object. A wrapper's keys would be
the same text in every request of that shape, and the stored admission
of one tainted request would carry its taint into all the rest.

SPEC §4.10 now says what "the argument" covers for a forwarding
enforcement point, and what it may leave out if it says so.

Tests: 6 new. All 13 mutations of the new logic are killed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 11:23:23 -04:00