ruvnet/ruflo · error · AuthorizationPropagationError
depth-underflow
depth-underflow
Error message
cannot delegate further — delegationDepth=${currentScope.delegationDepth} What it means
AgentAuthorizationPropagator.wrapOutbound enforces ADR-144's monotonically-reducing scope: each delegation hop consumes at least one unit of delegationDepth. Wrapping an outbound message with a scope whose delegationDepth <= 0 throws AuthorizationPropagationError code 'depth-underflow' — the delegation chain is longer than the original grant permitted, and depth can never be replenished mid-chain.
Source
Thrown at v3/@claude-flow/security/src/authorization/propagator.ts:152
constructor(private readonly opts: { provenanceBufferMax?: number } = {}) {}
/**
* Attach a reduced scope to an outbound SendMessage.
*
* Invariants enforced (throws `AuthorizationPropagationError` on violation):
* - newly granted tools MUST be a subset of `currentScope.grantedTools`
* - newly granted servers MUST be a subset of `currentScope.grantedServers`
* - delegationDepth MUST decrement by ≥ 1 (must remain ≥ 0)
* - principalId is propagated unchanged
* - expiresAt cannot be extended; copied from the holder
*/
wrapOutbound<T>(
payload: T,
currentScope: AuthScope,
requested: { tools?: ReadonlyArray<string>; servers?: ReadonlyArray<string> } = {},
): SendMessageEnvelope<T> {
if (currentScope.delegationDepth <= 0) {
throw new AuthorizationPropagationError(
'depth-underflow',
`cannot delegate further — delegationDepth=${currentScope.delegationDepth}`,
);
}
const now = Date.now();
if (currentScope.expiresAt <= now) {
throw new AuthorizationPropagationError(
'scope-expired',
`scope expired at ${new Date(currentScope.expiresAt).toISOString()}`,
);
}
const reducedTools = subsetOrThrow(
currentScope.grantedTools,
requested.tools ?? currentScope.grantedTools,
'tools',
);
const reducedServers = subsetOrThrow(View on GitHub (pinned to fa13ee4ad6)
Solutions
- Have the receiving agent execute the work itself — at depth 0 delegation is forbidden by design
- Request a fresh scope with a higher delegationDepth from the original principal (out-of-band)
- Audit each hop: call wrapOutbound exactly once per SendMessage; re-wrapping an envelope's scope double-decrements
- Mint the root scope with delegationDepth >= the maximum expected chain length
Example fix
// before
const envelope = propagator.wrapOutbound(msg, scope); // scope.delegationDepth === 0 → depth-underflow
// after
if (scope.delegationDepth <= 0) {
await handleLocally(msg); // leaf: execute, don't delegate
} else {
const envelope = propagator.wrapOutbound(msg, scope);
} Defensive patterns
Strategy: validation
Validate before calling
import type { AuthScope } from './authorization/propagator.js';
function canDelegate(scope: AuthScope, now = Date.now()): boolean {
return scope.delegationDepth > 0 && scope.expiresAt > now;
}
if (!canDelegate(scope)) throw new Error('scope cannot delegate — request a fresh one from the principal');
const envelope = propagator.wrapOutbound(msg, scope); Type guard
function isAuthPropagationError(e: unknown, code?: string): boolean {
return e instanceof Error && e.name === 'AuthorizationPropagationError'
&& (code === undefined || (e as { code?: string }).code === code);
} Try / catch
try {
return propagator.wrapOutbound(msg, scope);
} catch (e) {
if (isAuthPropagationError(e, 'depth-underflow')) {
return handleLocally(msg); // execute at this hop instead of delegating
}
throw e;
} Prevention
- Mint root scopes with delegationDepth >= maximum expected chain length
- Wrap the original payload exactly once per hop — never re-wrap an envelope's scope
- Unit-test deep chains against the depth budget before shipping topologies
When it happens
Trigger: Agent C calls wrapOutbound with the scope it received when that scope's depth already hit 0; a retry/re-forward path re-wraps an already-wrapped scope, burning depth a second time; scopes minted with delegationDepth: 0 intended as 'leaf agents may not delegate' actually hit the throw.
Common situations: Swarm topologies with 3+ delegation hops built from a root scope with a small depth budget; message-forwarding middleware that wraps envelopes again; test fixtures hand-building scopes with default 0 depth instead of makeLegacyPermissiveScope().
Related errors
- scope-expired
- scope-cannot-grow
- delegation-depth-exhausted
- approval issuance requires an authenticated human identity a
- policy-${decision.enforcedOutcome}:${decision.reason}; recei
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/5fba53b98dbbf0b0.
Report an issue: GitHub.