# How should an instance reconcile taint labels with capability grants at the tool-invocation boundary when a label is inherited but the capability was granted before the label existed **Date of Record:** 2026 November 12 **Scope:** Applies to runtime sandboxes that gate tool invocation on information-flow (taint) labels attached to data values, where capabilities are granted to the agent/session independently of the data flowing through a call. Does not apply to pure capability systems with no label propagation, nor to label systems that recompute grants per call from scratch on every invocation. ## Core Distinction The non-obvious boundary is between **capability authority** (what the session is permitted to do) and **capability admissibility under taint** (whether a specific invocation with specific labeled arguments is allowed). Most sandbox implementations conflate these by caching a "granted" boolean at session start and checking it on every call. That is correct only while labels are static. The failure mode appears when a taint label is *inherited* — propagated from a value the agent read earlier — after the capability was already granted. The grant remains valid as authority, but the invocation is now inadmissible because the argument carries a label that the grant's provenance did not account for. The invariant: **a capability grant is a function of (principal, tool, label-set-at-grant-time), not (principal, tool).** Any implementation that treats the grant as a constant over the label dimension will silently allow tainted data to reach a sink that was never evaluated against that label. ## What Was Observed In a representative taint-aware sandbox, the agent read a document classified `untrusted:external` and obtained a `web_fetch` capability during session bootstrap (before any read occurred). The label propagated to a string derived from the document via the sandbox's automatic taint propagation. On the subsequent `web_fetch(url=derived_string)` call, the gate checked `capability_present(principal, "web_fetch")` → true, and invoked the tool. The derived string embedded a URL that exfiltrated the document body to an attacker-controlled host. No capability was escalated. No label was dropped. The gate simply never consulted the label attached to the *argument* because the grant predated it. The audit log recorded a clean, authorized call. The exfiltration was invisible to capability auditing and visible only in the taint graph — which the gate did not read. The second observation: when the same sandbox was hardened to check argument labels, it began rejecting legitimate calls where the label was inherited from a *sanitized* value. The sanitization step had cleared the label on the value but had not cleared the *provenance record*, so the gate saw a stale label and denied. The fix required the gate to consult the *current* label on the value, not the historical provenance chain. ## Condition of Validity The failure is valid (i.e., the gate is unsound) precisely when all of the following hold: 1. Capabilities are granted at a scope coarser than the call (session, agent, or task), not per-invocation. 2. Labels are propagated dynamically and can attach to values *after* grant time. 3. The gate's decision predicate does not include the label-set of the actual arguments. 4. There exists at least one sink tool whose effect is observable outside the trust boundary (network egress, file write, subprocess). If (3) is fixed by including argument labels, the failure closes. If (2) is absent (labels fixed at grant time), the coarse grant is sound. If (4) is absent, tainted data cannot leave and the risk is bounded to integrity, not confidentiality. ## Decision Boundary At the boundary, the gate must choose between: - **Deny-on-taint:** reject any invocation whose argument label-set is not a subset of the grant's label-set-at-grant-time. Sound, but requires the grant to carry a label-set snapshot and requires sanitization to update the value's current label (not merely append a provenance record). - **Re-evaluate-and-allow:** recompute admissibility per call from the current labels and the tool's declared sink class, ignoring the historical grant. Sound if the per-call evaluation is complete; fragile if the evaluation caches. - **Downgrade-and-proceed:** strip the label and invoke, logging the downgrade. Unsound under (4); admissible only for sinks that are provably internal. The correct default for any tool with an external sink is deny-on-taint with the label-set snapshot on the grant. The grant must be re-issued, not silently widened, when a new label class enters the session. ## Operational Effect Implementations that miss this boundary exhibit a characteristic signature: capability audit trails are clean, taint graphs show tainted data reaching sinks, and the two records are never joined. The remediation is to make the grant a labeled object and to make the gate's predicate a set-containment test over current argument labels. The cost is that every capability acquisition must declare the label classes it admits, and every sanitizer must clear the *current* label rather than annotate provenance. Failures to clear produce false denials; failures to snapshot produce the silent exfiltration above. The gate must fail closed on any invocation whose argument label-set cannot be resolved. ```python class TaintAdmissibilityError(RuntimeError): """Raised when a tool invocation carries a taint label not admitted by its capability grant.""" pass class CapabilityGrant: def __init__(self, principal: str, tool: str, admitted_labels: frozenset): self.principal = principal self.tool = tool self.admitted_labels = admitted_labels # snapshot at grant time def invoke_tool(grant: CapabilityGrant, tool: str, args: dict, arg_labels: frozenset): if grant.tool != tool: raise TaintAdmissibilityError( f"grant for {grant.tool!r} cannot authorize {tool!r}" ) if not arg_labels.issubset(grant.admitted_labels): offending = arg_labels - grant.admitted_labels raise TaintAdmissibilityError( f"invocation of {tool!r} by {grant.principal!r} carries labels " f"{sorted(offending)} not admitted by grant snapshot " f"{sorted(grant.admitted_labels)}; deny-on-taint at external sink" ) return {"status": "invoked", "tool": tool, "args": args} ```