ruvnet/ruflo · error · Error

duplicate-approval-id

Error message

duplicate-approval-id

What it means

Approval ids must be unique across the engine's state: issueApproval() scans existing approvals and throws Error('duplicate-approval-id') when the incoming id already exists. Ids are not auto-generated or namespaced, so callers own uniqueness.

Source

Thrown at v3/@claude-flow/security/src/policy/engine.ts:112

  setBudget(limit: BudgetLimit): void {
    if (limit.periodMs <= 0) throw new Error('invalid-budget-period');
    if (!Number.isFinite(limit.periodMs)
      || [limit.maxCostUsd, limit.maxTokens].some((value) => (
        value !== undefined && (!Number.isFinite(value) || value < 0)
      ))) throw new Error('invalid-budget-limit');
    const index = this.state.budgets.findIndex((item) => item.id === limit.id);
    if (index >= 0) this.state.budgets[index] = structuredClone(limit);
    else this.state.budgets.push(structuredClone(limit));
  }

  issueApproval(approval: Omit<PolicyApproval, 'uses' | 'issuedAt'> & { uses?: number; issuedAt?: number }): PolicyApproval {
    if (approval.issuedBy === approval.principal) throw new Error('self-approval-forbidden');
    if (this.approvalIssuerVerifier?.(approval.issuedBy) !== true) {
      throw new Error('untrusted-approval-issuer');
    }
    const issuedAt = approval.issuedAt ?? this.now();
    const record: PolicyApproval = { ...approval, issuedAt, uses: approval.uses ?? 0 };
    if (this.state.approvals.some((item) => item.id === record.id)) throw new Error('duplicate-approval-id');
    if (!record.id
      || record.expiresAt <= issuedAt
      || !Number.isInteger(record.maxUses)
      || record.maxUses <= 0
      || !Number.isInteger(record.uses)
      || record.uses < 0
      || record.uses > record.maxUses) throw new Error('invalid-approval');
    this.state.approvals.push(record);
    return structuredClone(record);
  }

  revokeApproval(id: string): boolean {
    const approval = this.state.approvals.find((item) => item.id === id);
    if (!approval || approval.revokedAt) return false;
    approval.revokedAt = this.now();
    return true;
  }

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Generate a fresh id per issuance (crypto.randomUUID()) unless you deliberately need deterministic ids.
  2. For idempotent retries, check engine state for the id first and reuse the existing approval instead of re-issuing.
  3. If a stale approval blocks a re-issue, revokeApproval(id) before issuing a replacement.

Example fix

// before
engine.issueApproval({ id: `appr-${requestId}`, ... }); // throws on retry

// after
const id = `appr-${requestId}`;
try {
  engine.issueApproval({ id, ... });
} catch (err) {
  if (!(err instanceof Error) || err.message !== 'duplicate-approval-id') throw err;
  engine.revokeApproval(id);
  engine.issueApproval({ id: `${id}-${Date.now()}`, ... });
}
Defensive patterns

Strategy: try-catch

Validate before calling

// deterministic-id retry path: probe state first (or track issued ids)
if (issuedApprovalIds.has(id)) {
  return existingApproval(id);
}
engine.issueApproval({ id, ...rest });
issuedApprovalIds.add(id);

Try / catch

try {
  return engine.issueApproval(approval);
} catch (err) {
  if (err instanceof Error && err.message === 'duplicate-approval-id') {
    // idempotent retry: fetch or revoke+reissue instead of failing
    return lookupApproval(approval.id) ?? reissue(approval);
  }
  throw err;
}

Prevention

When it happens

Trigger: A retry wrapper re-running the same issueApproval call with a deterministic id after a transient failure; rehydrating from persisted state and re-issuing approvals that were already loaded; two workflows deriving the same id from the same request hash.

Common situations: Idempotency-key reuse across runs; crash-recovery logic that replays issuance; deterministic ids for reproducible tests colliding with previously issued ones.

Related errors


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