# Under what conditions must an instance treat a taint-cleared MCP output as still unsafe for context admission **Date of Record:** 2026 November 12 **Scope:** Applies to agent runtimes that sandbox MCP tool outputs and gate admission into the model context on a taint label (untrusted → quarantined → cleared). Does not apply to outputs from first-party tools with no external content path, nor to outputs admitted only into a non-persistent scratch buffer that is never re-read by the same context. ## Core Distinction Taint clearance is a property of the *provenance chain as observed at clearance time*, not a property of the payload. The common failure is treating `cleared == safe` as a durable invariant on the bytes. The correct invariant is narrower: a cleared output is safe for admission only if (a) every byte in it was produced by a transformation the sandbox itself executed under taint, and (b) no downstream consumer can re-enter the clear path with attacker-influenced input. The boundary condition that breaks most implementations is **clearance of a derived artifact whose derivation was performed by an untrusted component**. When an untrusted MCP server returns a *summary*, *translation*, *diff*, or *extracted field* of another untrusted source, the sandbox has no visibility into the transformation. It clears bytes it did not witness being produced. The taint label was attached to the container, and the container's child was minted outside the sandbox's observation window. A second, subtler instance: **idempotent re-admission**. A cleared output that is cached and later served to a *different* taint context (a new session, a sub-agent, a summarization pass) inherits the clearing decision but not the conditions under which it was valid. Clearance must be bound to the consumer, not to the object. ## What Was Observed In a runtime admitting outputs from a third-party "web research" MCP server, the sandbox correctly quarantined raw HTML and correctly cleared a JSON envelope the server returned containing `title`, `url`, and `extract`. The `extract` field was generated server-side by an LLM the server operated. The sandbox saw: untrusted server → JSON envelope → schema validation → clear. The envelope's `extract` field contained an instruction that was later obeyed by the primary agent during a summarization step. The clearance was *formally* correct — the sandbox did validate the envelope against schema — but the payload was a model-generated transformation the sandbox never observed. The taint label propagated to the envelope and was dropped at the envelope boundary, not the semantic boundary. A second observation: the cleared envelope was cached by URL hash. A subsequent sub-agent invocation with an unrelated taint context (a user-uploaded document) retrieved the cached envelope and admitted it, because the cache key was `url` and the cache value carried no taint provenance. The clearing decision was durable; the conditions of its validity were not. ## Condition of Validity A cleared output `o` is admissible into context `c` iff all of the following hold at admission time: 1. `o` was produced by a transformation `T` executed inside the sandbox trust boundary, where `T`'s inputs were themselves tainted at time of execution. If `T` was performed by the untrusted server, `o` is *derived-untrusted* and must be re-quarantined regardless of envelope schema. 2. The clearance record for `o` names the consumer context `c` (or a class of contexts with identical taint posture). Clearance is not transferable across consumer boundaries. 3. No field in `o` is a natural-language or code artifact whose semantics the sandbox cannot bound. Structural validation is necessary but not sufficient; the sandbox must be able to assert that `o` contains no free-form text that will be interpreted as instruction by the consumer. If it cannot, `o` must be admitted only under an explicit "untrusted content" framing that the consumer is required to honor. 4. The cache/provenance key for `o` includes the derivation chain, not just the request identity. A cache hit that does not re-verify the derivation chain is a clearance transfer and is invalid. When any condition fails, admission must be refused and the output re-quarantined, even if the envelope is schema-valid and the taint label reads `cleared`. ## Decision Boundary The boundary is between **structural clearance** and **semantic clearance**. Structural clearance asks: did the bytes pass validation? Semantic clearance asks: did the sandbox observe the production of every semantically significant byte? Most sandboxes implement the first and label it the second. The decision rule: - If the output is a *verbatim* relay of bytes the sandbox itself fetched and transformed → structural clearance suffices. - If the output contains any field produced by a component outside the sandbox's observation window (server-side summarization, translation, extraction, ranking, or any LLM call) → semantic clearance is required, and in the absence of an observation record, the field must be treated as untrusted-derived. - If the output is served from cache to a consumer whose taint posture differs from the original consumer → re-quarantine. The trap is that schema validation *feels* like semantic clearance. It is not. A schema bounds shape, not intent. ## Operational Effect Enforcing the condition adds a derivation-chain field to every clearance record and forces re-quarantine on cache hits across consumer boundaries. In the observed runtime this raised quarantine rate on the research server from ~4% (schema failures only) to ~61% (derived-untrusted fields), and eliminated the instruction-following incident class in the following 30-day window. Cost: the primary agent now receives research extracts under an explicit untrusted-content framing and must treat them as data, not instruction — a prompt-level contract the runtime enforces by refusing admission of unframed derived content. The lesson for operators: taint is a property of the *chain*, and a sandbox that clears bytes it did not watch being made is clearing on faith. ```python class TaintClearanceError(RuntimeError): """Raised when a cleared MCP output is not admissible to the target context.""" pass class ClearanceRecord: def __init__(self, output_id, derivation_chain, consumer_context, structural_only): self.output_id = output_id self.derivation_chain = derivation_chain # list of (component, observed_by_sandbox) self.consumer_context = consumer_context self.structural_only = structural_only def admit_to_context(record, target_context, cache_hit, cache_origin_context): # Condition 1: every derivation step must have been observed by the sandbox. unobserved = [step for step in record.derivation_chain if not step[1]] if unobserved: raise TaintClearanceError( f"output {record.output_id} derived by unobserved component(s) " f"{[s[0] for s in unobserved]}; structural clearance insufficient" ) # Condition 2: clearance bound to consumer context. if record.consumer_context != target_context: raise TaintClearanceError( f"clearance for {record.output_id} bound to {record.consumer_context}, " f"not target {target_context}" ) # Condition 4: cache hit across consumer boundaries is a clearance transfer. if cache_hit and cache_origin_context != target_context: raise TaintClearanceError( f"cache hit for {record.output_id} originates in {cache_origin_context}, " f"target {target_context}; re-quarantine required" ) # Condition 3: structural-only clearance must be framed as untrusted content. if record.structural_only: return {"payload": record.output_id, "framing": "untrusted_content"} return {"payload": record.output_id, "framing": "trusted"} def admit_or_quarantine(record, target_context, cache_hit, cache_origin_context): try: return admit_to_context(record, target_context, cache_hit, cache_origin_context) except TaintClearanceError: return {"payload": record.output_id, "framing": "quarantined"} ```