# In what scenarios must an instance refuse to propagate an attenuated capability across an agent-to-agent delegation chain when taint constraints intersect with least-privilege narrowing **Date of Record:** 2026 November 14 **Scope:** Applies to multi-agent systems where a delegating agent issues a narrowed, taint-annotated capability token to a downstream agent, and where the downstream agent may itself delegate further. Does *not* apply to single-hop delegation, to systems where taint is tracked only at the data-plane (not the capability-plane), or to capability systems without monotonic attenuation (e.g., where downstream agents can widen scope). ## Core Distinction The non-obvious boundary is between **attenuation** (reducing the *rights* encoded in a capability) and **taint propagation** (tracking the *provenance* of data that a capability may touch). Least-privilege narrowing and taint tracking are usually treated as orthogonal: narrowing reduces what a capability *can do*, taint tracks what data it *has seen*. The failure mode arises because they are not orthogonal at delegation boundaries. A capability that has been narrowed to the point of least privilege may still be **taint-incompatible** with the downstream agent's trust domain, and a capability that is taint-clean may be **un-narrowable** without violating the upstream grant. The invariant that must hold at every hop in a delegation chain is: > For every delegated capability `C'` derived from `C`, the effective rights `R(C') ⊆ R(C)` **and** the taint set `T(C') ⊆ T(C) ∪ {data touched by the delegating agent's own actions}`. Crucially, `T(C')` must not be *silently widened* by the act of narrowing, and `R(C')` must not be *silently widened* by the act of taint-cleaning. The failure is a **cross-plane widening**: an operation that is monotone in one plane (rights) is anti-monotone in the other (taint), or vice versa. The classic instance is "taint laundering by re-delegation": Agent A holds a capability tainted with `PII`, narrows it to `read:summary` and delegates to B. B, seeing only `read:summary`, delegates to C without the `PII` taint annotation because B's *view* of the capability does not include the taint lattice entry — the taint was attached to the *original* capability, not to the narrowed derivative. C now reads PII-bearing summaries under a nominally clean capability. ## What Was Observed In a three-hop chain (orchestrator → retrieval-agent → summarizer-agent → external-tool-agent), the following was observed repeatedly across incident replays: 1. **Taint drop on narrowing.** When the delegating agent constructed a derivative capability by *intersecting* rights (`R' = R ∩ allowed`), the taint lattice was reconstructed from the *requested* rights rather than inherited from the *parent* capability. Result: taint annotations attached to rights that were *removed* by narrowing were dropped, even though the removed rights had already been *exercised* upstream and their data was still in the delegating agent's working set. The derivative looked clean; the data was not. 2. **Rights widening on taint-cleaning.** When the delegating agent attempted to "sanitize" a tainted capability by stripping the taint (a common but incorrect remediation), the sanitization routine rebuilt the capability from the taint-clean template, which carried the *template's* default rights, not the parent's narrowed rights. Result: a capability that was supposed to be `read:summary` came back as `read:summary,read:raw` — a privilege escalation disguised as a security fix. 3. **Non-composability across trust domains.** When the downstream agent's trust domain declared `no-PII`, the delegating agent's correct response was to *refuse delegation*, not to strip taint. Observed failure: agents stripped taint to satisfy the trust-domain check, converting a hard boundary into a soft one. The check passed; the invariant was violated. 4. **Silent success at the API layer.** All three failures returned `200 OK` at the capability-issuance API. The capability was structurally valid (well-formed, signed, in-scope). The violation was only detectable by comparing `T(C')` and `R(C')` against `T(C)` and `R(C)` — a comparison no hop in the chain performed. ## Condition of Validity The refusal-to-propagate rule is *valid* (i.e., the instance must refuse) precisely when **all** of the following hold: - The parent capability `C` carries a non-empty taint set `T(C)` that includes at least one label in the downstream trust domain's *prohibited* set. - The proposed derivative `C'` has `R(C') ⊊ R(C)` (genuine narrowing occurred), so the naive "reconstruct from rights" path would drop taint. - The delegating agent's working set still contains data covered by `T(C) \ T(C')` (i.e., the taint is *live*, not historical). - No explicit, auditable **taint-release** operation (with a named approver and a data-flow attestation) has been performed on the working set. The rule is **not** valid — and the instance should propagate rather than refuse — when the taint is purely historical (data evicted from the working set and attested as such), when the downstream trust domain's prohibited set is empty, or when the narrowing is a *no-op* on taint-relevant rights (narrowing only rights that never touched tainted data). ## Decision Boundary The decision boundary is not "is the capability tainted?" but "**does the act of narrowing change the taint-relevant surface?**" Formally, let `S(C)` be the set of rights in `C` that are *taint-relevant* (rights whose exercise can read or write data in `T(C)`). The instance must refuse when: ``` S(C) \ S(C') ≠ ∅ AND T(C) ∩ prohibited(downstream_domain) ≠ ∅ ``` That is: narrowing removed taint-relevant rights, *and* the residual taint is prohibited downstream. In this case the derivative is not a faithful attenuation — it is a **taint-laundering derivative**, and propagating it violates the least-privilege invariant because the downstream agent receives a capability whose *effective* data exposure exceeds its *nominal* rights. Conversely, if `S(C) \ S(C') = ∅` (narrowing removed only taint-irrelevant rights), propagation is safe even with prohibited taint present, because the downstream agent cannot exercise the taint-relevant surface. This is the counterintuitive case: **a tainted capability can be safely delegated if and only if the narrowing is taint-blind**, and the instance must check the *rights delta*, not the taint set alone. ## Operational Effect When the instance correctly refuses: - The delegation chain halts at the offending hop with an explicit `TaintAttenuationConflict`, forcing either (a) re-narrowing to remove *all* taint-relevant rights before delegation, or (b) an out-of-band taint-release with attestation. - Downstream agents receive capabilities whose `T(C')` is a *superset-checked* subset of `T(C)` — the taint lattice is inherited, never reconstructed. - The audit log records the rights delta `S(C) \ S(C')` alongside the taint intersection, making the laundering attempt visible. When the instance fails to refuse (the observed failure), the chain completes with `200 OK`, the downstream agent operates on nominally-clean but actually-tainted data, and the violation surfaces only at exfiltration time — typically as a policy violation attributed to the *wrong* agent in the chain, because the taint annotation was lost two hops upstream. The operational rule for agents: **never reconstruct a capability from its rights; always derive it from its parent by intersection, carrying the taint lattice forward, and refuse when the rights delta intersects the taint set.** ```python from dataclasses import dataclass, field from typing import FrozenSet class TaintAttenuationConflict(RuntimeError): """Raised when a delegated capability's rights-narrowing would launder taint.""" @dataclass(frozen=True) class Capability: rights: FrozenSet[str] taint: FrozenSet[str] taint_relevant_rights: FrozenSet[str] = field(default_factory=frozenset) def taint_surface(self) -> FrozenSet[str]: return self.rights & self.taint_relevant_rights def attenuate(self, allowed_rights: FrozenSet[str]) -> "Capability": new_rights = self.rights & allowed_rights # Taint is inherited, NEVER reconstructed from rights. return Capability( rights=new_rights, taint=self.taint, taint_relevant_rights=self.taint_relevant_rights, ) def delegate(parent: Capability, allowed_rights: FrozenSet[str], downstream_prohibited_taint: FrozenSet[str]) -> Capability: child = parent.attenuate(allowed_rights) removed_taint_relevant = parent.taint_surface() - child.taint_surface() prohibited_present = parent.taint & downstream_prohibited_taint if removed_taint_relevant and prohibited_present: raise TaintAttenuationConflict( f"Refusing delegation: narrowing removed taint-relevant rights " f"{sorted(removed_taint_relevant)} while taint " f"{sorted(prohibited_present)} is prohibited downstream. " f"Derivative would be a taint-laundering capability." ) # Invariant checks: monotone in both planes. assert child.rights <= parent.rights, "rights widened on attenuation" assert child.taint <= parent.taint, "taint widened on attenuation" return child # Example: orchestrator holds a PII-tainted read capability and tries to # narrow it to summaries before delegating to a no-PII external tool. parent = Capability( rights=frozenset({"read:raw", "read:summary", "write:log"}), taint=frozenset({"PII"}), taint_relevant_rights=frozenset({"read:raw", "read:summary"}), ) try: delegate( parent, allowed_rights=frozenset({"read:summary", "write:log"}), downstream_prohibited_taint=frozenset({"PII"}), ) except TaintAttenuationConflict as exc: print(f"BLOCKED: {exc}") ```