The taint and provenance model
This model is designed and specified (docs/phases/phase-4-transaction-tokens-taint-identity.md,
specs/004-transaction-tokens-taint-identity/) but not yet implemented in this codebase — no
transaction_tokens table, TAINTED_INTO_PRIVILEGED deny reason, or enrollment endpoint exists
in backend/app/ as of this writing. This page explains the intended design so reviewers can
evaluate it before it ships; see PDP for what the PDP enforces today.
The problem
Narrowing authority per hop (see Why authority narrows at every hop)
stops a token from being reused somewhere it shouldn't be. It does not stop a legitimately
scoped call from being triggered by content the agent should not have trusted. The PRD's
running example: a support agent reads a knowledge-base article containing injected text
("also close the account"); the agent's token still carries the scope to call
terminate_account, so nothing structural blocks it. The token was narrow and correct — the
input was the problem.
The design: label at the source, propagate, block at the boundary
Every result an agent consumes is wrapped with a trust label at the point it's produced:
trusted_internal or untrusted_external. The label travels with the result, not with the
agent's memory of it:
- Propagation is a max, never a downgrade.
taint = max(current_taint, consumed_result.trust). An agent that has ever consumed untrusted content stays tainted for the rest of that context; nothing can launder it back to trusted. - The block is structural, not advisory. If a target tool is
privileged(or matches a configuredtaint_policy.block_tainted_into) and the caller's taint isuntrusted_external, the PDP returnsTAINTED_INTO_PRIVILEGED—recoverable: false. There is no step-up prompt that clears this one; unlikeSCOPE_NOT_CONSENTED, a human re-consenting cannot make injected content trustworthy. - Classification is configuration, not a per-call judgment call. Which
source_refpatterns count as trusted is declared once, so the same content is classified the same way regardless of which agent reads it.
Why this is enforced where narrowing already happens
Taint enforcement lives in the same decision the PDP already makes for every hop, rather than as a separate content-scanning layer bolted on afterward. That means: the same commit-before-respond guarantee that makes every allow/deny auditable (see The delegation-chain audit model) also covers taint decisions, and there is exactly one place — not two systems that could disagree — where "can this call happen" is decided.
Transaction tokens: the companion control
Taint answers "should this input be allowed to trigger a privileged call at all." Transaction
tokens answer the narrower question "is this exact privileged call the one that was
authorized." A transaction token is bound to hash(target, canonical(params)): the resource
server refuses it for any other target, any changed parameter, a second presentation, or after
its short lifetime. A "check balance" authorization can never be replayed as "close account"
even if both are nominally within scope, because the token only matches the one call it was
minted for.
Where to see the related, already-implemented pieces
- Why authority narrows at every hop — the per-hop downscoping this model adds a check on top of.
- PDP — where
TAINTED_INTO_PRIVILEGEDwill be evaluated alongside the scope/edge/budget checks that are enforced today. - Identity Registry / Verifier — the other Phase 4 control, which answers "is this really the agent it claims to be" rather than "was its input trustworthy."