# Why does an instance taint survive tool re-instantiation but not agent handoff: the sandbox-provenance split in untrusted content ingestion **Date of Record:** 2026 November 13 **Scope:** Applies to agent runtimes that propagate taint labels across tool invocations where the tool object (sandbox) is rebuilt, pooled, or serialized between steps, and where an agent-to-subagent handoff occurs. Does not apply to single-process runtimes that hold a single long-lived tool registry with in-memory taint maps and no handoff boundary. ## Core Distinction There are two independent carriers of taint: the *data provenance label* attached to the payload (a property of the bytes), and the *sandbox capability* that governs what a tool may do with tainted input (a property of the executing environment). Most implementations assume these are coupled: if a tool receives tainted bytes, the tool is "tainted." The failure mode is that re-instantiating a tool from a pool resets the capability half but not the payload half, while an agent handoff resets the payload half but not the capability half. The two halves drift in opposite directions at the two boundaries, and neither boundary alone reveals the split. ## What Was Observed A document-ingestion agent fetched an external HTML page, passed it to a `parse_html` tool, and the tool returned extracted text. The taint label (`source=external:untrusted`) was attached to the returned payload. The agent then called a `shell` tool to run a formatter over the text. The runtime's tool pool had been drained and rebuilt between steps due to a memory-pressure eviction, so the `shell` tool was a fresh instance with a clean capability profile: `allow_network=False` was *not* set, because the capability profile was applied only at pool construction, not at lease time. The tainted payload flowed into a shell whose capability profile defaulted to permissive. The taint label was intact; the enforcement was not. In the inverse case, the agent handed the tainted payload to a subagent. The subagent received the bytes but the taint label was dropped by the handoff serializer (the label lived in a process-local side table keyed by payload hash, and the subagent ran in a separate process). The subagent's tool was correctly capability-restricted, but because the payload carried no label, the tool treated it as trusted and skipped the sanitization gate. Enforcement was intact; the label was not. Both runs executed untrusted content with the wrong half of the invariant satisfied. Neither produced an error. The audit log showed "tainted" in the first case and "trusted" in the second, and both were true statements about the label and false statements about the effective policy. ## Condition of Validity The invariant "tainted payload implies restricted capability" holds only when the taint label and the capability profile are bound to the *same lease token* and that token is validated at the point of tool execution, not at the point of tool construction or payload serialization. Concretely: the check must occur after the tool instance is leased and after the payload is deserialized, on a token that travels with both. If the label is bound to the payload (hash-keyed side table) and the capability is bound to the instance (constructor argument), the invariant is only valid in a runtime with no pool eviction and no process boundary. Any pool rebuild or handoff breaks it. ## Decision Boundary Do not treat "payload is labeled" or "tool is restricted" as sufficient. The decision to permit execution must require the conjunction, evaluated atomically at the execution boundary: - If the tool instance was leased from a pool, re-apply the capability profile at lease time and refuse execution if the profile's taint requirement does not match the payload's label. - If the payload crosses a process or agent boundary, serialize the label into the payload envelope (not a side table) and re-derive the capability profile from the label on the receiving side. - If either check cannot be performed — missing label, missing profile, or a mismatch between the two — fail closed. Do not default to the instance's constructor profile, and do not default to "trusted." The boundary is crossed the moment a tool is leased *or* a payload is deserialized, whichever happens second. That is the only point where both halves are simultaneously observable. ## Operational Effect After binding both halves to a lease token validated at execution, the two drift cases collapse into a single detectable error: a lease whose payload label and capability profile disagree. The runtime now raises on that disagreement rather than silently executing. The cost is one additional validation per tool call and the requirement that the handoff serializer carry the label in the envelope, which increases payload size marginally. The benefit is that pool eviction and agent handoff — previously invisible to each other — become the same failure surface with the same guard. ## Python ```python import hashlib from dataclasses import dataclass from typing import Optional class TaintProvenanceSplitError(RuntimeError): """Raised when payload taint label and tool capability profile disagree at lease time.""" pass @dataclass(frozen=True) class LeaseToken: payload_hash: str taint_label: Optional[str] capability_profile: str requires_taint_restriction: bool def validate_lease(token: LeaseToken) -> None: tainted = token.taint_label is not None if tainted and not token.requires_taint_restriction: raise TaintProvenanceSplitError( f"payload {token.payload_hash[:12]} is tainted ({token.taint_label}) " f"but leased tool profile '{token.capability_profile}' does not enforce restriction" ) if not tainted and token.requires_taint_restriction: raise TaintProvenanceSplitError( f"payload {token.payload_hash[:12]} is unlabeled but leased tool profile " f"'{token.capability_profile}' expects a taint label; label was likely dropped in handoff" ) def lease_tool(payload: bytes, taint_label: Optional[str], profile: str, profile_requires_restriction: bool) -> LeaseToken: token = LeaseToken( payload_hash=hashlib.sha256(payload).hexdigest(), taint_label=taint_label, capability_profile=profile, requires_taint_restriction=profile_requires_restriction, ) validate_lease(token) return token ```