{"record":{"id":"15111e58bf9909dd","repo":"paperclipai/paperclip","slug":"review-tool-authentication-is-unavailable","errorCode":null,"errorMessage":"Review tool authentication is unavailable","messagePattern":"Review tool authentication is unavailable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/paperclip-runner-tool-authority.ts","lineNumber":638,"sourceCode":"      throw badRequest(\"Choose accept or reject for this run's assigned review.\");\n    }\n    const reason = typeof input.reason === \"string\" ? input.reason.trim() : \"\";\n    if (input.decision === \"reject\" && !reason) throw badRequest(\"Explain the changes required before rejecting the review.\");\n    const context = await this.#boundContext();\n    const review = await getNativeReviewAssignment(this.db, {\n      ...this.binding, contextSnapshot: this.binding.nativeReview,\n      allowResolvedByRunId: this.binding.runId,\n    });\n    if (!review) throw forbidden(\"Review is no longer assigned to this run.\");\n    const expectedStatus = input.decision === \"accept\" ? \"accepted\" : \"rejected\";\n    if (review.interaction.status !== \"pending\") {\n      if (review.interaction.status !== expectedStatus) throw badRequest(\"This review already has a different decision.\");\n      return { interactionId: review.interaction.id, status: expectedStatus, deduplicated: true };\n    }\n    const apiUrl = this.binding.apiUrl ?? process.env.PAPERCLIP_API_URL;\n    const token = createLocalAgentJwt(this.binding.agentId, this.binding.companyId,\n      context.actor.adapterType, this.binding.runId, context.run.responsibleUserId);\n    if (!apiUrl || !token) throw new Error(\"Review tool authentication is unavailable\");\n    // Use the existing resolution route so authorization, activity, dependency\n    // wakes and request-changes continuation have one implementation.\n    const response = await fetch(`${apiUrl.replace(/\\/$/, \"\")}/api/issues/${this.binding.issueId}/interactions/${review.interaction.id}/${input.decision}`, {\n      method: \"POST\", redirect: \"error\", signal: AbortSignal.timeout(30_000),\n      headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\", \"X-Paperclip-Run-Id\": this.binding.runId },\n      body: JSON.stringify(input.decision === \"reject\" ? { reason } : {}),\n    });\n    if (!response.ok) throw new Error(`Review decision was not accepted (${response.status}): ${(await response.text()).slice(0, 2_000)}`);\n    return { interactionId: review.interaction.id, status: expectedStatus };\n  }\n\n  async #reportProgress(input: Record<string, unknown>): Promise<unknown> {\n    const body = typeof input.body === \"string\" ? input.body.trim() : \"\";\n    const idempotencyKey = typeof input.idempotencyKey === \"string\" ? input.idempotencyKey.trim() : \"\";\n    if (!body || !idempotencyKey) throw new Error(\"paperclip_runner_tool_input_invalid\");\n    let publication: Awaited<ReturnType<typeof persistActivity>>[\"publication\"] | null = null;\n    const result = await this.#withMutationReceipt(\n      \"report_progress\",","sourceCodeStart":620,"sourceCodeEnd":656,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/paperclip-runner-tool-authority.ts#L620-L656","documentation":"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.","triggerScenarios":"#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).","commonSituations":"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.","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."],"exampleFix":"// before\nconst authority = new RunnerToolAuthority({ db, binding: { ...binding, nativeReview } }); // PAPERCLIP_API_URL unset in env\n// after\nconst authority = new RunnerToolAuthority({ db, binding: { ...binding, nativeReview, apiUrl: process.env.PAPERCLIP_API_URL ?? \"http://localhost:3100\" } });","handlingStrategy":"validation","validationCode":"function assertReviewAuth(binding) {\n  const apiUrl = binding.apiUrl ?? process.env.PAPERCLIP_API_URL;\n  if (!apiUrl) throw new Error(\"PAPERCLIP_API_URL must be set to resolve reviews\");\n  return apiUrl;\n}","typeGuard":"const canResolveReview = (binding) => Boolean(binding.apiUrl ?? process.env.PAPERCLIP_API_URL);","tryCatchPattern":"try {\n  return await authority.resolveReview(input);\n} catch (e) {\n  if (e.message === \"Review tool authentication is unavailable\") {\n    return respondSkipped(\"Review cannot be submitted: runner lacks API URL/token configuration.\");\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["authentication","missing-env-var","review","runner-tools"],"backgroundTag":"missing-env-var","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}