paperclipai/paperclip · error
paperclip_runner_tool_not_bound
paperclip_runner_tool_not_bound
Error message
paperclip_runner_tool_not_bound
What it means
After passing advertisement and mode checks, execute() dispatches the tool to a private handler via a switch over operationId. At paperclip-runner-tool-authority.ts:238 the default arm throws 'paperclip_runner_tool_not_bound' when the tool name is implemented and advertised but has no dispatch case wired to a handler. This is an internal wiring invariant: every entry in IMPLEMENTED_OPERATIONS (minus the ones handled earlier, like search_api/call_api and connection tools) must have a corresponding case in the switch.
Source
Thrown at server/src/services/native-runtime/paperclip-runner-tool-authority.ts:238
}
case "get_approval_context": {
const approval = await this.#approval(requiredString(input.approvalId));
const tasks = await this.db.select({ issue: issues }).from(issueApprovals)
.innerJoin(issues, eq(issues.id, issueApprovals.issueId))
.where(and(
eq(issueApprovals.approvalId, approval.id),
eq(issueApprovals.companyId, this.binding.companyId),
eq(issues.companyId, this.binding.companyId),
));
return { approval, tasks: tasks.map((row) => row.issue) };
}
case "report_progress": return this.#reportProgress(input);
case "request_human_input": return this.#requestHumanInput(input,
(await captureRunIdentity(this.db, this.binding)).context?.id ?? null);
case "create_task": return this.#createTask(input,
(await captureRunIdentity(this.db, this.binding)).context?.id ?? null);
case "set_dependencies": return this.#setDependencies(input);
default: throw new Error("paperclip_runner_tool_not_bound");
}
}
async #callApi(callId: string, value: unknown): Promise<unknown> {
const bound = await this.#boundContext();
const context = { ...this.binding, issueIdentifier: bound.issue.identifier, workMode: bound.issue.workMode };
const { input, operation } = validateRunnerApiCall(value, context);
const apiUrl = this.binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
if (!apiUrl) throw new Error("Paperclip API origin is unavailable");
const token = createLocalAgentJwt(this.binding.agentId, this.binding.companyId, bound.actor.adapterType, this.binding.runId, bound.run.responsibleUserId);
if (!token) throw new Error("Paperclip run authentication is unavailable");
const execute = async () => {
const current = await this.#boundContext();
if (!runnerApiToolsEnabled(this.binding.companyId, this.binding.apiToolsEnabled)) throw new Error("paperclip_runner_tool_not_advertised");
return executeRunnerApi(input, { ...context, workMode: current.issue.workMode }, {
apiUrl, token,
beforeDispatch: async () => {
const fresh = await this.#boundContext();View on GitHub (pinned to 01ad858492)
Solutions
- Add the missing `case "<tool>": return this.#handler(input);` arm in execute() for the unbound operation.
- Remove the operation from IMPLEMENTED_OPERATIONS/CAPABILITY_SEMANTIC_TOOL_CATALOG if it should not be callable.
- Add a test asserting every IMPLEMENTED_OPERATIONS entry has a matching switch case so the wiring cannot drift.
- Rebuild/redeploy so catalog and dispatcher come from the same build if version skew caused it.
Example fix
// before
case "set_dependencies": return this.#setDependencies(input);
default: throw new Error("paperclip_runner_tool_not_bound");
// after
case "set_dependencies": return this.#setDependencies(input);
case "new_operation": return this.#newOperation(input); // bind every implemented operation
default: throw new Error("paperclip_runner_tool_not_bound"); Defensive patterns
Strategy: try-catch
Validate before calling
// Build-time exhaustive check: every implemented operation must have a dispatch arm
const BOUND_TOOLS = new Set(["search_api", "call_api", "get_task_context", "get_task_history", "search_tasks", "list_documents", "read_document", "write_document", "list_document_revisions", "list_agents", "get_agent", "list_approvals", "get_approval", "get_approval_context", "report_progress", "request_human_input", "create_task", "set_dependencies"]);
for (const op of IMPLEMENTED_OPERATIONS) if (!BOUND_TOOLS.has(op)) throw new Error(`operation ${op} implemented but not bound in execute()`); Try / catch
try {
return await authority.execute(call);
} catch (err) {
if (err instanceof Error && err.message === "paperclip_runner_tool_not_bound") {
logger.error({ tool: call.tool }, "implemented operation missing dispatch arm");
return { error: "internal_tool_wiring", hint: "Report this — the tool exists in the catalog but has no handler." };
}
throw err;
} Prevention
- Enforce exhaustiveness with a test mapping IMPLEMENTED_OPERATIONS to switch cases in execute().
- Add the dispatch arm in the same PR that adds an operation to IMPLEMENTED_OPERATIONS.
- Prefer a Record<operationId, handler> map over a switch so TypeScript's exhaustiveness checking flags missing handlers.
- Run targeted runner-tool tests after any catalog or dispatcher change.
When it happens
Trigger: execute() reaches the switch with a call.tool that survived the IMPLEMENTED_OPERATIONS check but matches no case — e.g. an operation was added to IMPLEMENTED_OPERATIONS/CAPABILITY_SEMANTIC_TOOL_CATALOG without adding a `case` in execute(), or a code change renamed a case while the catalog still uses the old operationId.
Common situations: A developer added a new capability-semantic tool to the catalog and IMPLEMENTED_OPERATIONS but forgot the switch arm; a merge dropped a case; an operation was moved to a different dispatcher but still listed as implemented; version skew where catalog and dispatcher come from different builds.
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/83c6dcbaa3d1e32b.
Report an issue: GitHub.