paperclipai/paperclip · error · ToolGatewayHttpError
access.reasonCode
access.reasonCode
Error message
access.explanation
What it means
This 403 ToolGatewayHttpError is thrown during a board-side approval review (commitToolActionReview path) when the reviewer approves an execute-on-approve tool request. Just before committing the approval, the policy service re-decides access against the current session/tool/parameters; if the live decision denies the action (and is not 'require_approval'), the approval is blocked with the policy's explanation and reasonCode. This prevents a human approver from green-lighting an action that current policy forbids.
Solutions
- Read access.explanation/reasonCode in the error to identify the denying policy rule; either update the policy to allow the tool or decline/re-create the request under the current rules.
- Reject the stale pending request and have the agent re-issue the tool call so a new request is created and evaluated under current policy.
- Verify the tool's policy classification and the agent's role in the board settings before re-approving.
- If the tool's remote connection definition changed, expect 'approved_tool_target_changed' instead and request a fresh review.
Example fix
// before: approving a stale request blocked by new policy
await approveToolActionRequest(requestId); // 403 access.explanation
// after: re-decide first; if denied, reject and re-request
const access = await policyService.decide(policyInputForTool({ session, tool, parameters }));
if (!access.allowed && access.decision !== "require_approval") {
await rejectToolActionRequest(requestId, "policy now denies this tool");
} else {
await approveToolActionRequest(requestId);
} Defensive patterns
Strategy: validation
Validate before calling
// reviewer-side pre-check before approving
const access = await policyService.decide(policyInputForTool({ session, tool, parameters }));
if (!access.allowed && access.decision !== "require_approval") {
await rejectToolActionRequest(requestId, `Policy now denies: ${access.explanation}`);
return;
} Type guard
function canApprove(access: { allowed: boolean; decision: string }): boolean {
return access.allowed || access.decision === "require_approval";
} Try / catch
try {
await approveToolActionRequest(requestId);
} catch (err) {
if (err?.status === 403 && typeof err?.code === "string") {
// policy denies under current rules: reject the stale request and ask the agent to re-issue
await rejectToolActionRequest(requestId, err.message);
} else throw err;
} Prevention
- Surface a live policy verdict in the approval UI so reviewers see denials before clicking approve.
- Expire or re-validate long-pending requests after policy changes.
- Keep tool policy and connection definitions stable while requests await review.
- Route 403 reasonCodes from reviews to the agent so it can adjust and re-request.
When it happens
Trigger: Approving a pending tool action request (executionOnApprove === true, non-test origin) where the policy engine now denies the tool for the agent/company — e.g. the tool's policy class changed to denied, the agent's permissions were reduced, or parameter/connection-scoped rules forbid the arguments after the request was created.
Common situations: A policy update landed between when the agent requested the action and when the operator clicked approve; the approval queue contains stale requests created before a tool was restricted; the remote connection's capabilities or permission snapshot changed so the re-decision denies.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- currentAccess.reasonCode
- paperclip_runner_chat_attachment_read_not_authorized
- Run telemetry is outside this actor's authorization boundary
- Runtime service control is outside this actor's…
- trustPreset.detail
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/daf23bb121dff3b4.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/tool-gateway.ts:9473
!approvalSnapshotsMatch(
signedPayload.approvalSnapshot,
await connectedRemoteApprovalSnapshot(session, tool),
)
)
throw new ToolGatewayHttpError(
409,
"Tool definition or connection changed; request a new review",
"approved_tool_target_changed",
);
const access = await policyService.decide(
policyInputForTool({
session,
tool,
parameters: signedPayload.arguments,
}),
);
if (!access.allowed && access.decision !== "require_approval")
throw new ToolGatewayHttpError(
403,
access.explanation,
access.reasonCode,
);
}
const updated = await commitToolActionReview(db, {
...input,
decision: "approved",
});
await reflectToolActionInteractionLifecycle({
actionRequestId: updated.id,
status: "approved",
});
// A test-tab ask-first request has no agent run to carry out the parked
// call, so approving it is what runs it. Execute against the signed
// arguments and record the result on the invocation for the live panel.
if (isTestOriginInvocation(invocation)) {
await runApprovedTestInvocation(View on GitHub (pinned to 3f1d897a7c)