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

  1. Add the missing `case "<tool>": return this.#handler(input);` arm in execute() for the unbound operation.
  2. Remove the operation from IMPLEMENTED_OPERATIONS/CAPABILITY_SEMANTIC_TOOL_CATALOG if it should not be callable.
  3. Add a test asserting every IMPLEMENTED_OPERATIONS entry has a matching switch case so the wiring cannot drift.
  4. 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

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.