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

  1. Add the issuer's id to whatever allowlist the verifier checks, then retry issuance.
  2. Make the verifier return an explicit boolean (e.g. trustedIssuers.has(id) is fine, but ensure trustedIssuers is the live set).
  3. 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

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


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