ruvnet/ruflo · error · Error
untrusted-approval-issuer
Error message
untrusted-approval-issuer
What it means
When the engine was constructed with an approvalIssuerVerifier, every issueApproval() call must have that verifier return exactly true for approval.issuedBy; anything else (false, undefined, a truthy non-boolean) throws Error('untrusted-approval-issuer'). This gates approvals to a known set of issuers.
Source
Thrown at v3/@claude-flow/security/src/policy/engine.ts:108
if (index >= 0) this.state.rules[index] = structuredClone(rule);
else this.state.rules.push(structuredClone(rule));
}
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;View on GitHub (pinned to fa13ee4ad6)
Solutions
- Add the issuer's id to whatever allowlist the verifier checks, then retry issuance.
- Make the verifier return an explicit boolean (e.g. trustedIssuers.has(id) is fine, but ensure trustedIssuers is the live set).
- Only construct the engine without a verifier when you truly accept unauthenticated approvals — in production you should not.
Example fix
// before
const verifier = (id: string) => id === 'old-approver' as unknown as boolean;
engine.issueApproval({ id, issuedBy: 'new-approver', ... }); // throws
// after
const trusted = new Set(['old-approver', 'new-approver']);
const engine = new PolicyEngine({ approvalIssuerVerifier: (id) => trusted.has(id) });
engine.issueApproval({ id, issuedBy: 'new-approver', ... }); Defensive patterns
Strategy: validation
Validate before calling
const trustedIssuers = new Set(['human-ops', 'supervisor']);
if (!trustedIssuers.has(approval.issuedBy)) {
throw new Error(`issuer ${approval.issuedBy} is not in the trusted set`);
}
// engine constructed with: new PolicyEngine({ approvalIssuerVerifier: (id) => trustedIssuers.has(id) }) Try / catch
try {
return engine.issueApproval(approval);
} catch (err) {
if (err instanceof Error && err.message === 'untrusted-approval-issuer') {
return forbidden(`issuer ${approval.issuedBy} not trusted`);
}
throw err;
} Prevention
- Keep the verifier's allowlist in live config so rotations take effect without redeploys.
- Ensure the verifier returns an explicit boolean (=== true is required, not truthy).
- Log issuer ids on rejection to catch stale allowlists quickly.
When it happens
Trigger: A new approver id that was never added to the verifier's allowlist; a verifier implemented to return a truthy string or Set.has result coerced oddly; verifier returning undefined because of a missing return path.
Common situations: Rotating approver identities without updating the verifier; the verifier closure checking against a stale config snapshot; unit tests constructing an engine with a verifier that allows nothing.
Related errors
- self-approval-forbidden
- approval issuance requires an authenticated human identity a
- policy-${decision.enforcedOutcome}:${decision.reason}; recei
- policy administration requires an authenticated user context
- policy administration requires an interactive local terminal
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/c895b82d39458ff4.
Report an issue: GitHub.