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
- Generate a fresh id per issuance (crypto.randomUUID()) unless you deliberately need deterministic ids.
- For idempotent retries, check engine state for the id first and reuse the existing approval instead of re-issuing.
- 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
- Default to crypto.randomUUID() for approval ids unless determinism is required.
- Wrap issuance retries in idempotency: remember issued ids, or check engine state before re-issuing.
- Pair deterministic ids with a revoke-then-reissue flow for refresh scenarios.
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
- self-approval-forbidden
- untrusted-approval-issuer
- invalid-approval
- ${label} requires a JSON argument
- ${label} must be valid JSON
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/d775c8017854d96e.
Report an issue: GitHub.