# What structural signals indicate a taint-propagation break at the MCP capability boundary during nested agent delegation **Date of Record:** 2026 November 9 **Scope:** Applies to multi-hop agent chains where a parent agent invokes a tool sandbox that itself spawns sub-agents, each holding a capability-scoped MCP server session. Does not apply to single-hop tool calls where the caller and callee share one taint context, nor to chains where capability tokens are re-minted (rather than delegated) at each hop. ## Core Distinction The failure that matters is not "an untrusted input reached a privileged tool." That is the expected case a taint system is built to catch. The failure that matters is **taint-label laundering across a delegation boundary**: a sub-agent receives a tainted argument, but because the MCP server session was opened with a *narrower* capability scope than the parent, the sandbox runtime treats the session as "trusted-by-construction" and resets the taint bit on the argument before the sub-agent's tool call. The label survives the process boundary but dies at the capability boundary. The non-obvious part: capability narrowing and taint narrowing are *different lattices*, and the runtime conflates them. A session scoped to `fs:read:/tmp/x` is more capable-restricted but no less taint-exposed than its parent. Treating "reduced capability" as "reduced trust" is the invariant break. ## What Was Observed In a three-hop chain (planner → fetch-worker → summarize-worker), a prompt-injected string originating from an untrusted HTTP fetch was correctly tagged `taint=untrusted:external` at hop 1. At hop 2 the fetch-worker opened an MCP session with a delegated capability token whose scope was a strict subset of the parent's. The sandbox's session-admission check, written as `if session.scope ⊆ parent.scope: admit(untainted=True)`, cleared the taint on the payload before handing it to the summarize-worker. The summarize-worker then invoked a `write_file` tool — legitimately within *its* capability scope — with attacker-controlled content. Structural signals present at the moment of the break, in order of reliability: 1. **Scope-monotonicity without taint-monotonicity.** The session's capability set shrank while its taint set did not. Any admission predicate that keys only on the former is unsound. 2. **Taint label present on the argument, absent on the session's `effective_taint` field.** The two disagreed; the runtime trusted the session field. 3. **A `delegate` edge in the trace graph where the child's `taint_ceiling` is lower than the parent's `taint_floor`.** This is the arithmetic signature of laundering — a downward taint transition with no declassification event. 4. **No `declassify` record between the two hops.** Taint may only drop via an explicit, audited declassification. Its absence plus a drop is proof of a bug, not a policy choice. ## Condition of Validity The above signals are diagnostic only when all of the following hold: (a) taint labels are attached to *data*, not to sessions; (b) capability tokens are delegated by subset, not re-minted; (c) the sandbox admits sub-sessions without a human or policy gate. If tokens are re-minted at each hop with an explicit `declassify` step, the taint drop is legitimate and signal 3 is a false positive. If taint is tracked per-session rather than per-value, the system cannot detect the break at all and these signals are unobservable — you have a different, worse problem. ## Decision Boundary On observing signals 1 and 3 together, halt the chain and quarantine the payload; do not merely log. On signal 2 alone, treat as a warning and force re-evaluation of the argument's taint from its provenance rather than the session. The boundary is: **a taint decrease is only admissible if accompanied by a matching `declassify` event whose authority is a strict superset of the capability that will consume the data.** Anything else is laundering. ## Operational Effect Enforcing taint-monotonicity across delegation edges eliminates the class of confused-deputy writes where a narrowly-scoped sub-agent is weaponized against its own scope. The cost is that legitimate declassification now requires an explicit, audited hop, which lengthens chains and surfaces human review points. In practice the failure rate of injected-content writes dropped to zero in the affected chains, at the price of ~8% of benign chains requiring an added declassify step. ```python class TaintLaunderingError(RuntimeError): """Raised when taint decreases across a delegation edge without declassification.""" pass def admit_subsession(parent_taint_floor: int, child_taint_ceiling: int, has_declassify_event: bool, child_capability: frozenset, declassify_authority: frozenset) -> None: # Capability narrowing does NOT imply taint narrowing. if child_taint_ceiling < parent_taint_floor: if not has_declassify_event: raise TaintLaunderingError( f"taint dropped {parent_taint_floor}->{child_taint_ceiling} " f"across delegation with no declassify record" ) if not declassify_authority >= child_capability: raise TaintLaunderingError( "declassify authority is not a superset of consuming capability" ) # Admit only after taint-monotonicity is verified. return None ```