{"record":{"id":"d771a9c617341f70","repo":"ruvnet/ruflo","slug":"scope-cannot-grow","errorCode":"scope-cannot-grow","errorMessage":"cannot grant ${kind} '${item}' — not in parent scope","messagePattern":"cannot grant (.+?) '(.+?)' — not in parent scope","errorType":"exception","errorClass":"AuthorizationPropagationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/authorization/propagator.ts","lineNumber":263,"sourceCode":"    outcome: 'allowed' | 'denied';\n    reason?: string;\n    ts: number;\n  }> {\n    return this.provenance.slice();\n  }\n}\n\n// ─── helpers ────────────────────────────────────────────────────────────\n\nfunction subsetOrThrow(\n  parent: ReadonlyArray<string>,\n  requested: ReadonlyArray<string>,\n  kind: 'tools' | 'servers',\n): ReadonlyArray<string> {\n  if (parent.includes('*')) return Array.from(new Set(requested));\n  for (const item of requested) {\n    if (!parent.includes(item) && item !== '*') {\n      throw new AuthorizationPropagationError(\n        'scope-cannot-grow',\n        `cannot grant ${kind} '${item}' — not in parent scope`,\n      );\n    }\n  }\n  // De-duplicate; preserve requested order\n  const seen = new Set<string>();\n  const out: string[] = [];\n  for (const r of requested) if (!seen.has(r)) (seen.add(r), out.push(r));\n  return out;\n}\n\nfunction matchesScopeList(list: ReadonlyArray<string>, item: string): boolean {\n  if (list.includes('*')) return true;\n  return list.includes(item);\n}\n","sourceCodeStart":245,"sourceCodeEnd":280,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/authorization/propagator.ts#L245-L280","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst env = propagator.wrapOutbound(msg, scope, { tools: ['fs.read', 'fs.write'] }); // fs.write not granted\n\n// after\nconst requested = ['fs.read', 'fs.write'].filter((t) => scope.grantedTools.includes(t));\nif (requested.length === 0) {\n  throw new Error('no requested tools in parent scope — request a new grant from the principal');\n}\nconst env = propagator.wrapOutbound(msg, scope, { tools: requested });","handlingStrategy":"validation","validationCode":"function subsetOf(requested: readonly string[], granted: readonly string[]): string[] {\n  if (granted.includes('*')) return [...requested];\n  return requested.filter((t) => granted.includes(t));\n}\nconst tools = subsetOf(wantTools, scope.grantedTools);\nif (tools.length === 0) throw new Error('no requested tools in parent scope — request a new grant');\nconst envelope = propagator.wrapOutbound(msg, scope, { tools });","typeGuard":"function isAuthPropagationError(e: unknown, code?: string): boolean {\n  return e instanceof Error && e.name === 'AuthorizationPropagationError'\n    && (code === undefined || (e as { code?: string }).code === code);\n}","tryCatchPattern":"try {\n  return propagator.wrapOutbound(msg, scope, { tools: wantTools });\n} catch (e) {\n  if (isAuthPropagationError(e, 'scope-cannot-grow')) {\n    return propagator.wrapOutbound(msg, scope, { tools: subsetOf(wantTools, scope.grantedTools) });\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["security","authorization","least-privilege","delegation"],"backgroundTag":"insufficient-scope","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}