paperclipai/paperclip · error · Error

Skill tool authentication is unavailable

Error message

Skill tool authentication is unavailable

What it means

The create_skill tool call-back to the Paperclip API needs both an API URL and an agent JWT. The URL comes from binding.apiUrl ?? process.env.PAPERCLIP_API_URL and the token from createLocalAgentJwt(...). This error means at least one of those was missing/empty, so the skill-creation request cannot be authenticated.

Solutions

  1. Set PAPERCLIP_API_URL (e.g., http://localhost:3100) in the runner's environment.
  2. Pass apiUrl explicitly in the runner binding so it doesn't depend on the env var.
  3. Verify the server constructs the binding with agentId/companyId/runId so createLocalAgentJwt can mint a token.
  4. Check that context.actor.adapterType and context.run.responsibleUserId are populated before tool execution.

Example fix

// before
PAPERCLIP_API_URL= node dist/server.js  // unset
// after
PAPERCLIP_API_URL=http://localhost:3100 node dist/server.js
Defensive patterns

Strategy: validation

Validate before calling

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

Type guard

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

Try / catch

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

Prevention

When it happens

Trigger: execute() handles call.tool === 'create_skill', the mode check passes, and either apiUrl is falsy (binding.apiUrl unset AND PAPERCLIP_API_URL env var missing/empty) or createLocalAgentJwt returned an empty token (missing agent/company/run/actor inputs).

Common situations: Runner launched outside the server process without PAPERCLIP_API_URL set; deployment config dropped the env var; binding.apiUrl not injected by the runner host; createLocalAgentJwt inputs (adapterType, responsibleUserId) missing from context.

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/79d51df4eca2f6e3. Report an issue: GitHub.

Appendix: source

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

    if (call.tool === REUSE_CHAT_ATTACHMENT_TOOL_NAME) {
      return this.#reuseChatAttachment(input);
    }
    const descriptor = CAPABILITY_SEMANTIC_TOOL_CATALOG.find(
      (candidate) => candidate.operationId === call.tool,
    );
    if (
      !descriptor ||
      !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));

View on GitHub (pinned to 3f1d897a7c)