{"record":{"id":"5979fc19c1a7e7ca","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-principal-denied","errorCode":"paperclip_runner_chat_attachment_principal_denied","errorMessage":"paperclip_runner_chat_attachment_principal_denied","messagePattern":"paperclip_runner_chat_attachment_principal_denied","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":540,"sourceCode":"              eq(chatEndpointResources.companyId, binding.companyId),\n              eq(chatEndpointResources.endpointId, endpoint.id),\n            ),\n          );\n        return lockMode === \"read\"\n          ? query.then((rows) => rows[0] ?? null)\n          : lockMode === \"nonblocking\"\n            ? query\n                .for(\"update\", { noWait: true })\n                .then((rows) => rows[0] ?? null)\n            : query.for(\"update\").then((rows) => rows[0] ?? null);\n      })()\n    : null;\n  if (!destinationAllowed(endpoint, conversation, resource)) {\n    throw new Error(\"paperclip_runner_chat_attachment_destination_denied\");\n  }\n  for (const principalId of new Set(links.map((row) => row.principalId!))) {\n    if (!(await principalAuthorized(tx, endpoint, principalId, lockMode))) {\n      throw new Error(\"paperclip_runner_chat_attachment_principal_denied\");\n    }\n  }\n  return { conversationId: conversation.id, endpointId: endpoint.id };\n}\n\nfunction externalChatWaitCandidate(\n  contextSnapshot: unknown,\n  binding: ChatReuseBinding,\n): { provider: string; commentIds: string[] } | null {\n  const context = record(contextSnapshot);\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\",","sourceCodeStart":522,"sourceCodeEnd":558,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L522-L558","documentation":"This error is thrown by authorizeChatConversationForBoundRun in chat-attachment-reuse.ts:540 after it has already resolved the inbound chat conversation, endpoint, and destination. For every distinct chat principal (the external chat participant, e.g. a Slack/GitHub/Discord user) linked to the source comments, the service calls principalAuthorized; if any principal fails that check, the runner's chat attachment reuse tool call is rejected. It exists to guarantee the run only exchanges attachments with chat participants the endpoint still authorizes, so a revoked or removed participant's messages can never be used as an attachment source.","triggerScenarios":"A runner tool call (chat attachment reuse / list / wait authorization path: attempt, current, conversation, resolveExternalChatResponseWaitAuthorizationInTransaction, etc.) resolves a conversation whose inbound chat_message_links carry a principalId that principalAuthorized rejects — the principal was revoked on the endpoint, the principal no longer belongs to the destination resource (channel/room/repo), or the endpoint's authorization state changed between delivery and the tool call.","commonSituations":"A Slack/Discord/Telegram participant was removed from the channel or left the workspace after sending the message whose attachment the agent now wants to reuse; an admin rotated or revoked the endpoint's principal allowlist; an endpoint setup/verification changed so previously delivered messages now reference unauthorized principals; the binding's lock mode ('nonblocking') surfaces a transient lock conflict as an authorization failure in principalAuthorized.","solutions":["Check the chat endpoint's principal/authorization state for the participant linked to the source comments and re-authorize the principal on the endpoint before retrying the tool call.","Verify the conversation is still the run's bound wake conversation (single conversation/endpoint, inbound links for all wake comment ids) — a stale or partially delivered binding fails principal checks; re-wake the issue to rebuild the binding.","If the participant was removed intentionally, have the operator re-send the attachment from an authorized participant or attach the file directly to the issue instead of reusing the chat attachment.","Treat this like the sibling _destination_denied/_binding_denied codes: callers map it to state 'revoked' (see lines 760-765); clear the revoked wait state and re-deliver the message so a fresh principal authorization is recorded."],"exampleFix":"// before: reusing attachment from a chat message whose author was removed\nconst result = await reuseChatAttachment({ binding, sourceCommentId, attachmentId });\n// throws paperclip_runner_chat_attachment_principal_denied\n\n// after: re-authorized participant re-sends the file, then reuse succeeds\n// operator re-invites/authorizes the participant on the endpoint, message is re-delivered\nconst result = await reuseChatAttachment({ binding, sourceCommentId: newCommentId, attachmentId: newAttachmentId });","handlingStrategy":"try-catch","validationCode":"// before the tool call, confirm the wake participants are still authorized on the endpoint\nconst auth = await resolveExternalChatResponseWaitAuthorization({ db, binding });\n// if this pre-check throws paperclip_runner_chat_attachment_principal_denied, skip the reuse call","typeGuard":"function isPrincipalDenied(err: unknown): err is Error {\n  return err instanceof Error && err.message === \"paperclip_runner_chat_attachment_principal_denied\";\n}","tryCatchPattern":"try {\n  await reuseChatAttachment({ binding, sourceCommentId, attachmentId });\n} catch (err) {\n  if (err instanceof Error && err.message === \"paperclip_runner_chat_attachment_principal_denied\") {\n    // treat the wait state as 'revoked'; stop the run's external-chat wait and notify the operator\n    return { status: \"revoked\" as const };\n  }\n  throw err;\n}","preventionTips":["Re-validate participant authorization immediately before each reuse call instead of caching endpoint grants across turns.","Watch for participant removal events (member_left, revoked) on the endpoint and proactively expire outstanding wait states.","Prefer the preflight resolveExternalChatResponseWaitAuthorization over a bare reuse attempt so denials surface before work is done.","Log the principalId and endpointId on denial to speed up operator re-authorization."],"tags":["authorization","chat-endpoint","principal","security","native-runtime"],"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"}