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
- Set PAPERCLIP_API_URL in the runner's environment or supply binding.apiUrl.
- Ensure the run row has responsibleUserId and the actor has adapterType so createLocalAgentJwt produces a token.
- Verify binding.agentId/companyId/runId are populated on the tool authority binding.
- 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
- Ensure PAPERCLIP_API_URL is set wherever native review runners execute.
- Pass binding.apiUrl explicitly for review-enabled bindings.
- Keep run rows' responsibleUserId populated so the agent JWT can be minted.
- Probe API reachability at runner startup when the binding includes nativeReview.
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Project tool authentication is unavailable
- Skill tool authentication is unavailable
- Agent identity is required
- ANTHROPIC_API_KEY is required in the CLI process environment
- Cloud control assertion is not a compact JWS
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)