{"record":{"id":"b7f309e2d513ecf7","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-destination-denied","errorCode":"paperclip_runner_chat_attachment_destination_denied","errorMessage":"paperclip_runner_chat_attachment_destination_denied","messagePattern":"paperclip_runner_chat_attachment_destination_denied","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":536,"sourceCode":"          .from(chatEndpointResources)\n          .where(\n            and(\n              eq(chatEndpointResources.id, conversation.resourceId!),\n              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\",","sourceCodeStart":518,"sourceCodeEnd":554,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L518-L554","documentation":"Thrown by authorizeChatConversationForBoundRun when destinationAllowed(endpoint, conversation, resource) returns false. After resolving the optional chatEndpointResources row for the conversation, this final policy check rejects delivery of bound output to the requested destination — e.g. the conversation type, resource scope, or endpoint destination policy does not allow replies for this binding. Distinct from the _binding_denied family: binding checks passed but the destination itself is disallowed.","triggerScenarios":"Calling when destinationAllowed fails, typically because: the conversation's resource (channel/board) is not permitted as a reply destination for this endpoint; the conversation's destination configuration (e.g. DM vs channel policy, resourceId scope) is disallowed by endpoint settings; the resource row exists but belongs to a scope the endpoint cannot post to.","commonSituations":"Endpoint configured to reply only in threads but the resolved conversation targets a channel; conversation resource was deleted/re-pointed after the run started; admin tightened destination policy while a run was waiting; mismatched chatEndpointResources row (different endpointId) so the resource lookup returned null and policy treats it as disallowed.","solutions":["Review the endpoint's destination policy settings and enable the conversation type/resource as an allowed reply destination.","Verify chatEndpointResources row for conversation.resourceId exists with matching companyId and endpointId.","Re-point or recreate the conversation if its resource was deleted or reassigned to another endpoint.","Fall back to a permitted destination (e.g. the originating thread) instead of the disallowed one."],"exampleFix":"// before: assumes any conversation on the endpoint is a valid destination\nawait authorizeChatConversationForBoundRun(tx, binding, ctx);\n// after: check destination policy first\nconst allowed = await isDestinationAllowed(endpoint, conversation);\nif (!allowed) {\n  console.warn(\"conversation not an allowed reply destination; falling back to origin thread\");\n  await deliverToOriginThread(binding);\n  return;\n}\nawait authorizeChatConversationForBoundRun(tx, binding, ctx);","handlingStrategy":"try-catch","validationCode":"// Verify the conversation resource is valid for this endpoint before authorizing\nif (conversation.resourceId) {\n  const [resource] = await db.select()\n    .from(chatEndpointResources)\n    .where(and(\n      eq(chatEndpointResources.id, conversation.resourceId),\n      eq(chatEndpointResources.companyId, binding.companyId),\n      eq(chatEndpointResources.endpointId, endpointId),\n    ));\n  if (!resource) throw new Error(\"conversation resource missing or belongs to another endpoint\");\n}","typeGuard":"function resourceBelongsToEndpoint(resource: { endpointId: string } | null, endpointId: string): resource is { endpointId: string } {\n  return resource !== null && resource.endpointId === endpointId;\n}","tryCatchPattern":"try {\n  return await authorizeChatConversationForBoundRun(tx, binding, ctx);\n} catch (err) {\n  if ((err as Error).message === \"paperclip_runner_chat_attachment_destination_denied\") {\n    // deliver to the permitted fallback destination instead of the conversation\n    await deliverToFallbackDestination(binding);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Review endpoint destination policy when creating conversations; only start runs against allowed destinations.","Keep chatEndpointResources rows consistent (companyId/endpointId) when re-pointing conversations.","Treat destination_denied separately from binding_denied in logs so policy changes are visible to admins."],"tags":["authorization","chat","destination-policy","attachment-reuse"],"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"}