paperclipai/paperclip · error · ToolGatewayHttpError

currentAccess.reasonCode

currentAccess.reasonCode

Error message

currentAccess.explanation

What it means

This 403 ToolGatewayHttpError is thrown during execution of an approved (execute-on-approve) tool action, after the request claim is won and the signed arguments are verified. Before running the side effect, the policy service is re-consulted (policyService.decide) with the current session, tool, and parameters; if the live policy decision is not allowed and not 'require_approval', the action is aborted with the policy's explanation and reasonCode, and the action request is marked failed. This enforces that policy revocations between approval time and execution time are honored.

Solutions

  1. Read currentAccess.explanation/reasonCode in the error to see which policy rule denied the action, then adjust that policy (or the tool/connection config) and have the agent re-request approval.
  2. Re-request the tool action so a fresh approval is created and decided under the current policy.
  3. If the deny is unintended, update the tool policy for the company/agent to allow the tool class, then retry the invocation.
  4. Check whether the tool or its remote connection definition changed after approval; if so a new review is required regardless.

Example fix

// before: policy denies post-approval, action fails
await executeApprovedAction(actionRequestId);
// after: verify policy before requesting approval to avoid wasted approvals
const access = await policyService.decide(policyInputForTool({ session, tool, parameters }));
if (!access.allowed && access.decision !== "require_approval") throw new Error(`Policy denies tool: ${access.explanation}`);
await executeApprovedAction(actionRequestId);
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: re-check policy before requesting approval
const access = await policyService.decide(policyInputForTool({ session, tool, parameters }));
if (!access.allowed && access.decision !== "require_approval") {
  throw new Error(`Tool policy denies this action: ${access.explanation}`);
}

Type guard

function isPolicyAllowed(access: { allowed: boolean; decision: string }): boolean {
  return access.allowed || access.decision === "require_approval";
}

Try / catch

try {
  await executeApprovedAction(actionRequestId);
} catch (err) {
  if (err?.status === 403 && typeof err?.code === "string") {
    // policy changed between approval and execution; mark request failed and re-request
    await requestFreshApproval({ toolName, parameters, note: err.message });
  } else throw err;
}

Prevention

When it happens

Trigger: Executing a claimed approved tool action where the company/agent tool policy has changed since approval: the tool was moved to a denied policy class, the agent lost permission for the tool or connection, budget/domain/parameter-level policy rules now deny the call, or the identity context restored for the action is no longer permitted the tool.

Common situations: An operator tightened tool permissions while the approval sat in the queue; the approval was granted under one policy snapshot but a policy update landed before execution; a remote MCP connection's permission set changed so the re-decision denies; token/role downgrades on the agent between approve and execute.

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


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/27d91e4da80c7c2d. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/tool-gateway.ts:7720

        signedPayload.identityContextId,
      );
      tool = await findToolForSession(session, invocation.toolName);
      liveApprovalSnapshot = await connectedRemoteApprovalSnapshot(
        session,
        tool,
      );
      const currentAccess = await policyService.decide(
        policyInputForTool({
          session,
          tool,
          parameters: signedPayload.arguments,
        }),
      );
      if (
        !currentAccess.allowed &&
        currentAccess.decision !== "require_approval"
      )
        throw new ToolGatewayHttpError(
          403,
          currentAccess.explanation,
          currentAccess.reasonCode,
        );
    } catch (error) {
      await markApprovedActionFailed({
        actionRequestId: claimed.id,
        invocationId: invocation.id,
        claimUpdatedAt: claimed.updatedAt,
        expectedInvocationStatus: "awaiting_approval",
        error,
      });
      throw error;
    }
    if (
      !approvalSnapshotsMatch(
        signedPayload.approvalSnapshot,
        liveApprovalSnapshot,

View on GitHub (pinned to 3f1d897a7c)