paperclipai/paperclip · critical

Paperclip run authentication is unavailable

Error message

Paperclip run authentication is unavailable

What it means

executeRunnerApi refuses to dispatch an HTTP request when io.token is falsy, throwing "Paperclip run authentication is unavailable". The runner API client requires a Bearer token (local agent JWT) plus the X-Paperclip-Run-Id header for every call; a missing token would be an unauthenticated request, so it fails closed before any network I/O.

Source

Thrown at server/src/services/native-runtime/runner-api-client.ts:144

  try {
    while (true) {
      const next = await reader.read();
      if (next.done) break;
      bytes += next.value.byteLength;
      if (bytes > maxBytes) throw unprocessable("API response exceeds the transfer limit; narrow the request");
      chunks.push(next.value);
    }
  } finally {
    await reader.cancel().catch(() => {});
    reader.releaseLock();
  }
  return Buffer.concat(chunks);
}

export async function executeRunnerApi(input: RunnerApiCall, context: RunnerApiContext, io: RunnerApiIo) {
  const { operation } = validateRunnerApiCall(input, context);
  const url = runnerApiUrl(operation, input, context, io.apiUrl);
  if (!io.token) throw new Error("Paperclip run authentication is unavailable");
  const headers = new Headers({ Authorization: `Bearer ${io.token}`, "X-Paperclip-Run-Id": context.runId });
  let body: BodyInit | undefined;
  const contentType = input.contentType ?? (input.files?.length ? "multipart/form-data" : "application/json");
  if (/\r|\n/.test(contentType)) throw badRequest("Invalid content type");
  let totalBytes = 0;
  if (input.files?.length) {
    if (["GET", "HEAD"].includes(operation.method)) throw badRequest("Read requests cannot upload files");
    const form = new FormData();
    if (input.body !== undefined && (!input.body || typeof input.body !== "object" || Array.isArray(input.body))) throw badRequest("Multipart body must be an object of form fields");
    for (const [key, value] of Object.entries((input.body ?? {}) as Record<string, unknown>)) {
      const text = typeof value === "string" ? value : JSON.stringify(value);
      totalBytes += Buffer.byteLength(text);
      form.append(key, text);
    }
    for (const file of input.files) {
      const resolved = await io.readFile(file);
      totalBytes += resolved.bytes.length;
      if (totalBytes > RUNNER_API_MAX_BYTES) throw badRequest("API upload exceeds the transfer limit");

View on GitHub (pinned to 01ad858492)

Solutions

  1. Configure the server JWT signing config so createLocalAgentJwt returns a real token, then retry the run.
  2. At call sites, guard the token before invoking executeRunnerApi: if (!token) throw/handle before building io.
  3. In tests, pass an explicit signed test token in RunnerApiIo instead of leaving it undefined.
  4. Check startup logs for JWT config initialization failures and fix the underlying secret/key setup.

Example fix

// before
await executeRunnerApi(input, context, { apiUrl, token: maybeToken, ... });
// after
if (!maybeToken) throw new Error("Paperclip run authentication is unavailable");
await executeRunnerApi(input, context, { apiUrl, token: maybeToken, ... });
Defensive patterns

Strategy: type-guard

Validate before calling

if (!io.token || typeof io.token !== "string") {
  throw new Error("RunnerApiIo.token must be a non-empty JWT before calling executeRunnerApi");
}

Type guard

function hasToken(io: RunnerApiIo): io is RunnerApiIo & { token: string } {
  return typeof io.token === "string" && io.token.length > 0;
}

Try / catch

try {
  const res = await executeRunnerApi(input, context, io);
} catch (err) {
  if (err instanceof Error && err.message === "Paperclip run authentication is unavailable") {
    // re-mint the JWT (fix JWT config) and retry once
  } else throw err;
}

Prevention

When it happens

Trigger: executeRunnerApi is invoked with io.token undefined/null/empty — i.e. the caller (e.g. PaperclipRunnerToolAuthority.#callApi) passed the result of createLocalAgentJwt without checking, which is null when the server JWT config is missing; or a direct caller of executeRunnerApi constructs io without a token.

Common situations: Server deployed without JWT signing configuration; test harness calling executeRunnerApi directly with a stub io object omitting token; token-minting code short-circuited after a config change and the null propagated to the client.

Understand the failure class

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/b479675ffa766faf. Report an issue: GitHub.