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

  1. Issue approvals from a distinct principal: a human approver id or a dedicated supervisor/issuer identity.
  2. Register that issuer in the engine's approvalIssuerVerifier so it also passes the untrusted-approval-issuer check that runs next.
  3. 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

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

Related errors


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