{"record":{"id":"0233b810846d5b5c","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-read-source-changed","errorCode":"paperclip_runner_chat_attachment_read_source_changed","errorMessage":"paperclip_runner_chat_attachment_read_source_changed","messagePattern":"paperclip_runner_chat_attachment_read_source_changed","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-read.ts","lineNumber":167,"sourceCode":"        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);\n    const body = await this.#bytes(source);\n    const current = await this.#authorized(input);\n    if (\n      current.objectKey !== source.objectKey ||\n      current.sha256 !== source.sha256 ||\n      current.byteSize !== source.byteSize ||\n      current.contentType !== source.contentType\n    ) {\n      throw new Error(\"paperclip_runner_chat_attachment_read_source_changed\");\n    }\n    // The committed revalidation admits these exact verified bytes. Never\n    // hold issue/endpoint/principal locks over filesystem work (including\n    // descriptor inspection and fsync), which can delay inbound admission.\n    this.#assertOpen();\n    const staged = await stageNativeRunnerAttachmentBytes({\n      workspaceRoot: this.options.workspaceRoot,\n      body,\n    });\n    this.#cleanups.push(staged.cleanup);\n    try {\n      // Cancellation during asynchronous staging must never publish a path.\n      this.#assertOpen();\n      return {\n        sourceCommentId: source.sourceCommentId,\n        attachmentId: source.attachmentId,\n        filename: current.filename,\n        contentType: current.contentType,","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-read.ts#L149-L185","documentation":"The scope reads bytes, then re-runs authorization and compares the freshly authorized source (objectKey, sha256, byteSize, contentType) against the source used for the read. If any field differs — i.e. the stored attachment changed between the first authorization and the committed revalidation — the staged content is considered untrustworthy and this error is thrown instead of publishing a path.","triggerScenarios":"The same attachmentId was re-uploaded/overwritten in storage (new objectKey or hash) while the read was in flight; byteSize or contentType metadata updated between the two authorizations; a migration or re-ingest rewrote the object mid-read.","commonSituations":"Users editing/replacing attachments while an agent run is reading them; background jobs re-encrypting or re-hashing stored objects; TOCTOU races under active chat mutation.","solutions":["Retry the read; a stable attachment will pass the second authorization unchanged.","Identify what rewrote the attachment (re-upload, migration) and serialize it against run reads.","If attachments are immutable by design, fix the writer that mutated objectKey/sha256 in place.","Surface this as 'attachment changed during read; please re-select' in agent-facing UX."],"exampleFix":"// before\nconst file = await scope.read(input); // throws if source mutated mid-read\n// after\nlet file;\nfor (let i = 0; i < 3; i++) {\n  try { file = await scope.read(input); break; }\n  catch (e) {\n    if (e.message !== \"paperclip_runner_chat_attachment_read_source_changed\") throw e;\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isSourceChangedError(e: unknown): boolean {\n  return e instanceof Error && e.message === \"paperclip_runner_chat_attachment_read_source_changed\";\n}","tryCatchPattern":"try {\n  return await scope.read(input);\n} catch (e) {\n  if (isSourceChangedError(e)) {\n    return { status: \"attachment_changed\" }; // re-list and re-read, don't trust old bytes\n  }\n  throw e;\n}","preventionTips":["Treat stored attachments as immutable; never overwrite objectKey/sha256 in place.","Serialize attachment replacement against run reads.","Re-run list_chat_attachments after a change error before retrying.","Alert on any pipeline that mutates stored objects during runs."],"tags":["integrity","toctou","concurrency","verification"],"backgroundTag":"checksum-mismatch","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"}