paperclipai/paperclip · error · ToolGatewayHttpError

human_review_required

human_review_required

Error message

Only a human can resolve a tool review

What it means

Tool review resolution (approve/reject of a tool action request) is a human-only governance gate. If the actor carries an agentId, the gateway immediately rejects with 403 — agents may not resolve their own approval requests.

Source

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

          await db.update(toolInvocations).set({ status: "failed", errorCode, errorMessage, completedAt: now, updatedAt: now }).where(eq(toolInvocations.id, row.invocationId));
          await reflectToolActionInteractionLifecycle({ actionRequestId: row.id, status, errorCode, errorMessage });
        }
        scanned += rows.length;
        if (rows.length < 100) break;
        cursor = rows[rows.length - 1].id;
      }
      return { scanned };
    },

    async approveActionRequest(input: {
      companyId: string;
      rememberAction?: boolean;
      issueId?: string;
      interactionId?: string;
      actionRequestId: string;
      actor: { agentId?: string | null; userId?: string | null };
    }) {
      if (input.actor.agentId) throw new ToolGatewayHttpError(403, "Only a human can resolve a tool review", "human_review_required");
      const [actionRequest] = await db
        .select()
        .from(toolActionRequests)
        .where(eq(toolActionRequests.id, input.actionRequestId))
        .limit(1);
      if (!actionRequest || actionRequest.companyId !== input.companyId) {
        throw new ToolGatewayHttpError(404, "Tool action request not found", "action_request_not_found");
      }
      const [invocation] = await db
        .select()
        .from(toolInvocations)
        .where(eq(toolInvocations.id, actionRequest.invocationId))
        .limit(1);
      if (!invocation || invocation.companyId !== input.companyId) {
        throw new ToolGatewayHttpError(404, "Tool invocation not found", "invocation_not_found");
      }
      if (input.issueId !== undefined || input.interactionId !== undefined) {
        if (

View on GitHub (pinned to 01ad858492)

Solutions

  1. Call the review resolution with a human actor (board session / userId, no agentId)
  2. Have a human approve via the board UI or an operator-authenticated API call
  3. Remove the agentId from input.actor if the caller is actually a human acting on behalf

Example fix

// before
await resolveToolActionReview({ companyId, actionRequestId, actor: { agentId } });
// after
await resolveToolActionReview({ companyId, actionRequestId, actor: { userId: operatorUserId } });
Defensive patterns

Strategy: validation

Validate before calling

if (actor.agentId) throw new Error('Review resolution requires a human actor (userId), not an agent key');

Type guard

const isHumanActor = (a: { agentId?: string | null; userId?: string | null }) => !a.agentId && !!a.userId;

Prevention

When it happens

Trigger: Calling the resolveToolActionReview endpoint/function with input.actor.agentId set (agent bearer key) instead of a userId (board/human session).

Common situations: Agent automation tries to auto-approve its own parked tool call; a script uses an agent API key where an operator session is required; misconfigured job passes the agent identity into the review resolver.

Understand the failure class

Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/0ffa32e9d2c15355. Report an issue: GitHub.