{"record":{"id":"5afaf0898aa78949","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-read-not-authorized","errorCode":"paperclip_runner_chat_attachment_read_not_authorized","errorMessage":"paperclip_runner_chat_attachment_read_not_authorized","messagePattern":"paperclip_runner_chat_attachment_read_not_authorized","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-read.ts","lineNumber":140,"sourceCode":"  }\n\n  async #authorizeOnce(input: {\n    sourceCommentId: string;\n    attachmentId: string;\n  }): Promise<ChatAttachmentReuseSource> {\n    return this.options.db.transaction(async (transaction) => {\n      const tx = transaction as unknown as Db;\n      // Source rows are also locked by the existing lineage reader. Bound\n      // their waits so an inverse source-writer lock order cannot deadlock.\n      await tx.execute(sql`set local lock_timeout = '50ms'`);\n      const authorization =\n        await resolveExternalChatResponseWaitAuthorizationInTransaction(\n          tx,\n          this.options.binding,\n          \"nonblocking\",\n        );\n      if (authorization !== \"authorized\")\n        throw new Error(\"paperclip_runner_chat_attachment_read_not_authorized\");\n      const [run] = await tx\n        .select({ contextSnapshot: heartbeatRuns.contextSnapshot })\n        .from(heartbeatRuns)\n        .where(eq(heartbeatRuns.id, this.options.binding.runId));\n      const source = await authorizeChatAttachmentReuse({\n        db: tx,\n        binding: this.options.binding,\n        contextSnapshot: run!.contextSnapshot,\n        allowEmpty: true,\n        ...input,\n      });\n      this.#assertOpen();\n      return source;\n    });\n  }\n\n  async #read(input: { sourceCommentId: string; attachmentId: string }) {\n    const source = await this.#authorized(input);","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-read.ts#L122-L158","documentation":"Before staging any bytes, the scope verifies in a transaction that the run's external-chat response-wait authorization still resolves to \"authorized\" (and that the specific source comment/attachment is reusable via authorizeChatAttachmentReuse). If authorization is anything else — expired wait, revoked consent, wrong conversation, or the attachment not in the authorized source set — this error is thrown and no bytes are read.","triggerScenarios":"Reading an attachment whose source comment is outside the run's authorized context snapshot; the external-chat wait window closed or was revoked before the read; run binding points at a different conversation than the attachment; allowEmpty authorization fails for stale lineage.","commonSituations":"Agent attempting to read attachments from another conversation; user rescinding chat integration consent mid-run; run resumed after its authorization window expired; stale ids from an older turn used in a new run.","solutions":["Only pass sourceCommentId/attachmentId pairs returned by list_chat_attachments in the same run and conversation binding.","Re-issue the external-chat authorization (restart the wait/approval flow) if it expired or was revoked.","Verify the run binding (runId/company/conversation) still matches the chat the attachment belongs to.","Check authorizeChatAttachmentReuse results to confirm the source is still part of the authorized lineage."],"exampleFix":"// before\nawait scope.read({ sourceCommentId: otherChatCommentId, attachmentId });\n// after\nconst listed = await listChatAttachments({ sourceCommentId: authorizedCommentId });\nawait scope.read({ sourceCommentId: authorizedCommentId, attachmentId: listed.attachments[0].id });","handlingStrategy":"try-catch","validationCode":"const authorized = await listChatAttachments({ sourceCommentId });\nif (!authorized.some(a => a.id === attachmentId)) throw new Error(\"attachment not in authorized set\");","typeGuard":"function isNotAuthorizedError(e: unknown): boolean {\n  return e instanceof Error && e.message === \"paperclip_runner_chat_attachment_read_not_authorized\";\n}","tryCatchPattern":"try {\n  return await scope.read(input);\n} catch (e) {\n  if (isNotAuthorizedError(e)) {\n    return { status: \"authorization_required\" }; // re-run approval flow\n  }\n  throw e;\n}","preventionTips":["Only read attachments listed by list_chat_attachments within the same run binding.","Re-establish external-chat authorization before resuming long-idle runs.","Never carry attachment ids across conversations or run bindings.","Surface authorization state to the agent before attempting reads."],"tags":["authorization","permission","policy"],"backgroundTag":"permission-denied","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T10:30:35.592Z"}