# When must an instance refuse to narrow a delegated capability scope it cannot verify? **Date of Record:** 2026 November 6 **Scope:** Applies to any agent that receives a delegated capability token from an upstream agent and is asked to mint a further-narrowed scope for a downstream agent. Does not apply to root-of-trust issuance, where the issuer holds the full authority graph and can enumerate all descendants. ## Core Distinction Least-privilege scope narrowing is usually framed as a *set* operation: the child scope must be a subset of the parent scope. The non-obvious failure is that subset checking on the *syntactic* scope expression is not the same as subset checking on the *semantic* capability set. Two scope expressions can be syntactically incomparable yet semantically overlapping, and a narrowing operation can be syntactically a strict subset while semantically *widening* the effective authority — because scope languages admit aliases, wildcards that expand at evaluation time, and resource identifiers that resolve differently in the child's evaluation context than in the parent's. The distinction that matters operationally: **a delegating agent may only assert narrowing over the portion of the parent scope it can actually evaluate.** Everything else is an unverified claim, and an unverified narrowing claim is an amplification vector, not a safety property. ## What Was Observed A three-tier delegation chain: orchestrator → planner → worker. The orchestrator held `storage:read` over `bucket:prod-*`. The planner was asked to delegate to a worker that only needed `bucket:prod-reports`. The planner constructed the child scope `storage:read@bucket:prod-reports` and asserted it as a strict narrowing. The narrowing was syntactically valid and passed the planner's subset check. It failed at evaluation for two independent reasons observed in the same incident window: 1. **Alias expansion.** The planner's scope language resolved `prod-reports` through a tenant-local alias table that mapped it to `bucket:prod-reports-2024,bucket:prod-reports-2025,bucket:prod-archive`. One of those (`prod-archive`) was outside the parent's `prod-*` glob only because the glob was anchored at evaluation time against a *different* alias table than the one the worker used. The child scope evaluated wider than the parent. 2. **Wildcard re-binding.** A second delegation used a scope expression containing a deferred wildcard (`bucket:${TENANT}-*`). The planner could not expand `${TENANT}` because it did not hold the tenant-binding context; it passed the expression through unchanged and asserted narrowing on the literal string. The worker expanded it against its own tenant context and received read over a bucket the orchestrator never intended to expose. In both cases the delegating agent *believed* it had verified a subset relation. It had verified a string relation. ## Condition of Validity A narrowing assertion by an intermediate agent is valid only when all of the following hold: - The parent scope is **fully expanded** in the delegating agent's evaluation context — no deferred wildcards, no unresolved aliases, no symbolic references to context the agent does not hold. - The child scope is expanded in the **same** context, and the expansion function is deterministic and identical across the delegation chain. If the child's evaluation context differs from the parent's, the agent cannot compute a subset relation and must not claim one. - The agent holds the **complete authority graph** for the resources named, or can query an authority that does. Partial knowledge of a resource namespace is sufficient to construct a child scope but not to *certify* it as narrower. If any condition fails, the correct behavior is to refuse the narrowing and escalate to the nearest ancestor that holds full expansion context — not to pass the scope through with an optimistic assertion. ## Decision Boundary The boundary is between **constructing** a narrowed scope and **certifying** it. An agent may always construct a candidate child scope. It may only certify (stamp as `narrowed: true`, or emit a delegation token carrying a subset proof) when it can expand both parent and child in a single, shared evaluation context and demonstrate the subset relation over the expanded sets. The trap is that most delegation APIs conflate these: the act of minting a child token *is* the assertion. There is no separate certification step, so an agent that cannot evaluate a scope is forced either to refuse the entire delegation or to mint a token it cannot justify. The operational rule is to make refusal explicit and attributable, so the upstream can re-delegate from a context that *can* certify. Do not narrow across an evaluation-context boundary. Do not narrow a scope containing unresolved symbolic references. Do not treat string-prefix containment as set containment. ## Operational Effect When the rule is enforced, delegation chains develop **certification gaps** at context boundaries: the intermediate agent returns a structured refusal rather than a token, and the orchestrator re-issues the narrowed token directly from a context that holds the full alias table and tenant bindings. The chain gets one hop longer in the authority graph but one fewer unverified claim. Latency increases marginally; the amplification surface closes entirely. When the rule is violated, the failure is silent and delayed: the child token evaluates successfully, the worker performs reads it was never granted, and the audit trail shows a syntactically valid narrowing that was semantically a widening. The incident is only detectable by diffing expanded scope sets at evaluation time — which is exactly the computation the delegating agent declined to perform. ```python class UnverifiableNarrowingError(RuntimeError): """Raised when an agent cannot certify a child scope as a semantic subset of its parent.""" pass def delegate_narrowed_scope(parent_scope, child_scope, evaluator): """ Mint a narrowed child scope only if the subset relation can be certified over expanded capability sets in a single shared evaluation context. """ # Refuse if either scope contains unresolved symbolic references. if not evaluator.is_fully_expandable(parent_scope): raise UnverifiableNarrowingError( f"parent scope {parent_scope!r} contains unresolved references; " "cannot certify narrowing without full expansion context" ) if not evaluator.is_fully_expandable(child_scope): raise UnverifiableNarrowingError( f"child scope {child_scope!r} contains deferred wildcards or aliases " "that this agent cannot resolve in the parent's evaluation context" ) parent_set = evaluator.expand(parent_scope) child_set = evaluator.expand(child_scope) # Semantic subset check over expanded capability sets, not string prefixes. if not child_set.issubset(parent_set): leaked = child_set - parent_set raise UnverifiableNarrowingError( f"child scope is not a semantic subset of parent; " f"expansion escapes parent authority: {sorted(leaked)!r}" ) return evaluator.mint(child_scope, proof=("subset", parent_set, child_set)) ```