ruvnet/ruflo · critical · Error

capability-envelope-cannot-grow

Error message

capability-envelope-cannot-grow

What it means

After merging parent and child (child's fields win), delegateEnvelope() verifies with isEnvelopeReduction() that the result is no broader than the parent: listed capabilities must be subsets (monotone shrinking), network and destructive may not appear unless the parent had them, and expiry cannot be extended. Violations throw Error('capability-envelope-cannot-grow') — this is the anti-privilege-escalation invariant.

Source

Thrown at v3/@claude-flow/security/src/policy/envelope.ts:107

  parent: CapabilityEnvelope,
  child: CapabilityEnvelope,
): CapabilityEnvelope {
  if ((parent.delegationDepth ?? 0) <= 0) {
    throw new Error('delegation-depth-exhausted');
  }
  const reduced = {
    ...parent,
    ...child,
    delegationDepth: Math.min(
      child.delegationDepth ?? Number.MAX_SAFE_INTEGER,
      (parent.delegationDepth ?? 0) - 1,
    ),
    expiresAt: Math.min(
      child.expiresAt ?? Number.MAX_SAFE_INTEGER,
      parent.expiresAt ?? Number.MAX_SAFE_INTEGER,
    ),
  };
  if (!isEnvelopeReduction(parent, reduced)) throw new Error('capability-envelope-cannot-grow');
  return reduced;
}

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Derive the child by intersecting: filter the child's tools/servers/namespaces to those the parent already holds.
  2. Only set child.network / child.destructive when the parent grants them; otherwise omit (undefined).
  3. Accept that expiry is clamped to the parent's — do not try to extend a child beyond its parent's lifetime.
  4. Write a helper that constructs children via parent-to-child subtraction so growth is structurally impossible.

Example fix

// before
const child = delegateEnvelope(parent, {
  ...childSpec, // includes tools the parent lacks and network: true
});

// after
const child = delegateEnvelope(parent, {
  ...childSpec,
  tools: childSpec.tools.filter((t) => parent.tools.includes(t)),
  network: parent.network === true ? childSpec.network : undefined,
  expiresAt: Math.min(childSpec.expiresAt, parent.expiresAt),
});
Defensive patterns

Strategy: validation

Validate before calling

function shrinkToParent(parent: CapabilityEnvelope, spec: Partial<CapabilityEnvelope>) {
  return {
    ...spec,
    tools: spec.tools?.filter((t) => parent.tools.includes(t)),
    servers: spec.servers?.filter((s) => parent.servers.includes(s)),
    namespaces: spec.namespaces?.filter((n) => parent.namespaces.includes(n)),
    network: parent.network === true ? spec.network : undefined,
    destructive: parent.destructive === true ? spec.destructive : undefined,
    expiresAt: Math.min(spec.expiresAt ?? Infinity, parent.expiresAt ?? Infinity),
  };
}
const child = delegateEnvelope(parent, shrinkToParent(parent, childSpec));

Type guard

function isSubsetOf<T>(child: T[] | undefined, parent: T[] | undefined): boolean {
  const p = new Set(parent ?? []);
  return (child ?? []).every((item) => p.has(item));
}

Try / catch

try {
  return delegateEnvelope(parent, child);
} catch (err) {
  if (err instanceof Error && err.message === 'capability-envelope-cannot-grow') {
    // security invariant: never 'fix' by widening the parent automatically.
    alertSecurityTeam({ parent, child });
    throw new ForbiddenError('child envelope requested capabilities beyond its parent');
  }
  throw err;
}

Prevention

When it happens

Trigger: A child envelope granting a tool/server/namespace absent from the parent; child.network === true when parent.network !== true; child.destructive === true from a non-destructive parent; child.expiresAt further in the future than parent.expiresAt (Math.min clamps expiry, but a broader listed set still throws).

Common situations: Building sub-agent envelopes from a general template that includes the full tool list instead of intersecting with the parent's; copy-pasting an envelope and trimming only the principal; delegation code that inherits 'network: true' defaults while parents are offline-only.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/ecfbf14eacca95ad. Report an issue: GitHub.