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
- Set PAPERCLIP_API_URL (e.g., http://localhost:3100) in the runner's environment.
- Pass apiUrl explicitly in the runner binding so it doesn't depend on the env var.
- Verify the server constructs the binding with agentId/companyId/runId so createLocalAgentJwt can mint a token.
- 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
- Set PAPERCLIP_API_URL in every environment that launches native runners.
- Prefer passing apiUrl explicitly via the runner binding over relying on env vars.
- Health-check tool authentication (URL present, JWT mintable) at runner startup.
- Keep run rows populated with responsibleUserId so JWT creation succeeds.
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Project tool authentication is unavailable
- Review tool authentication is unavailable
- Remote Codex working directory requires an assigned…
- ACPX profile requires exact model ; received
- ACPX model must not be empty
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)