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
- 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.
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
- 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.
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
- paperclip_runner_chat_attachment_principal_denied
- paperclip_runner_chat_attachment_read_scope_unavailable
- paperclip_runner_chat_attachment_source_denied
- paperclip_runner_file_handoff_not_authorized
- paperclip_runner_tool_binding_not_authorized
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)