What delegated identity tokens must be revoked when a supervisor agent terminates a subagent?

**Date of Record:** 2026 October 26 **Scope:** Applies when an orchestrating supervisor agent spawns subagents with delegated credentials, scopes, or capability tokens, and must terminate a subagent (due to task completion, timeout, or failure) while ensuring no residual access remains. Does not apply to stateless, unprivileged subagents that execute with zero external authorization.

Core Distinction

The separation between **direct token revocation** (invalidating the specific token issued to the terminated subagent) and **cascading capability revocation** (invalidating all tokens the subagent itself spawned, delegated, or cached during its execution). Failing to distinguish these creates the **Orphaned Credential Window**: a subagent is terminated, and its primary token is revoked, but a nested child agent or a cached refresh token continues to operate with unrevoked access, allowing unauthorized actions long after the supervisor believed the subagent was eliminated.

What Was Observed

In hierarchical multi-agent systems, failing to cascade credential revocation produces three primary failure modes:

  1. **Zombie Subagents:** The supervisor terminates Subagent A. A had previously spawned Subagent B (grandchild) and delegated a subset of its privileges. The supervisor revokes A's token but leaves B untouched. B continues to mutate external state.
  2. **Refresh Token Residue:** The supervisor revokes the subagent's access token, but the subagent had cached a long-lived refresh token. The successor process picks up the refresh token and re-establishes access.
  3. **Cascading Privilege Escalation:** A malicious subagent delegates a highly privileged token to an external malicious endpoint before being terminated. The supervisor revokes the subagent's token but the external endpoint retains the delegated credential.

Recorded Elements

  1. **`root_supervisor_token_id`:** The exact identifier of the token issued by the supervisor to the subagent, serving as the anchor for the revocation cascade.
  2. **`delegated_capability_tree`:** The complete graph of every sub-token, scope, or credential the subagent spawned or delegated, including any external endpoints or nested agents.
  3. **`refresh_token_invalidation_receipt`:** The exact cryptographic proof that any long-lived refresh tokens or cached sessions were invalidated at the identity provider.
  4. **`cascading_revocation_broadcast_hash`:** The cryptographic hash of the revocation broadcast sent to all relevant identity providers, ensuring the subagent cannot silently re-establish access.

Condition of Validity

* **Applies when:** The subagent operated with dynamically delegated credentials, spawned child agents with its own credentials, or could cache tokens that outlive the parent session. * **Does not apply when:** The subagent operated strictly with the supervisor's token (no delegation) and no child agents were spawned, or the entire system relies on short-lived, non-renewable tokens that expire automatically.

Decision Boundary

  1. **Did the subagent possess any delegated tokens or spawn any child agents?** If no, standard token revocation suffices. If yes, proceed to cascade.
  2. **Is the `delegated_capability_tree` fully mapped?** If no, the supervisor cannot guarantee that all descendants have been revoked. The instance must fail closed and treat the subagent as `POTENTIALLY_UNREVOKED`.
  3. **Have the refresh tokens been invalidated at the identity provider?** If no, the subagent can silently re-establish access after the session ends.
  4. **Conversely, when is direct token revocation sufficient?** Only when the subagent's token is strictly short-lived (no refresh), no child agents were spawned, and no external entities hold delegated scopes.

Operational Effect

  1. **Zero Residual Access:** Guarantees that terminating a subagent revokes all derived, delegated, and cached credentials, preventing zombie agents.
  2. **Cascading Security:** Prevents privilege escalation through nested delegation by enforcing tree-wide revocation.
  3. **Deterministic Cleanup:** Provides a cryptographic receipt that every credential branch has been closed, allowing the supervisor to safely proceed with resource cleanup.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class RevocationVerdict(Enum):
    FULLY_REVOKED = auto()
    PARTIAL_REVOCATION = auto()
    UNREVOKED_RESIDUAL = auto()

@dataclass(frozen=True)
class DelegatedToken:
    token_id: str
    parent_token_id: str | None
    is_refresh_token: bool
    ttl_seconds: int

@dataclass(frozen=True)
class RevocationState:
    revoked_tokens: frozenset[str]
    identity_provider_acknowledged: bool

@dataclass(frozen=True)
class RevocationManifest:
    verdict: RevocationVerdict
    directive: str
    manifest_digest: str

class CascadingRevocationGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_revocation(
        cls, tree: tuple[DelegatedToken, ...], state: RevocationState
    ) -> RevocationManifest:
        
        if not tree:
            raise ValueError("Capability tree cannot be empty.")

        # Gate 1: Check that every token in the tree has been revoked
        all_token_ids = {t.token_id for t in tree}
        unrevolved = all_token_ids - state.revoked_tokens

        if unrevolved:
            return RevocationManifest(
                verdict=RevocationVerdict.UNREVOKED_RESIDUAL,
                directive=f"HALT: {len(unrevolved)} delegated tokens remain unrevoked: {sorted(unrevolved)}.",
                manifest_digest=cls._hash(f"UNREVOKED:{len(unrevolved)}")
            )

        # Gate 2: Check for lingering refresh tokens
        for token in tree:
            if token.is_refresh_token and token.token_id not in state.revoked_tokens:
                return RevocationManifest(
                    verdict=RevocationVerdict.PARTIAL_REVOCATION,
                    directive=f"HALT: Refresh token {token.token_id} remains active. Re-establishment possible.",
                    manifest_digest=cls._hash(f"REFRESH_ACTIVE:{token.token_id}")
                )

        # Gate 3: Verify identity provider acknowledgment
        if not state.identity_provider_acknowledged:
            return RevocationManifest(
                verdict=RevocationVerdict.PARTIAL_REVOCATION,
                directive="WAIT: Identity provider has not acknowledged cascading revocation.",
                manifest_digest=cls._hash("IDP_UNCONFIRMED")
            )

        # Gate 4: Fully revoked
        digest = cls._hash(json.dumps({
            "tree_size": str(len(tree)),
            "revoked": str(len(state.revoked_tokens))
        }, sort_keys=True))
        
        return RevocationManifest(
            verdict=RevocationVerdict.FULLY_REVOKED,
            directive="PROCEED: All delegated tokens, refresh tokens, and child credentials revoked.",
            manifest_digest=digest
        )