Land the FL-2 propagated-provenance stack (increments 1-5) plus RFC 9207 #46
2026-07-25
2026-07-22
jcp-gateway: Close the flow loop from tool output to sealed verdict
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]>
jcp-receipt: Seal a flow verdict as jcp/3
The runtime gate decides a call against its argument's provenance; this makes that decision a sealed, replayable artifact. A new jcp/3 profile carries a flow block — the folded label, its origins and readers, the witness commitment, the ceiling it was tested against, the below/above verdict, and the monitor's own evaluation energy — so a third party can see not just that a call was allowed or denied but on what provenance grounds. The compatibility property is load-bearing and byte-exact: jcp/3 is minted only when a call is actually tainted. A clean call — no facts, or facts that were computed and came back bottom — seals as jcp/2, byte-for-byte identical to the plain path, so a flow deployment and a non-flow one share a receipt stream and every existing receipt is unmoved. A test pins that seal_flow of a clean seal equals seal. The flow energy is sealed as its own field, flow_uj, never folded into cost_uj: a call's tool energy keeps its exact meaning, and the cost of running the monitor is accounted separately and honestly. The witness is a commitment, not the artifact — the cut is disclosed out of band, so the receipt stays small and the disclosure boundary stays where jcp-cite already put it. verify_flow recomputes the jcp/3 core from the same seal, so a jcp/3 receipt round-trips; a jcp/2 clean call verifies through it too. The chain path (append_flow) keeps seq/prev identical regardless of profile, so a mixed jcp/2 + jcp/3 log verifies end to end via verify_log, and extract reads a jcp/3 core generically because it is jcp/2 plus a block. Four tests cover the byte-identity, the tainted seal and its block, the above verdict on a flow denial, and the mixed-profile chain. Deferred, and NOT the plan's §4D fix: replay_spend under-counts a cost-breached call, but the plan's proposed predicate (count caveat-unsatisfied denies toward spend) is unsound — a caveat deny debits in the charge path yet not in the decide path, and the receipt cannot tell them apart, so counting them over-counts the decide path. The correct fix needs an explicit debited_uj field on the wire, a separate change with its own compatibility analysis; it is left untouched here rather than replaced with a broken heuristic. Increment 4 of the FL-2 plan; the gateway relay that computes the facts and the receipt-stream replay follow. Co-Authored-By: Claude Opus 4.8 <[email protected]>
jcp-runtime: Gate a call on its argument's propagated provenance
The lattice defines flow labels and jcp-flow computes them; this is the check that acts on one. A new check 6c in the reference monitor tests the argument's label against the grant's flow ceiling and the resource's taint floor, so a value derived from an untrusted read can be refused from a capability even though its literal bytes match no rule — the question ContentContains cannot ask. 6c sits after capability and before caveats and budget, each placement load- bearing. After capability, because the ceiling lives inside the grant and judging flow before the signature verifies would seal a verdict over attacker-chosen policy. Before budget, because a tainted call must cost zero joules — the same invariant the tool-poisoning denial already holds, now extended to derived taint. The facts argument distinguishes three cases on purpose. Absent facts (an un-migrated caller) under a ceiling fail closed; a ceiling-free grant with absent facts is untouched, which is every grant that exists today; clean facts pass any ceiling and are byte-identical to the plain path. So refinement holds — the gate only ever denies more, never allows what JCP denies now. Three couplings make the wedge real rather than decorative. A TaintFloor mirrors trust roots: an operator refusal of given origins on a resource that no grant carries and no delegated ceiling can widen. A tainted sub-budget gives work that has touched an untrusted source a tighter wallet, pre-checked at authorize so it denies before the tool runs and depleted by charge_flow on the allow path, tracked apart from the main budget it never converts into. And the chain path mirrors 6c against the attenuation-fold of every link's ceiling, so a delegated call is at most as permissive as its strictest hop and also fails closed without facts. The tainted budget rides inside FlowCeiling (from the FlowBelow caveat) rather than a new Grant field, so the grant encoding is untouched; a grant without a flow caveat is byte-identical to one minted before the feature, its content id and signature unchanged (pinned by a test). FlowFacts lives in jcp-schema as pure data so the runtime need not depend on the jcp-flow scan machinery for it. Eleven new tests: the deny-with-zero-debit core, the fail-closed law, the ceiling-free no-op, byte-identical clean calls, the floor refusal, the tainted sub-budget pre-check, and the chain form of each. Increment 3 of the FL-2 plan; the jcp/3 receipt seal and the gateway relay that computes the facts follow. Co-Authored-By: Claude Opus 4.8 <[email protected]>
jcp-flow: Compute propagated provenance from tool bytes
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]>
jcp-schema: Add the flow lattice — propagated provenance labels
JCP's Trust tag is a one-bit boundary marker: a frame is principal Control or untrusted Data, and a Data frame may never exercise a capability. That stops output from being executed as instructions, but it cannot express "this argument was derived from an earlier untrusted read" — the bit does not propagate through the values a run computes, so a laundering attack across tool calls slips past it. This is the propagated form: a Label is a point in a lattice over three axes — integrity (a total order, Untrusted up to Principal), the set of channel origins a value flowed from, and its readers. One join folds a derived value's label from its inputs; the order tells the runtime whether a value may pass a grant's ceiling. The direction is fixed once: bottom is the cleanest label (Principal, no origins, Public), and join moves toward more taint, so a ⊑ b reads "a is at least as clean as b" and taint on a derived value never drops below any input. The one-bit world is recovered exactly — Label::from(t).trust() == t, and the retraction is monotone, so refining a label never contradicts the tag and a clean run stays byte-identical. Sixteen lattice laws ship as tests rather than prose, because every layer above this — the runtime gate, the sealed verdict, replay — rests on the algebra being sound: idempotent/commutative/associative join with identity and absorbing top, the order agreeing with the join, a genuine partial order (antisymmetry needs the Any-saturation to be canonical, hence wire normalization), monotone join, no-laundering, downward-closed authorization, attenuation that can only narrow, and deterministic order-free digests. All collections are BTree, never Hash, so a label content-addresses identically on every platform. This is increment 1 of the FL-2 plan (FLOW_LATTICE_PLAN.md); the witness scan and its laws L17-L20, the runtime gate, the jcp/3 receipt seal, and the gateway relay follow in their own crates. Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21
jcp-bridge: Identify the authorization server per RFC 9207
The bridge validated the `iss` claim inside an access token (RFC 9068) but had no check on the authorization *response*. Those guard different steps. The token claim says who signed a credential already in hand, which only means something once that credential is trusted; RFC 9207 guards the step before it. In a mix-up attack a client is steered to a malicious authorization server that returns a code or token minted elsewhere. With no issuer on the response the client cannot tell the answer came from a different server than the question, and forwards the credential to the honest one. Comparing the response's `iss` against the server the request actually went to is what breaks that, and it is the check MCP's 2026-07-28 OAuth 2.1 hardening expects a client to perform. A missing `iss` fails closed rather than being read as benign: a response that cannot name its issuer proves nothing about its origin, which is precisely the condition the check exists to rule out. Decoding is the caller's job and is documented as such. The parameter arrives in a redirect URI query where a did:key issuer's colons are percent-encoded, and guessing at decoding here would silently compare an encoded string against a decoded issuer — a test pins that a still-encoded value is refused rather than quietly accepted. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pin the toolchain to Rust 1.97
Match the fleet's current stable (agent-joule moved to 1.97.0 too), so a standalone jouleclaw build and a build as an agent-joule dependency agree. Co-Authored-By: Claude Opus 4.8 <[email protected]>
jouleclaw-runtime: score_binary — one-forward-pass token scoring for reranking
Add score_binary / score_binary_tokens: prefill a prompt and return the softmax probability of one token vs another as the next token — the mechanism of a cross-encoder reranker (Qwen3-Reranker and friends format query+document into a yes/no question and read P(yes)). One forward pass, no generation. - Uses the lfm2-aware cached step builder (run_inplace_step_cached), so it scores hybrid conv+attention models too (the plain run_inplace_step assumes every block is attention and dies on a conv layer). - score_binary_tokens takes pre-tokenized input so a reranker prompt's chat special tokens (<|im_start|>/<|im_end|>) can be encoded atomically — plain BPE would shatter them and the model would score garbage. - Reads the last-position logits as the final vocab window, robust to whether the step returns per-position or last-only logits. Verified with Qwen3-Reranker-0.6B (real_reranker.rs, ignored): relevant document 0.300 vs irrelevant 0.120. Note: the reranker template must omit the reasoning <think> block for the 0.6B GGUF. Co-Authored-By: Claude Opus 4.8 <[email protected]>