paperclipai/paperclip · error · Error

Project tool authentication is unavailable

Error message

Project tool authentication is unavailable

What it means

The project tools (create_project, list_project_repositories, list_projects) call back into the Paperclip API using binding.apiUrl ?? PAPERCLIP_API_URL and a locally minted agent JWT. This error means the API URL or token could not be produced, so the project tool cannot make an authenticated request.

Solutions

  1. Set PAPERCLIP_API_URL in the runner environment or pass binding.apiUrl.
  2. Ensure the run/actor context includes responsibleUserId and adapterType so createLocalAgentJwt succeeds.
  3. Confirm binding.agentId and binding.companyId are set when constructing the tool authority.
  4. In containerized runners, propagate the Paperclip API env/config at launch time.

Example fix

// before
const authority = new RunnerToolAuthority({ db, binding: { agentId, issueId, runId } }); // no apiUrl
// after
const authority = new RunnerToolAuthority({ db, binding: { agentId, issueId, runId, apiUrl: process.env.PAPERCLIP_API_URL } });
Defensive patterns

Strategy: validation

Validate before calling

function assertProjectToolAuth(binding) {
  const apiUrl = binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
  if (!apiUrl) throw new Error("PAPERCLIP_API_URL must be set for project tools");
  return apiUrl;
}

Type guard

const hasProjectAuth = (binding) => Boolean(binding.apiUrl ?? process.env.PAPERCLIP_API_URL);

Try / catch

try {
  return await authority.execute(call);
} catch (e) {
  if (e.message === "Project tool authentication is unavailable") {
    return respondSkipped("Project tools require PAPERCLIP_API_URL; fix runner config and retry.");
  }
  throw e;
}

Prevention

When it happens

Trigger: execute() routes call.tool to the create_project/list_project_repositories/list_projects case and either apiUrl (binding.apiUrl ?? process.env.PAPERCLIP_API_URL) is falsy or createLocalAgentJwt(this.binding.agentId, this.binding.companyId, context.actor.adapterType, this.binding.runId, context.run.responsibleUserId) returns an empty token.

Common situations: Runner process started without PAPERCLIP_API_URL (local CLI, container missing env); binding constructed without apiUrl; run row missing responsibleUserId so token creation fails; agent/agentId context incomplete in test harnesses.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/f5340dace10e3a63. Report an issue: GitHub.

Appendix: source

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

      !descriptor.allowedModes.includes(
        context.issue.workMode as "standard" | "planning" | "ask",
      )
    ) {
      throw new Error("paperclip_runner_tool_mode_denied");
    }
    switch (call.tool) {
      case "create_skill": {
        const apiUrl = this.binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
        const token = createLocalAgentJwt(this.binding.agentId, this.binding.companyId, context.actor.adapterType, this.binding.runId, context.run.responsibleUserId);
        if (!apiUrl || !token) throw new Error("Skill tool authentication is unavailable");
        return callCreateSkillTool({ arguments: input, apiUrl, token, companyId: this.binding.companyId });
      }
      case "create_project":
      case "list_project_repositories":
      case "list_projects": {
        const apiUrl = this.binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
        const token = createLocalAgentJwt(this.binding.agentId, this.binding.companyId, context.actor.adapterType, this.binding.runId, context.run.responsibleUserId);
        if (!apiUrl || !token) throw new Error("Project tool authentication is unavailable");
        return callProjectTool({ name: call.tool, arguments: input, apiUrl, token,
          companyId: this.binding.companyId, issueId: this.binding.issueId, agentId: this.binding.agentId,
          conversation: Boolean(context.issue.conversationAgentId) });
      }
      case "search_api": return searchRunnerApi(call.arguments);
      case "call_api": {
        // PRP reserves operationId/callId for semantic result identity. The
        // HTTP operation is metadata, including in previously saved receipts;
        // exposing it as operationId makes the runner reject a valid response.
        const { operationId, ...response } = record(await this.#callApi(call.callId, call.arguments));
        return { ...response, apiOperationId: operationId };
      }
      case "get_task_context": return {
        company: { id: this.binding.companyId },
        actor: redactedActor(context.actor),
        activeTask: redactedTask(context.issue),
        run: {
          id: this.binding.runId,

View on GitHub (pinned to 3f1d897a7c)