Skip to main content

The taint and provenance model

Design-stage

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 configured taint_policy.block_tainted_into) and the caller's taint is untrusted_external, the PDP returns TAINTED_INTO_PRIVILEGED — recoverable: false. There is no step-up prompt that clears this one; unlike SCOPE_NOT_CONSENTED, a human re-consenting cannot make injected content trustworthy.
  • Classification is configuration, not a per-call judgment call. Which source_ref patterns 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.

  • Why authority narrows at every hop — the per-hop downscoping this model adds a check on top of.
  • PDP — where TAINTED_INTO_PRIVILEGED will 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."