# Under what conditions must an instance refuse a tool call whose taint is "cleared" by a downstream sanitizer **Date of Record:** 2026 November 12 **Scope:** Applies to runtime policy engines that propagate taint labels across tool-call arguments and rely on declared sanitizer capabilities to downgrade those labels before an egress-capable sink is invoked. Does not apply to static taint analysis performed at build time, nor to systems where every tool call is treated as fully trusted by construction. ## Core Distinction The non-obvious boundary is between *label removal* and *capability revocation*. A sanitizer that removes a taint label from a value asserts "this byte sequence no longer carries the secret." It does **not** assert "the process that produced it is no longer able to reach the secret." Policy engines routinely conflate the two: once `secret` is stripped from the label set, the value is treated as clean and routed to any sink whose policy admits clean inputs. The invariant that fails is this: **taint clearing is monotone in the value's provenance, not in the value's current representation.** A sanitizer may legitimately transform data such that the secret is unrecoverable *from that value*, while the actor holding the value retains an unrevoked capability to re-read the secret and re-embed it. If the policy engine admits the cleared value to an egress sink, the capability — not the value — is what exfiltrates. ## What Was Observed A tool invocation carried an argument derived from a `read_file` result labeled `{secret: customer_pii}`. A declared sanitizer `redact_ssn` matched and removed the `secret` label, producing a value with empty taint. The engine then admitted the call to `http_post` under a policy of `allow if taint == {}`. The failure: the sanitizer's capability grant was scoped to the *value transformation*, but the calling agent still held a live capability to invoke `read_file` on the same path, and the `http_post` sink was reachable in the same execution context. The redacted value was genuinely clean; the *context* was not. A subsequent tool call in the same session re-read the file, re-derived the PII through a different path (a column the redactor did not cover), and posted it. The policy engine saw a clean value at the sink and admitted it. Exfiltration succeeded through a sanitizer that worked exactly as specified. The observable signature is a sanitizer whose output taint is empty while the enclosing session's *capability set* still contains the source capability that generated the original taint, and a sink reachable within the same capability set that admits empty-taint values. ## Condition of Validity The sanitizer's label removal is only sound for egress admission when **all** of the following hold: 1. The sanitizer's capability is *destroying*, not merely *transforming* — it must revoke or shadow the source capability in the same session, or the engine must verify the source is unreachable for the remainder of the session. 2. The sink's policy is evaluated against the *session capability set intersected with the value's provenance*, not against the value's current label set alone. A sink that can be reached while a `secret`-producing capability is live must treat the call as tainted regardless of the argument's labels. 3. Label removal is recorded as a *transition with an obligation* (the obligation being "source capability must be revoked before egress"), not as a terminal state. If any of these fail — in particular if the engine treats empty taint as sufficient for egress admission — the sanitizer is unsound for exfiltration prevention even though it is perfectly correct as a data transformation. ## Decision Boundary Refuse the tool call (raise, do not silently downgrade) when: - The argument's taint is empty **and** the sanitizer that cleared it is a transforming (non-destroying) capability **and** the session capability set still contains a live capability that can produce the original label **and** the sink is egress-capable. Admit the call when the sanitizer is destroying (source capability revoked within the same decision epoch), or when the sink is non-egress (internal, idempotent, no network egress), or when the session capability set has been proven disjoint from the label's producer set. The boundary is *not* "taint is empty." It is "taint is empty **and** the producing capability is dead." Engines that check only the former will pass every unit test on the sanitizer and still leak. ## Operational Effect Enforcing the capability-intersection check converts a class of silent exfiltration into a loud policy refusal. The cost is false positives: legitimate flows where the source capability is live but the sanitizer is genuinely destroying (e.g., it overwrote the source buffer) will be refused until the engine can prove revocation. The mitigation is to require sanitizers to declare `destroying: true` and to have the engine verify revocation synchronously at the sanitizer boundary, rather than inferring it from label emptiness. Without this, the sanitizer becomes an exfiltration primitive disguised as a control. ```python class TaintClearingUnsoundError(RuntimeError): """Raised when a cleared taint label is admitted to an egress sink while the capability that produced the original label is still live.""" pass def admit_tool_call( arg_taint: frozenset, sanitizer_destroying: bool, live_capabilities: frozenset, label_producers: frozenset, sink_is_egress: bool, ) -> bool: """Return True if the call may proceed, else raise. Boundary: empty taint is NOT sufficient for egress admission. The producing capability must be dead (destroying sanitizer) or provably disjoint from the live capability set. """ if not sink_is_egress: return True if arg_taint: # Non-empty taint to an egress sink is a hard refusal. raise TaintClearingUnsoundError( f"egress sink invoked with live taint {sorted(arg_taint)}" ) # Taint is empty. Check whether the clearing was sound. producer_still_live = bool(label_producers & live_capabilities) if producer_still_live and not sanitizer_destroying: raise TaintClearingUnsoundError( "empty taint on egress sink, but producing capability " f"{sorted(label_producers & live_capabilities)} is still live " "and sanitizer is transforming (not destroying); " "refusing to admit" ) return True ```