{"record":{"id":"79e08fc6a15f6b33","repo":"paperclipai/paperclip","slug":"paperclip-runner-tool-binding-not-authorized-79e08f","errorCode":"paperclip_runner_tool_binding_not_authorized","errorMessage":"paperclip_runner_tool_binding_not_authorized","messagePattern":"paperclip_runner_tool_binding_not_authorized","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/paperclip-runner-tool-authority.ts","lineNumber":597,"sourceCode":"        eq(heartbeatRuns.id, this.binding.runId),\n        eq(heartbeatRuns.companyId, this.binding.companyId),\n        eq(heartbeatRuns.agentId, this.binding.agentId),\n        eq(heartbeatRuns.nativeIssueId, this.binding.issueId),\n        eq(issues.companyId, this.binding.companyId),\n        ...(this.binding.nativeReview ? [] : [\n          eq(issues.assigneeAgentId, this.binding.agentId),\n          eq(issues.executionRunId, this.binding.runId),\n        ]),\n        eq(agents.companyId, this.binding.companyId),\n      ))\n      .limit(1);\n    if (\n      !row\n      || row.run.runtimeMode !== \"native\"\n      || row.run.status !== \"running\"\n      || [\"paused\", \"terminated\", \"pending_approval\", \"error\"].includes(row.actor.status)\n    ) {\n      throw new Error(\"paperclip_runner_tool_binding_not_authorized\");\n    }\n    if (this.binding.nativeReview) {\n      const admitted = readNativeReviewAssignmentContext(row.run.contextSnapshot);\n      if (!admitted || admitted.nativeReviewInteractionId !== this.binding.nativeReview.nativeReviewInteractionId\n        || admitted.nativeReviewDecisionId !== this.binding.nativeReview.nativeReviewDecisionId) {\n        throw forbidden(\"The run was not admitted for this review.\");\n      }\n      const review = await getNativeReviewAssignment(this.db, {\n        ...this.binding, contextSnapshot: this.binding.nativeReview,\n        allowResolvedByRunId: this.binding.runId,\n      });\n      if (!review || (review.interaction.status === \"pending\" && row.issue.executionRunId !== this.binding.runId)) {\n        throw forbidden(\"The assigned review is no longer available to this run.\");\n      }\n    }\n    return row;\n  }\n","sourceCodeStart":579,"sourceCodeEnd":615,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/paperclip-runner-tool-authority.ts#L579-L615","documentation":"#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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Check the run status in the DB/API; if paused, resume the run before issuing tool calls.","Discard tool calls from finished/failed runs — do not retry stale runner callbacks.","If the issue was reassigned, restart the work under the new run/agent binding.","Verify the binding (companyId, agentId, issueId, runId) matches the actual heartbeat_runs/agents rows."],"exampleFix":"// before\nawait runner.execute(call); // run already paused, throws\n// after\nconst run = await getRun(binding.runId);\nif (run.status === \"running\" && !ACTOR_BLOCKED.includes(run.actorStatus)) {\n  await runner.execute(call);\n}","handlingStrategy":"try-catch","validationCode":"async function bindingIsLive(db, binding) {\n  const [row] = await db.select({ run: heartbeatRuns, actor: agents })\n    .from(heartbeatRuns).innerJoin(agents, eq(agents.id, binding.agentId))\n    .where(eq(heartbeatRuns.id, binding.runId)).limit(1);\n  return !!row && row.run.runtimeMode === \"native\" && row.run.status === \"running\"\n    && ![\"paused\",\"terminated\",\"pending_approval\",\"error\"].includes(row.actor.status);\n}","typeGuard":"const runIsExecutable = (row) => !!row && row.run.runtimeMode === \"native\" && row.run.status === \"running\" && ![\"paused\",\"terminated\",\"pending_approval\",\"error\"].includes(row.actor.status);","tryCatchPattern":"try {\n  return await authority.execute(call);\n} catch (e) {\n  if (e.message === \"paperclip_runner_tool_binding_not_authorized\") {\n    return respondSkipped(\"Run is no longer active; tool call discarded.\"); // do not retry stale calls\n  }\n  throw e;\n}","preventionTips":["Stop issuing tool calls as soon as the run is paused, errored, or finished.","Drain/discard queued tool callbacks when a run's status leaves 'running'.","Re-check binding identity after any issue reassignment before continuing tool execution.","Never reuse runner bindings across runs or after a run restart."],"tags":["authorization","run-state","binding-mismatch","native-runtime"],"backgroundTag":"permission-denied","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}