ruvnet/ruflo · error · AuthorizationPropagationError
scope-cannot-grow
scope-cannot-grow
Error message
cannot grant ${kind} '${item}' — not in parent scope What it means
subsetOrThrow enforces that a delegated scope can only shrink: every requested tool/server ID must already exist in the parent's grantedTools/grantedServers, unless the parent set includes '*' (full pass-through). Requesting anything the parent never granted throws AuthorizationPropagationError code 'scope-cannot-grow' — this is ADR-144's anti-privilege-escalation guard for SendMessage delegation.
Source
Thrown at v3/@claude-flow/security/src/authorization/propagator.ts:263
outcome: 'allowed' | 'denied';
reason?: string;
ts: number;
}> {
return this.provenance.slice();
}
}
// ─── helpers ────────────────────────────────────────────────────────────
function subsetOrThrow(
parent: ReadonlyArray<string>,
requested: ReadonlyArray<string>,
kind: 'tools' | 'servers',
): ReadonlyArray<string> {
if (parent.includes('*')) return Array.from(new Set(requested));
for (const item of requested) {
if (!parent.includes(item) && item !== '*') {
throw new AuthorizationPropagationError(
'scope-cannot-grow',
`cannot grant ${kind} '${item}' — not in parent scope`,
);
}
}
// De-duplicate; preserve requested order
const seen = new Set<string>();
const out: string[] = [];
for (const r of requested) if (!seen.has(r)) (seen.add(r), out.push(r));
return out;
}
function matchesScopeList(list: ReadonlyArray<string>, item: string): boolean {
if (list.includes('*')) return true;
return list.includes(item);
}
View on GitHub (pinned to fa13ee4ad6)
Solutions
- Intersect the requested list with the parent's granted set before calling wrapOutbound
- Compare requested IDs against scope.grantedTools character-for-character to catch typos
- If the tool is genuinely needed, get a new grant from the original principal out-of-band
- Grant '*' at the root only when full pass-through semantics are acceptable
Example fix
// before
const env = propagator.wrapOutbound(msg, scope, { tools: ['fs.read', 'fs.write'] }); // fs.write not granted
// after
const requested = ['fs.read', 'fs.write'].filter((t) => scope.grantedTools.includes(t));
if (requested.length === 0) {
throw new Error('no requested tools in parent scope — request a new grant from the principal');
}
const env = propagator.wrapOutbound(msg, scope, { tools: requested }); Defensive patterns
Strategy: validation
Validate before calling
function subsetOf(requested: readonly string[], granted: readonly string[]): string[] {
if (granted.includes('*')) return [...requested];
return requested.filter((t) => granted.includes(t));
}
const tools = subsetOf(wantTools, scope.grantedTools);
if (tools.length === 0) throw new Error('no requested tools in parent scope — request a new grant');
const envelope = propagator.wrapOutbound(msg, scope, { tools }); 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, { tools: wantTools });
} catch (e) {
if (isAuthPropagationError(e, 'scope-cannot-grow')) {
return propagator.wrapOutbound(msg, scope, { tools: subsetOf(wantTools, scope.grantedTools) });
}
throw e;
} Prevention
- Derive requested tool lists from the parent's granted set, never from agent wishes
- Use one canonical tool-ID convention at grant time and request time
- Treat this throw as a security event to audit, not just an exception to suppress
When it happens
Trigger: wrapOutbound(msg, scope, { tools: ['fs.write'] }) when the parent scope only granted ['fs.read', 'shell.exec']; a typo or ID-convention mismatch between what the granter wrote and what the child asks for; requesting a tool added to the catalog after the root grant was minted.
Common situations: Child agents naming tools by a different ID format than the granter; evolving MCP tool catalogs where new tools must be re-granted at the root; accidental (or malicious) attempts to widen scope mid-chain.
Related errors
- Capability ${capability.id} is not delegatable
- depth-underflow
- scope-expired
- delegation-depth-exhausted
- approval issuance requires an authenticated human identity a
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/d771a9c617341f70.
Report an issue: GitHub.