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

  1. 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.