paperclipai/paperclip · error · Error

paperclip_runner_tool_binding_not_authorized

paperclip_runner_tool_binding_not_authorized

Error message

paperclip_runner_tool_binding_not_authorized

What it means

#boundContext re-verifies, on every tool execution, that the runner's binding still maps to a live, authorized run: the joined row exists, the run is in native runtime mode with status 'running', the actor (agent) is not paused/terminated/pending_approval/error, and the identity columns (company, agent, issue, assignee, executionRun) all match the binding. Any failure throws this error — the tool call is made from a binding that is no longer authorized.

Solutions

  1. Check the run status in the DB/API; if paused, resume the run before issuing tool calls.
  2. Discard tool calls from finished/failed runs — do not retry stale runner callbacks.
  3. If the issue was reassigned, restart the work under the new run/agent binding.
  4. Verify the binding (companyId, agentId, issueId, runId) matches the actual heartbeat_runs/agents rows.

Example fix

// before
await runner.execute(call); // run already paused, throws
// after
const run = await getRun(binding.runId);
if (run.status === "running" && !ACTOR_BLOCKED.includes(run.actorStatus)) {
  await runner.execute(call);
}
Defensive patterns

Strategy: try-catch

Validate before calling

async function bindingIsLive(db, binding) {
  const [row] = await db.select({ run: heartbeatRuns, actor: agents })
    .from(heartbeatRuns).innerJoin(agents, eq(agents.id, binding.agentId))
    .where(eq(heartbeatRuns.id, binding.runId)).limit(1);
  return !!row && row.run.runtimeMode === "native" && row.run.status === "running"
    && !["paused","terminated","pending_approval","error"].includes(row.actor.status);
}

Type guard

const runIsExecutable = (row) => !!row && row.run.runtimeMode === "native" && row.run.status === "running" && !["paused","terminated","pending_approval","error"].includes(row.actor.status);

Try / catch

try {
  return await authority.execute(call);
} catch (e) {
  if (e.message === "paperclip_runner_tool_binding_not_authorized") {
    return respondSkipped("Run is no longer active; tool call discarded."); // do not retry stale calls
  }
  throw e;
}

Prevention

When it happens

Trigger: Any execute() (or #resolveReview via #boundContext) where the joined select returns no row, or row.run.runtimeMode !== 'native', or row.run.status !== 'running', or row.actor.status is one of 'paused'|'terminated'|'pending_approval'|'error'. Also when issues.assigneeAgentId/executionRunId no longer match the binding (non-review bindings).

Common situations: Run was paused (budget stop, approval gate) or errored but the runner process kept issuing tool calls; run finished and a stale callback arrives; issue was reassigned to another agent mid-run; runner replaying queued tool calls from a previous run id; binding ids corrupted or cross-company.

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/79e08fc6a15f6b33. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/paperclip-runner-tool-authority.ts:597

        eq(heartbeatRuns.id, this.binding.runId),
        eq(heartbeatRuns.companyId, this.binding.companyId),
        eq(heartbeatRuns.agentId, this.binding.agentId),
        eq(heartbeatRuns.nativeIssueId, this.binding.issueId),
        eq(issues.companyId, this.binding.companyId),
        ...(this.binding.nativeReview ? [] : [
          eq(issues.assigneeAgentId, this.binding.agentId),
          eq(issues.executionRunId, this.binding.runId),
        ]),
        eq(agents.companyId, this.binding.companyId),
      ))
      .limit(1);
    if (
      !row
      || row.run.runtimeMode !== "native"
      || row.run.status !== "running"
      || ["paused", "terminated", "pending_approval", "error"].includes(row.actor.status)
    ) {
      throw new Error("paperclip_runner_tool_binding_not_authorized");
    }
    if (this.binding.nativeReview) {
      const admitted = readNativeReviewAssignmentContext(row.run.contextSnapshot);
      if (!admitted || admitted.nativeReviewInteractionId !== this.binding.nativeReview.nativeReviewInteractionId
        || admitted.nativeReviewDecisionId !== this.binding.nativeReview.nativeReviewDecisionId) {
        throw forbidden("The run was not admitted for this review.");
      }
      const review = await getNativeReviewAssignment(this.db, {
        ...this.binding, contextSnapshot: this.binding.nativeReview,
        allowResolvedByRunId: this.binding.runId,
      });
      if (!review || (review.interaction.status === "pending" && row.issue.executionRunId !== this.binding.runId)) {
        throw forbidden("The assigned review is no longer available to this run.");
      }
    }
    return row;
  }

View on GitHub (pinned to 3f1d897a7c)