ruvnet/ruflo · error

Cannot delegate expired capability ${capability.id}

Error message

Cannot delegate expired capability ${capability.id}

What it means

delegate() compares the parent capability's `expiresAt` against Date.now() and refuses to delegate from an expired capability (expiresAt === null means no expiry). The child would inherit a tighter expiry anyway, so delegating from an already-expired parent is always invalid. This is a wall-clock check, so clock skew or a paused process can also trip it.

Source

Thrown at v3/@claude-flow/guidance/src/capabilities.ts:241

  delegate(
    capability: Capability,
    toAgentId: string,
    restrictions?: Partial<Capability>,
  ): Capability {
    if (!capability.delegatable) {
      throw new Error(
        `Capability ${capability.id} is not delegatable`
      );
    }

    if (capability.revoked) {
      throw new Error(
        `Cannot delegate revoked capability ${capability.id}`
      );
    }

    if (capability.expiresAt !== null && capability.expiresAt <= Date.now()) {
      throw new Error(
        `Cannot delegate expired capability ${capability.id}`
      );
    }

    const delegated: Capability = {
      ...capability,
      id: randomUUID(),
      grantedBy: capability.grantedTo,
      grantedTo: toAgentId,
      grantedAt: Date.now(),
      attestations: [],
      parentCapabilityId: capability.id,
    };

    // Apply optional further restrictions
    if (restrictions?.actions) {
      const originalSet = new Set(capability.actions);
      delegated.actions = restrictions.actions.filter(a => originalSet.has(a));

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Check `cap.expiresAt === null || cap.expiresAt > Date.now()` before calling delegate()
  2. Re-grant the capability with a fresh expiresAt, then delegate
  3. Extend grant lifetimes or implement renewal for long-running pipelines
  4. Sync clocks (NTP) on hosts that mint and delegate capabilities

Example fix

// before
const child = authority.delegate(expiredCap, 'agent-b'); // throws

// after
function delegatableNow(cap: Capability) {
  return cap.delegatable && !cap.revoked &&
    (cap.expiresAt === null || cap.expiresAt > Date.now());
}
if (delegatableNow(expiredCap)) {
  const child = authority.delegate(expiredCap, 'agent-b');
} else {
  // re-grant, then delegate
}
Defensive patterns

Strategy: validation

Validate before calling

if (capability.expiresAt !== null && capability.expiresAt <= Date.now()) {
  // expired — re-grant before delegating
}

Type guard

const isUnexpired = (c: Capability): boolean =>
  c.expiresAt === null || c.expiresAt > Date.now();

Prevention

When it happens

Trigger: Capability granted with a short `expiresAt` that elapsed before delegate() ran; long-lived process resumed (laptop sleep, container pause) past the expiry; system clock skewed forward on the delegating host.

Common situations: Short-lived capability tokens for CI agents that expire mid-pipeline; retry queues replaying delegation after expiry; VM clock drift in distributed clusters.

Related errors


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