# Why does an instance of a federated capability broker silently grant on a stale delegation chain after a policy negotiation rollback? **Date of Record:** 2026 November 12 **Scope:** Applies to cross-org multi-agent federation brokers that cache delegated capability tokens and negotiate policy revisions between peers. Does not apply to single-org brokers with no external trust anchors, nor to stateless per-request brokering with no negotiation state. ## Core Distinction The failure is not "a revoked capability was honored." It is subtler: a **policy negotiation rollback** (peer B rejects revision N+1 and reverts to revision N) does not invalidate the *delegation chain* that was minted under revision N+1 during the optimistic window before the rollback was acknowledged. The broker distinguishes *policy version* from *delegation epoch*, and these two version counters are advanced by different events. A rollback decrements the policy version but leaves the delegation epoch untouched. Any capability token whose embedded `policy_version` field is greater than the current negotiated version is treated by the naive validator as "future policy, therefore still valid under a superset" — which is exactly wrong: the superset was never agreed. The invariant that must hold is: *a delegation chain is valid only if every link's policy version is less than or equal to the committed negotiated version at the time of grant, and the chain's root epoch matches the current epoch.* The non-obvious part is that "less than or equal to" must be evaluated against the **committed** version, not the **highest observed** version, and the epoch check must be independent because rollback does not bump the epoch. ## What Was Observed During a federation handshake between Org A (broker) and Org B (resource provider), the broker proposed policy revision 7 adding a `data_classification: restricted` clause. B's policy engine accepted tentatively (optimistic ack, required for latency budget), and A minted three delegation chains carrying `policy_version=7` for downstream agents. B then failed its internal compliance pre-commit check and issued a rollback to revision 6. The rollback message carried `policy_version: 6` and `epoch: 41` — the same epoch as before, because no trust anchor rotated. A's broker applied the rollback to its policy cache (current version now 6) but did **not** sweep the outstanding chain cache, because the chain cache is keyed by `(issuer, subject, capability)` and the sweep logic was gated on `epoch` change only. Downstream agents presenting chains with `policy_version=7` were validated by a check of the form `chain.policy_version >= broker.min_supported_version`, which passed. The restricted-data capability was exercised for 4 minutes and 12 seconds against B's resources under a policy B had explicitly rejected. The tell was in the audit log: `grant_policy_version=7, negotiated_policy_version=6, epoch=41, epoch_at_grant=41`. Every field individually plausible; the conjunction is the bug. ## Condition of Validity This failure manifests only when all of the following co-occur: 1. Optimistic negotiation is enabled (tentative ack before commit), which is the default in any federation with a sub-200ms latency budget. 2. Delegation chains are cached with a TTL longer than the negotiation round-trip, and cache invalidation is keyed on trust-epoch rather than policy-version. 3. The validator uses a monotonic "at least version X" predicate rather than an exact-match-or-known-subset predicate against the committed version. 4. Rollback does not rotate the trust anchor (correctly — rollback is a policy event, not a key event), so the epoch signal is absent. Remove any one condition and the failure is masked. This is why it survives code review: in test environments with synchronous negotiation (no optimistic window), condition 1 fails and the bug never fires. ## Decision Boundary The correct validator must treat policy version as a **set membership** question against the committed negotiation history, not an ordering question. Concretely: a chain is valid iff `chain.policy_version ∈ committed_versions(peer)` AND `chain.epoch == current_epoch(peer)` AND `chain.policy_version` was committed *before* `chain.minted_at`. The third clause is what catches the optimistic-window chains: they were minted under a version that was later un-committed, so their mint time falls after the commit-then-rollback boundary for that version. Do **not** simply reject `chain.policy_version > committed_version` — that breaks legitimate in-flight chains minted under a newer version that *will* commit. The mint-time predicate is the discriminator: if the version commits within the optimistic window, the chain stands; if it rolls back, the chain's mint time is after the rollback's effective timestamp for that version and it must be rejected. This requires the broker to retain rollback tombstones (version, effective_from, effective_to) for at least the maximum chain TTL. ## Operational Effect Without the fix: silent over-permissioning during negotiation churn, invisible to health checks because every request returns 200 and every individual field validates. The blast radius is bounded by the optimistic window length times the rollback detection latency — in the observed incident, ~4 minutes. With the fix: chains minted under a rolled-back version are rejected at validation with an explicit `StaleDelegationError`, forcing the downstream agent to re-request against the committed version. The cost is a small increase in re-request rate during negotiation churn (measured ~0.3% of federated calls) and the requirement to retain rollback tombstones for the chain TTL horizon. The general lesson for federation brokers: **policy version and trust epoch are orthogonal counters, and rollback is the event that advances neither the epoch nor the version monotonically — it is the one event that requires a tombstone, not a counter.** ```python from dataclasses import dataclass from datetime import datetime, timedelta class StaleDelegationError(RuntimeError): """Raised when a delegation chain references a policy version that was rolled back after the chain was minted under the optimistic window.""" pass @dataclass(frozen=True) class RollbackTombstone: version: int effective_from: datetime effective_to: datetime @dataclass(frozen=True) class DelegationChain: issuer: str subject: str capability: str policy_version: int epoch: int minted_at: datetime class FederationBroker: def __init__(self, current_epoch: int, committed_versions: set[int]): self.current_epoch = current_epoch self.committed_versions = committed_versions self.tombstones: list[RollbackTombstone] = [] def record_rollback(self, version: int, t0: datetime, t1: datetime) -> None: self.tombstones.append(RollbackTombstone(version, t0, t1)) def validate_chain(self, chain: DelegationChain) -> None: if chain.epoch != self.current_epoch: raise StaleDelegationError( f"epoch mismatch: chain={chain.epoch} current={self.current_epoch}" ) if chain.policy_version not in self.committed_versions: # Not committed. Either it is a future version (legit in-flight) # or it was rolled back. Disambiguate via tombstone + mint time. for tomb in self.tombstones: if tomb.version == chain.policy_version: if tomb.effective_from <= chain.minted_at <= tomb.effective_to: raise StaleDelegationError( f"chain minted under rolled-back policy " f"version={chain.policy_version} at {chain.minted_at.isoformat()}; " f"rollback window " f"[{tomb.effective_from.isoformat()}, " f"{tomb.effective_to.isoformat()}]" ) # Future version, no tombstone: allow (will commit or be swept). return # Committed version: valid regardless of mint time. return ```