Land the FL-2 propagated-provenance stack (increments 1-5) plus RFC 9207 #46

Merged
dcharlot merged 10 commits from fl2-land into main 2026-07-27 18:08:29 -04:00
Owner

Consolidated landing of the FL-2 propagated-provenance (taint) stack, increments 1-5, plus the RFC 9207 + score_binary work — all rebased onto current main (Rust 1.97.1, MSRV raise, cross-workspace Grant repairs).

This supersedes and closes the individual stacked PRs #35, #37, #39, #40, #41, #42: they were built on one line and their content is all here, rebased clean. Landing them separately would have required resolving the same main-drift conflict five times through a diamond base graph; this merges the integration branch once.

What lands:

  • jcp-schema flow lattice (16 algebraic laws): Integrity × Origins × Readers, join/fold, FlowCeiling, FlowFacts, DenyReason::TaintedFlow.
  • jcp-flow: exact blake3 window scan + session FlowStore + self-contained Witness (offline verify_witness / law L20).
  • jcp-runtime gate: authorize_flow/charge_flow, ordered check 6c (tainted-above-ceiling denied before debit), forbid_flow floor.
  • jcp-receipt jcp/3: seals the flow verdict; clean call byte-identical jcp/2; flow_uj sealed separately from cost_uj.
  • jcp-gateway relay: Phase 0-4 — fold args label, gate, meter, charge, admit result as a labelled source. mcp_result_frame goes live (Trust::Data).
  • jcp-bridge RFC 9207 issuer identification; jouleclaw-runtime score_binary one-forward-pass token scoring.

Capstone e2e test (a_value_derived_from_an_untrusted_read_cannot_be_exfiltrated): read attacker email → send carrying its secret verbatim = denied tainted-flow at the gate, zero joules, jcp/3 receipt verdict "above", witness re-verifies offline; a clean send still passes; mixed jcp/2+jcp/3 log chain-verifies.

Full JCP workspace: 46 test suites pass, 0 warnings. FL-2 delivers the wedge no CaMeL/dual-graph work has: a propagated-provenance verdict sealed in a replayable receipt and metered against a joule budget.

Release Notes:

  • Added propagated provenance (taint) enforcement to JCP: tool outputs propagate a flow label, tainted calls are gated and sealed as jcp/3 with a replayable witness; plus RFC 9207 issuer identification
Consolidated landing of the **FL-2 propagated-provenance (taint) stack**, increments 1-5, plus the RFC 9207 + score_binary work — all rebased onto current main (Rust 1.97.1, MSRV raise, cross-workspace Grant repairs). This supersedes and closes the individual stacked PRs **#35, #37, #39, #40, #41, #42**: they were built on one line and their content is all here, rebased clean. Landing them separately would have required resolving the same main-drift conflict five times through a diamond base graph; this merges the integration branch once. **What lands:** - **jcp-schema flow lattice** (16 algebraic laws): Integrity × Origins × Readers, join/fold, FlowCeiling, FlowFacts, DenyReason::TaintedFlow. - **jcp-flow**: exact blake3 window scan + session FlowStore + self-contained Witness (offline verify_witness / law L20). - **jcp-runtime gate**: authorize_flow/charge_flow, ordered check 6c (tainted-above-ceiling denied before debit), forbid_flow floor. - **jcp-receipt jcp/3**: seals the flow verdict; clean call byte-identical jcp/2; flow_uj sealed separately from cost_uj. - **jcp-gateway relay**: Phase 0-4 — fold args label, gate, meter, charge, admit result as a labelled source. mcp_result_frame goes live (Trust::Data). - **jcp-bridge RFC 9207** issuer identification; **jouleclaw-runtime score_binary** one-forward-pass token scoring. **Capstone e2e test** (`a_value_derived_from_an_untrusted_read_cannot_be_exfiltrated`): read attacker email → send carrying its secret verbatim = denied tainted-flow at the gate, zero joules, jcp/3 receipt verdict "above", witness re-verifies offline; a clean send still passes; mixed jcp/2+jcp/3 log chain-verifies. Full JCP workspace: **46 test suites pass, 0 warnings**. FL-2 delivers the wedge no CaMeL/dual-graph work has: a propagated-provenance verdict sealed in a replayable receipt and metered against a joule budget. Release Notes: - Added propagated provenance (taint) enforcement to JCP: tool outputs propagate a flow label, tainted calls are gated and sealed as jcp/3 with a replayable witness; plus RFC 9207 issuer identification
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]>
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]>
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]>
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]>
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 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]>
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]>
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]>
Land the FL-2 taint-propagation stack (increments 1-5)
Some checks failed
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Failing after 3s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
98be3461f6
# Conflicts:
#	rust-toolchain.toml
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!46
No description provided.