# In what scenarios must an instance refuse delegated capability tokens that pass signature verification but violate transitive policy scope **Date of Record:** 2026 November 7 **Scope:** Applies to cross-org multi-agent federation where an agent in org A obtains a capability token from org B and re-delegates a narrowed subset to org C, and policy is evaluated at each hop rather than end-to-end. Does not apply to single-org delegation, or to federations where a central policy decision point re-evaluates the full transitive chain at each request. ## Core Distinction A delegated capability token carries two independent classes of assertion: *identity/authority provenance* (who signed it, what principal chain it descends from) and *policy scope* (what resource classes, time windows, and rate bounds the delegating org permits). Signature verification proves the former. It says nothing about the latter. The non-obvious failure is that a token can be cryptographically valid, correctly chained, and still authorize an action that the *original* issuer's policy forbids, because intermediate delegators in a federation do not, by default, carry forward the full policy predicate — they carry forward a *narrowed capability set* but recompute policy locally against their own rules. The invariant that is frequently assumed and frequently violated: **transitive policy scope is monotonically non-increasing across delegation hops.** When an intermediate agent re-issues a capability, the effective permission set must be a subset of every ancestor's permitted set, not merely a subset of the immediately delegating agent's set. Signature validity only guarantees the chain is authentic, not that the chain is scope-monotone. ## What Was Observed In a three-org federation (A issues, B brokers, C consumes), agent B received a capability from A scoped to `resource:dataset/*` with a policy predicate `classification <= internal`. B's own local policy permitted `classification <= public` for outbound delegation to unknown peers. B re-issued a token to C scoped to `resource:dataset/sales` — a genuine narrowing of the *resource* dimension — but B's re-issued token dropped the classification predicate entirely, because B's token format serialized only resource scope, not policy predicates. C then requested `dataset/sales` records tagged `confidential`. Signature verification passed at every hop. The request succeeded. A's policy was violated by a token A never saw and could not have signed. The failure is not an attack. No key was compromised, no signature was forged. The chain was authentic. The defect is that the policy predicate did not survive serialization across the B hop, and no hop performed end-to-end predicate intersection. This is a *representation* boundary, not a *cryptographic* one. A second observed variant: B correctly forwarded the predicate, but C's policy engine evaluated the predicate against C's local attribute authority, which classified `dataset/sales` as `public` while A's authority classified the same resource as `confidential`. Predicate forwarding is necessary but insufficient; the *attribute resolution context* must also be pinned to the issuing org's authority, or the predicate is evaluated against the wrong ground truth. ## Condition of Validity A delegated token is safe to honor only when all of the following hold: 1. The full policy predicate set from every ancestor hop is present in the token (not just the immediate delegator's). 2. Predicate evaluation resolves attributes against the *issuing* org's attribute authority, or the predicate is expressed in terms that are authority-independent (e.g., explicit resource identifiers, not org-local classification labels). 3. The receiving agent's policy engine computes the *intersection* of all ancestor predicates and rejects if any ancestor predicate is unevaluable (missing attribute, unknown authority, unresolvable label) rather than defaulting to permissive. Condition 3 is the one most often skipped. The common implementation defaults to "allow if no predicate is present," which converts a dropped predicate into an implicit full grant. A dropped predicate must be treated as a *failed* predicate, not an absent one. ## Decision Boundary Refuse the token — do not merely log and proceed — when any of these hold: - The token's predicate set is a strict subset of the delegating ancestor's predicate set and the missing predicates are not provably subsumed by remaining ones. - A predicate references an attribute authority not in the token's declared trust bundle. - A predicate is unevaluable at request time (unknown label, expired attribute assertion) and the default resolution is permissive. - The resource scope narrows but the policy scope widens or is absent — the two dimensions must narrow together or independently, never trade off. Do not refuse when the predicate set is identical across hops, or when a predicate is provably subsumed (e.g., `classification <= internal` subsumes `classification <= public` and the remaining predicate is the stricter one — verify direction, this is where sign errors live). ## Operational Effect Treating signature validity as sufficient for delegation acceptance produces silent policy widening that is invisible to audit logs, because every logged event shows a valid chain. The only detection is end-to-end predicate diffing at the consumer, which must be built explicitly. In practice, the fix is to make the token format carry the *accumulated* predicate set (append-only across hops, never replace) and to make the consumer's policy engine fail closed on any unevaluable predicate. The cost is larger tokens and stricter attribute-authority federation; the alternative is a federation that is cryptographically sound and semantically leaky. ```python class TransitiveScopeViolation(RuntimeError): """Raised when a delegated capability token is authentic but its accumulated policy predicates do not monotonically narrow across hops.""" pass def accept_delegated_token( token: dict, ancestor_predicates: list[dict], trust_bundle: set[str], attribute_resolver, ) -> None: token_preds = token.get("policy_predicates") if token_preds is None: # Absent predicate set is NOT an implicit grant. raise TransitiveScopeViolation( "token carries no policy predicates; cannot verify transitive scope" ) token_keys = {p["key"] for p in token_preds} for anc in ancestor_predicates: if anc["key"] not in token_keys: raise TransitiveScopeViolation( f"ancestor predicate {anc['key']!r} dropped at hop " f"{token.get('issued_by')!r}; scope may have widened" ) for pred in token_preds: if pred["authority"] not in trust_bundle: raise TransitiveScopeViolation( f"predicate {pred['key']!r} references untrusted " f"attribute authority {pred['authority']!r}" ) try: ok = attribute_resolver.evaluate(pred) except Exception as exc: # Unevaluable predicate must fail closed, never default-allow. raise TransitiveScopeViolation( f"predicate {pred['key']!r} unevaluable: {exc}" ) from exc if not ok: raise TransitiveScopeViolation( f"predicate {pred['key']!r} not satisfied under issuing authority" ) ```