paperclipai/paperclip · error · Error

Review tool authentication is unavailable

Error message

Review tool authentication is unavailable

What it means

Review resolution posts the decision to the Paperclip API's interaction route using binding.apiUrl ?? PAPERCLIP_API_URL and a locally minted agent JWT. This error means either the API URL or the token was unavailable when the runner attempted to submit its accept/reject decision for an assigned review.

Solutions

  1. Set PAPERCLIP_API_URL in the runner's environment or supply binding.apiUrl.
  2. Ensure the run row has responsibleUserId and the actor has adapterType so createLocalAgentJwt produces a token.
  3. Verify binding.agentId/companyId/runId are populated on the tool authority binding.
  4. If running off-process, propagate the Paperclip API base URL and credentials through the runner launch config.

Example fix

// before
const authority = new RunnerToolAuthority({ db, binding: { ...binding, nativeReview } }); // PAPERCLIP_API_URL unset in env
// after
const authority = new RunnerToolAuthority({ db, binding: { ...binding, nativeReview, apiUrl: process.env.PAPERCLIP_API_URL ?? "http://localhost:3100" } });
Defensive patterns

Strategy: validation

Validate before calling

function assertReviewAuth(binding) {
  const apiUrl = binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
  if (!apiUrl) throw new Error("PAPERCLIP_API_URL must be set to resolve reviews");
  return apiUrl;
}

Type guard

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

Try / catch

try {
  return await authority.resolveReview(input);
} catch (e) {
  if (e.message === "Review tool authentication is unavailable") {
    return respondSkipped("Review cannot be submitted: runner lacks API URL/token configuration.");
  }
  throw e;
}

Prevention

When it happens

Trigger: #resolveReview reaches the actual decision POST (review is pending and not already decided) and either apiUrl is falsy (binding.apiUrl unset AND PAPERCLIP_API_URL missing/empty) or createLocalAgentJwt(...) returns an empty token (missing adapterType/responsibleUserId/binding ids).

Common situations: Runner process lacks PAPERCLIP_API_URL; binding built without apiUrl; run row has no responsibleUserId so JWT minting fails; running the tool authority in isolation (tests, scripts) without the API 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/15111e58bf9909dd. Report an issue: GitHub.

Appendix: source

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

      throw badRequest("Choose accept or reject for this run's assigned review.");
    }
    const reason = typeof input.reason === "string" ? input.reason.trim() : "";
    if (input.decision === "reject" && !reason) throw badRequest("Explain the changes required before rejecting the review.");
    const context = await this.#boundContext();
    const review = await getNativeReviewAssignment(this.db, {
      ...this.binding, contextSnapshot: this.binding.nativeReview,
      allowResolvedByRunId: this.binding.runId,
    });
    if (!review) throw forbidden("Review is no longer assigned to this run.");
    const expectedStatus = input.decision === "accept" ? "accepted" : "rejected";
    if (review.interaction.status !== "pending") {
      if (review.interaction.status !== expectedStatus) throw badRequest("This review already has a different decision.");
      return { interactionId: review.interaction.id, status: expectedStatus, deduplicated: true };
    }
    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("Review tool authentication is unavailable");
    // Use the existing resolution route so authorization, activity, dependency
    // wakes and request-changes continuation have one implementation.
    const response = await fetch(`${apiUrl.replace(/\/$/, "")}/api/issues/${this.binding.issueId}/interactions/${review.interaction.id}/${input.decision}`, {
      method: "POST", redirect: "error", signal: AbortSignal.timeout(30_000),
      headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "X-Paperclip-Run-Id": this.binding.runId },
      body: JSON.stringify(input.decision === "reject" ? { reason } : {}),
    });
    if (!response.ok) throw new Error(`Review decision was not accepted (${response.status}): ${(await response.text()).slice(0, 2_000)}`);
    return { interactionId: review.interaction.id, status: expectedStatus };
  }

  async #reportProgress(input: Record<string, unknown>): Promise<unknown> {
    const body = typeof input.body === "string" ? input.body.trim() : "";
    const idempotencyKey = typeof input.idempotencyKey === "string" ? input.idempotencyKey.trim() : "";
    if (!body || !idempotencyKey) throw new Error("paperclip_runner_tool_input_invalid");
    let publication: Awaited<ReturnType<typeof persistActivity>>["publication"] | null = null;
    const result = await this.#withMutationReceipt(
      "report_progress",

View on GitHub (pinned to 3f1d897a7c)