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
- Call the review resolution with a human actor (board session / userId, no agentId)
- Have a human approve via the board UI or an operator-authenticated API call
- 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
- Use board/operator sessions for approval endpoints, never agent API keys
- Route any desired auto-approval through a human-configured policy rule instead
- Review automation scripts for leaked agent credentials
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
- native_interaction_self_approval
- native_interaction_governed_request_unresolved
- native_interaction_governed_result_mismatch
- identity_context_unavailable
- action_task_closed
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/0ffa32e9d2c15355.
Report an issue: GitHub.