{"record":{"id":"630f5a1aedb6e492","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-binding-denied","errorCode":"paperclip_runner_chat_attachment_binding_denied","errorMessage":"paperclip_runner_chat_attachment_binding_denied","messagePattern":"paperclip_runner_chat_attachment_binding_denied","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":417,"sourceCode":"  };\n  if (link?.status === \"linked\") {\n    return Boolean(link.userId && (await activeMember(link.userId)));\n  }\n  if (!endpoint.allowUnlinkedPeople) return false;\n  return endpoint.sponsorUserId ? activeMember(endpoint.sponsorUserId) : true;\n}\n\n/** Caller must independently verify the current run/issue execution owner. */\nexport async function authorizeChatConversationForBoundRun(\n  tx: Db,\n  binding: ChatReuseBinding,\n  contextSnapshot: unknown,\n  lockMode: AuthorizationLockMode = \"blocking\",\n): Promise<AuthorizedConversation> {\n  let context = record(contextSnapshot);\n  if (context.source === \"issue.interaction.respond\") {\n    const answer = await resolveExternalChatQuestionResponse(tx, binding, context, lockMode);\n    if (!answer) throw new Error(\"paperclip_runner_chat_attachment_binding_denied\");\n    context = answer.authorizationContext;\n  }\n  const source = typeof context.source === \"string\" ? context.source : \"\";\n  const provider = [\n    \"slack\",\n    \"github\",\n    \"discord\",\n    \"microsoft-teams\",\n    \"telegram\",\n    \"imessage-photon\",\n  ].find(\n    (candidate) =>\n      source === `chat:${candidate}` || source === `chat:${candidate}:recovery`,\n  );\n  const commentIds = wakeCommentIds(context);\n  if (\n    !provider ||\n    (context.paperclipHarnessCheckedOut !== true &&","sourceCodeStart":399,"sourceCodeEnd":435,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L399-L435","documentation":"This error is thrown by authorizeChatConversationForBoundRun in chat-attachment-reuse.ts when a run attempt tries to reuse a chat conversation's attachments for a bound run, but the interaction-response lookup cannot resolve the external chat question response. When the wake context source is 'issue.interaction.respond', the function must find a matching unanswered external chat question in the bound conversation; if none resolves, the binding is not authorized. This is a deliberate security gate ensuring attachment reuse only happens for conversations genuinely tied to this issue/agent run.","triggerScenarios":"Calling authorizeChatConversationForBoundRun with a contextSnapshot whose source === 'issue.interaction.respond' where resolveExternalChatQuestionResponse returns null/undefined — i.e. no pending external chat question matches the binding (issueId/agentId/companyId) and the interaction comment referenced in the context.","commonSituations":"The interaction being responded to was already answered or superseded; the interaction comment belongs to a different issue or agent than the bound run; the external chat question was resolved between snapshot capture and authorization; stale/incorrect contextSnapshot replayed during run recovery; a caller reuses a snapshot from another chat provider.","solutions":["Verify the interaction comment being responded to still has a pending, unanswered external chat question matching this binding (same issueId, agentId, companyId).","Re-capture a fresh contextSnapshot at the time of authorization instead of replaying a stale one.","Confirm the run is actually bound to the chat conversation (paperclipHarnessCheckedOut / paperclipExternalChatExecutionBound flags set on a sibling code path).","If the response legitimately cannot resolve, do not attempt attachment reuse — fall back to normal message delivery without reused attachments."],"exampleFix":"// before: stale snapshot reused during recovery\nawait authorizeChatConversationForBoundRun(tx, binding, staleContext);\n// after: guard before calling\nif (staleContext.source === \"issue.interaction.respond\") {\n  const pending = await findPendingExternalChatQuestion(tx, binding, staleContext);\n  if (!pending) throw new SkipAttachmentReuse(); // don't force authorization\n}\nawait authorizeChatConversationForBoundRun(tx, binding, await refreshContext(staleContext));","handlingStrategy":"try-catch","validationCode":"// Before authorizing interaction-response reuse:\nif (context.source === \"issue.interaction.respond\") {\n  const pending = await db\n    .select({ id: issueInteractionComments.id })\n    .from(issueInteractionComments)\n    .where(and(\n      eq(issueInteractionComments.issueId, binding.issueId),\n      isNull(issueInteractionComments.answeredAt),\n    ));\n  if (pending.length === 0) throw new Error(\"no pending external chat question for binding\");\n}","typeGuard":"function hasPendingExternalChatQuestion(ctx: Record<string, unknown>): boolean {\n  return ctx.source === \"issue.interaction.respond\" &&\n    Array.isArray(ctx.wakeCommentIds) && ctx.wakeCommentIds.length > 0;\n}","tryCatchPattern":"try {\n  const conv = await authorizeChatConversationForBoundRun(tx, binding, ctx);\n} catch (err) {\n  if ((err as Error).message === \"paperclip_runner_chat_attachment_binding_denied\") {\n    // skip attachment reuse; deliver without reused attachments\n  } else throw err;\n}","preventionTips":["Always authorize against a freshly captured contextSnapshot, not one replayed from an earlier run.","Check the interaction is still pending before invoking attachment reuse.","Log the interaction comment ID in the catch path to diagnose mismatched bindings."],"tags":["authorization","chat","attachment-reuse","security-gate"],"backgroundTag":"permission-denied","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"}