ruvnet/ruflo · error · Error
self-approval-forbidden
Error message
self-approval-forbidden
What it means
PolicyEngine.issueApproval() enforces separation of duties: the approver (issuedBy) may not equal the beneficiary (principal). Passing the same identity in both roles throws Error('self-approval-forbidden'), preventing an agent or user from minting approvals for its own requests.
Source
Thrown at v3/@claude-flow/security/src/policy/engine.ts:106
}
const index = this.state.rules.findIndex((item) => item.id === rule.id);
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 {View on GitHub (pinned to fa13ee4ad6)
Solutions
- Issue approvals from a distinct principal: a human approver id or a dedicated supervisor/issuer identity.
- Register that issuer in the engine's approvalIssuerVerifier so it also passes the untrusted-approval-issuer check that runs next.
- Audit call sites where issuedBy and principal are populated from the same variable.
Example fix
// before
engine.issueApproval({ id: 'a1', issuedBy: agentId, principal: agentId, ... });
// after
engine.issueApproval({ id: 'a1', issuedBy: 'human-ops', principal: agentId, ... }); Defensive patterns
Strategy: validation
Validate before calling
if (approval.issuedBy === approval.principal) {
throw new Error(
`approval ${approval.id}: issuer and principal must differ (both ${approval.issuedBy})`
);
}
engine.issueApproval(approval); Try / catch
try {
return engine.issueApproval(approval);
} catch (err) {
if (err instanceof Error && err.message === 'self-approval-forbidden') {
return forbidden('an approver cannot approve their own request');
}
throw err;
} Prevention
- Model the approver as a distinct principal (human operator or supervisor agent) in your identity design.
- Never default issuedBy and principal to the same service-account variable.
- Add a startup audit that greps approval call sites for shared identity variables.
When it happens
Trigger: issueApproval({ id, issuedBy: 'agent-1', principal: 'agent-1', ... }); single-principal automation where the same hardcoded id is used for issuer and beneficiary; a supervisor 'approving' a sub-task but reusing its own identity in both fields.
Common situations: Scripts that default both fields to the same service account; migrating from a system without separation of duties; tests that pass the same principal everywhere.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- untrusted-approval-issuer
- 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/eff83c7a23c5f448.
Report an issue: GitHub.