paperclipai/paperclip · error · ToolGatewayHttpError
approved_tool_target_changed
approved_tool_target_changed
Error message
Approved tool action target changed after review
What it means
Post-approval integrity check: the signed payload decoded but the target it names (tool/invocation identity) differs from what the stored invocation now references, meaning the review target changed after the board reviewed it. Execution is refused with 409 so a modified action never runs under a stale approval.
Source
Thrown at server/src/services/tool-gateway.ts:5862
async function matchingAgentActionRequest(input: {
session: ToolGatewaySession;
toolName: string;
argumentsHash: string;
}) {
if (!input.session.issueId || !input.session.agentId) return null;
const [match] = await db
.select({ actionRequest: toolActionRequests, invocation: toolInvocations })
.from(toolActionRequests)
.innerJoin(toolInvocations, eq(toolInvocations.id, toolActionRequests.invocationId))
.where(and(
eq(toolActionRequests.companyId, input.session.companyId),
eq(toolActionRequests.issueId, input.session.issueId),
eq(toolActionRequests.canonicalArgumentsHash, input.argumentsHash),
eq(toolInvocations.agentId, input.session.agentId),
eq(toolInvocations.toolName, input.toolName),
inArray(toolActionRequests.status, ["pending", "approved", "executing", "rejected", "executed"]),
))
.orderBy(desc(toolActionRequests.createdAt))
.limit(1);
if (!match) return null;
// The gateway builds an ask-first request in two steps inside one call: it
// inserts the row with a null signature and a null expiry, then signs the
// row and sets the expiry. A concurrent matching call can observe the row in
// this window. A null signature does not prove the create stopped, because a
// parallel create can still be signing the same row right now. Only treat an
// unsigned row as abandoned after the grace time from createdAt has passed;
// before that, return the match so the retry replays approval_required and
// does not create a duplicate request or expire a live row. After the grace
// time an unsigned row stays pending forever and the review queue hides it,
// so it can never be approved. Do not replay it as a live approval. Expire
// the row and let the retry create a fresh, signable request. A null expiry
// alone (without this guard) also makes the getTime() check below unsafe.
const pendingRequest = match.actionRequest;
const pendingUnsigned =
pendingRequest.status === "pending"View on GitHub (pinned to 01ad858492)
Solutions
- The tool/target changed after review. Re-submit for approval with the final target; never alter an approved action's target.
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at server/src/services/tool-gateway.ts:5675 when the library encounters an invalid state.
Common situations: See trigger scenarios.
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-08-18).
Data as JSON: /api/errors/cdf29000ab530468.
Report an issue: GitHub.