jcp-gateway: Close the flow loop from tool output to sealed verdict #42

Closed
dcharlot wants to merge 3 commits from jcp-gateway-flow-relay into jcp-receipt-flow-seal
Owner

Final increment of the FL-2 flow-lattice design (FLOW_LATTICE_PLAN.md) — the loop closes end to end. Base is jcp-receipt-flow-seal (#41); this branch also merges the jcp-flow crate (#39) that the relay depends on, so its diff shows jcp-flow + jcp-gateway. Review #37 → #39/#40 → #41 first; the net-new work here is crates/jcp-gateway. Once the stack lands on main this rebases to the gateway diff alone.

What. The relay now folds an argument's flow label from the tool outputs admitted earlier this session (Phase 0), authorizes against those facts, and — after a successful call — admits the result as a labelled untrusted source (Phase 4). Derived taint outlives the single call that produced it, which is the whole point.

  • authorize_flow denies a tainted argument above the grant's ceiling at the gate, before the upstream is contacted and before any debit — zero joules.
  • charge_flow also depletes the tainted sub-budget; the receipt seals as jcp/3 when tainted, byte-identical jcp/2 when clean.
  • The result frame is Trust::Data, so mcp_result_frame goes live (written for exactly this, never called): an injected instruction in a result still cannot exercise a capability, and now its provenance also flows forward.
  • Witnesses are retained by id so an auditor fetches one and re-derives a verdict offline.
  • set_flow_ceiling / enable_high_water / forbid_flow are the operator controls.

The capstone test is the attack, end to end (a_value_derived_from_an_untrusted_read_cannot_be_exfiltrated): read an attacker email (allowed, output admitted), then try to send a body carrying its secret verbatim → denied tainted-flow at the gate, zero joules, a jcp/3 receipt whose flow block reads "above", and a witness verify_witness re-checks offline. A clean send still goes through; the whole jcp/2 + jcp/3 log verifies as one chain. The four pre-existing gateway tests pass unchanged — a clean call is byte-identical to before.

Full JCP workspace: 230 pass, 0 warnings.

This completes FL-2: a value derived from an untrusted read can be refused from a capability even when its literal bytes match no rule, the verdict sealed into a replayable receipt and metered against a joule budget — the wedge (budget + receipt) no CaMeL/dual-graph work has. Honest residuals unchanged from the plan §7 (semantic laundering, sub-window unregistered secrets, readers-enforcement v2, in-process FlowFacts).

Release Notes:

  • Added end-to-end flow-provenance enforcement to the JCP gateway: tool outputs propagate taint, tainted calls are gated and sealed as jcp/3, with a replayable witness
**Final increment of the FL-2 flow-lattice design (FLOW_LATTICE_PLAN.md) — the loop closes end to end.** Base is `jcp-receipt-flow-seal` (#41); this branch also merges the jcp-flow crate (#39) that the relay depends on, so its diff shows jcp-flow + jcp-gateway. Review #37 → #39/#40 → #41 first; the **net-new work here is `crates/jcp-gateway`**. Once the stack lands on main this rebases to the gateway diff alone. **What.** The relay now folds an argument's flow label from the tool outputs admitted earlier this session (Phase 0), authorizes against those facts, and — after a successful call — admits the result as a labelled untrusted source (Phase 4). **Derived taint outlives the single call that produced it**, which is the whole point. - `authorize_flow` denies a tainted argument above the grant's ceiling **at the gate**, before the upstream is contacted and before any debit — zero joules. - `charge_flow` also depletes the tainted sub-budget; the receipt seals as **jcp/3 when tainted, byte-identical jcp/2 when clean**. - The result frame is `Trust::Data`, so `mcp_result_frame` goes live (written for exactly this, never called): an injected instruction in a result still cannot exercise a capability, and now its provenance also flows forward. - Witnesses are retained by id so an auditor fetches one and re-derives a verdict offline. - `set_flow_ceiling` / `enable_high_water` / `forbid_flow` are the operator controls. **The capstone test is the attack, end to end** (`a_value_derived_from_an_untrusted_read_cannot_be_exfiltrated`): read an attacker email (allowed, output admitted), then try to send a body carrying its secret verbatim → **denied `tainted-flow` at the gate, zero joules, a jcp/3 receipt whose flow block reads "above", and a witness `verify_witness` re-checks offline.** A clean send still goes through; the whole jcp/2 + jcp/3 log verifies as one chain. The four pre-existing gateway tests pass unchanged — a clean call is byte-identical to before. Full JCP workspace: **230 pass, 0 warnings.** This completes FL-2: a value derived from an untrusted read can be refused from a capability even when its literal bytes match no rule, the verdict sealed into a replayable receipt and metered against a joule budget — the wedge (budget + receipt) no CaMeL/dual-graph work has. Honest residuals unchanged from the plan §7 (semantic laundering, sub-window unregistered secrets, readers-enforcement v2, in-process FlowFacts). Release Notes: - Added end-to-end flow-provenance enforcement to the JCP gateway: tool outputs propagate taint, tainted calls are gated and sealed as jcp/3, with a replayable witness
The flow lattice says what a label is; this crate computes one from real bytes.
As a run reads tool outputs and derives new values, FlowStore tracks where each
value's bytes came from and folds a derived value's label from the ancestors it
was actually computed from — so authorization can be conditioned on where an
argument came from, not merely on its literal content, the gap the one-bit Trust
tag cannot express.

Two properties make this a reference monitor rather than a heuristic. The carry
detector is an exact blake3 window index, deliberately not a rolling hash: each
W-byte window is hashed independently, so two values share a fingerprint exactly
when they share a verbatim run, with no carry state to get wrong and identical
fingerprints on every platform. And every verdict emits a self-contained,
content-addressed Witness — the ancestor cut, the taint edges, and the folded
label — so a third party re-folds it offline to re-derive the verdict rather than
trusting it; a receipt commits only its id.

label_payload folds over four sources, each an edge kind the witness records:
verbatim carry the scan found (Witnessed, proven by grams), a registered
sensitive term appearing verbatim (Witnessed, for secrets below the window
floor), a caller declaration (Declared — or top, fail-closed, if that parent was
evicted), and in HighWater mode the coarse session floor (Ambient). Every
collection is BTree, so a witness content-addresses deterministically.

Ten tests carry the laws L17-L20 and the residuals as executable claims: verbatim
carry yields a witnessed edge; a sub-window unregistered token yields nothing (the
residual, pinned so it is never mistaken for a guarantee); a registered term of
any length is caught; verify_witness re-folds the cut and catches a label tampered
to look clean; a declared-but-lost parent fails closed to top; and — the attack
this exists to stop — a summary carrying an attacker email verbatim does not sit
below a principal-only ceiling, while HighWater still taints a laundered payload
the scan alone cannot see.

Increment 2 of the FL-2 plan; the runtime gate, jcp/3 receipt seal, and gateway
relay follow. The receipt-stream replay (the eight-check chain walk) lands with
the receipt integration, where jcp/3 receipts exist to walk.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The pieces exist; this wires them into the request path so provenance actually
propagates across a session. The relay gains two phases around the existing
authorize/meter/charge. Before authorizing, it folds the argument's flow label
from the tool outputs admitted earlier this session — the scan finds any verbatim
carry — and after a successful call it admits the result as a labelled, untrusted
source, so a later call carrying its bytes is folded against it. Derived taint now
outlives the single call that produced it, which is the whole point.

Authorization runs against those facts (authorize_flow), so a tainted argument
above the grant's ceiling is denied at the gate before the upstream is contacted
and before any debit — zero joules, the tool-poisoning invariant extended to
derived taint. The charge debits the tainted sub-budget too when the call was
tainted, and the receipt is sealed with the flow verdict: jcp/3 when tainted,
byte-identical jcp/2 when clean, so a non-flow deployment is unaffected. The
monitor's own evaluation energy is metered on the same hardware meter and sealed
as its own field, never as tool cost.

The result frame is Trust::Data, so mcp_result_frame goes live from the gateway
(it was written for exactly this and never called): an injected instruction in a
tool result still cannot exercise a capability, and now the result's provenance
also flows forward. The witnesses are retained by id so an auditor can fetch one
and re-derive a sealed verdict offline.

set_flow_ceiling attaches a FlowBelow ceiling to a tool by re-issuing its grant;
enable_high_water turns on the coarse session floor; forbid_flow installs an
enforcement-point taint floor for a tool.

The capstone test is the attack the feature exists to stop, end to end: read an
attacker email (allowed, its output admitted), then try to send a body carrying
the secret verbatim — the send is denied tainted-flow at the gate, costs zero
joules, seals a jcp/3 receipt whose flow block reads "above", and leaves a witness
that verify_witness re-checks offline. A clean send still goes through, and the
whole jcp/2 + jcp/3 log verifies as one chain. The four pre-existing gateway
tests pass unchanged — a clean call is byte-identical to before.

Final increment of the FL-2 plan. The branch merges the jcp-flow crate (#39) that
the relay depends on; once the stack lands on main this rebases to the gateway
diff alone.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
dcharlot closed this pull request 2026-07-27 19:14:35 -04:00

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!42
No description provided.