ruvnet/ruflo · error · Error
policy- : ; receipt=
Error message
policy-${decision.enforcedOutcome}:${decision.reason}; receipt=${decision.receiptId} What it means
Per ADR-324 every MCP tool call passes through a single policy chokepoint: authorizeMcpTool(toolName, input, context, classification). If the decision's enforcedOutcome is anything other than 'allowed' (e.g. denied), callMCPTool throws `policy-<outcome>:<reason>` including a receiptId. This is the authorization engine working as designed — the tool handler never ran — and the receipt is the audit handle for what rule fired. Policy administration is not exempt: policy.* actions must be explicitly allowed in enforce mode.
Solutions
- Read the reason and receiptId from the thrown message — the reason names the rule/outcome that fired; keep the receiptId for audit
- Add an explicit allow rule for that action class in the policy configuration (e.g. allow the classified action for the calling principal), or for policy.* administration use the local CLI bootstrap path which calls the engine directly
- Temporarily run policy in observe mode to see which rule matches the call, then codify the intended allow before re-enabling enforce
- Verify the context you pass to callMCPTool carries attributes (user/project/env) that the policy rules match on
Example fix
// before — enforce mode, no allow for the class
await callMCPTool('policy_compile', { policy: p });
// throws: policy-denied:no allow rule for policy.compile; receipt=...
// after — administer via the local CLI bootstrap path
// (bypasses recursive MCP dispatch by calling the engine directly)
await exec('npx @claude-flow/cli@latest policy compile --file policy.kdl'); Defensive patterns
Strategy: try-catch
Try / catch
try {
return await callMCPTool(toolName, input, context);
} catch (e) {
const m = /^(\w+)-policy-(\w+):(.+); receipt=(.+)$/.exec((e as Error).message)
?? /^policy-([\w-]+):(.+); receipt=(.+)$/.exec((e as Error).message);
if (m) {
// Authorization denial — never retry; route to admin/audit flow
return { denied: true, outcome: m[1], reason: m[2] ?? m[2], receiptId: m[3] };
}
throw e;
} Prevention
- Run the policy engine in observe mode first and codify the allows it logs before switching to enforce
- Keep explicit allow rules for policy.* administration actions or use the local CLI bootstrap path
- Pass complete context (user/project/env) to callMCPTool so rules can match
- Log receiptIds — they are the audit trail for denied calls
When it happens
Trigger: Running in enforce mode without an explicit allow rule for the action class that classifyMcpTool() assigned to the call; invoking policy.* administration tools through the MCP path instead of the local CLI bootstrap; a policy file missing context attributes the rule requires (user, project, environment); switching the engine from observe to enforce without adding the allows that observe-mode logs suggested.
Common situations: Hardening a deployment by flipping enforce on and every previously-working call starts throwing; CI bots calling policy-admin tools with no administrator allow; upgrading the CLI where classification of a tool changed, moving it into a restricted class; multi-tenant setups where one tenant's rules leak-deny another.
Related errors
- policy administration requires an authenticated user context
- approval issuance requires an authenticated human identity…
- self-approval-forbidden
- untrusted-approval-issuer
- Dangerous key segment rejected
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-08-18).
Data as JSON: /api/errors/31bb7474ee38d234.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-client.ts:267
// Look up tool in registry
const tool = TOOL_REGISTRY.get(toolName);
if (!tool) {
throw new MCPClientError(
`MCP tool not found: ${toolName}`,
toolName
);
}
try {
// ADR-324: one policy chokepoint for every local CLI/MCP invocation.
// Policy administration is not exempt: authorization calls the engine
// directly, so there is no recursive MCP dispatch. In enforce mode an
// administrator must explicitly allow policy.* actions or use the local
// CLI bootstrap path.
const decision = await authorizeMcpTool(toolName, input, context, classifyMcpTool(toolName));
if (decision.enforcedOutcome !== 'allowed') {
throw new Error(`policy-${decision.enforcedOutcome}:${decision.reason}; receipt=${decision.receiptId}`);
}
// Call the tool handler
const result = await tool.handler(input, context);
// ADR-146 P2: scan every tool result for indirect-injection before it
// returns to the caller. The screen is opt-in via env (default off in
// 3.10.34 — flip to default in v4) so existing pipelines keep their
// exact behaviour while the call site is exercised by tests and
// adopters. Telemetry from the screen lands in the shared
// GuardrailEvent sink (P5).
return applyContentBoundaryGuardrail(toolName, result) as T;
} catch (error) {
// Wrap and re-throw with context
throw new MCPClientError(
`Failed to execute MCP tool '${toolName}': ${error instanceof Error ? error.message : String(error)}`,
toolName,
error instanceof Error ? error : undefined
);
}View on GitHub (pinned to 2602b642d9)