paperclipai/paperclip · error
paperclip_runner_chat_attachment_destination_denied
paperclip_runner_chat_attachment_destination_denied
Error message
paperclip_runner_chat_attachment_destination_denied
What it means
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.
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.
Example fix
// before: assumes any conversation on the endpoint is a valid destination
await authorizeChatConversationForBoundRun(tx, binding, ctx);
// after: check destination policy first
const allowed = await isDestinationAllowed(endpoint, conversation);
if (!allowed) {
console.warn("conversation not an allowed reply destination; falling back to origin thread");
await deliverToOriginThread(binding);
return;
}
await authorizeChatConversationForBoundRun(tx, binding, ctx); Defensive patterns
Strategy: try-catch
Validate before calling
// Verify the conversation resource is valid for this endpoint before authorizing
if (conversation.resourceId) {
const [resource] = await db.select()
.from(chatEndpointResources)
.where(and(
eq(chatEndpointResources.id, conversation.resourceId),
eq(chatEndpointResources.companyId, binding.companyId),
eq(chatEndpointResources.endpointId, endpointId),
));
if (!resource) throw new Error("conversation resource missing or belongs to another endpoint");
} Type guard
function resourceBelongsToEndpoint(resource: { endpointId: string } | null, endpointId: string): resource is { endpointId: string } {
return resource !== null && resource.endpointId === endpointId;
} Try / catch
try {
return await authorizeChatConversationForBoundRun(tx, binding, ctx);
} catch (err) {
if ((err as Error).message === "paperclip_runner_chat_attachment_destination_denied") {
// deliver to the permitted fallback destination instead of the conversation
await deliverToFallbackDestination(binding);
return null;
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- paperclip_runner_chat_attachment_binding_denied
- access.reasonCode
- agent_authorization_required
- agent_not_assigned
- ambiguous_personal_grant
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/b7f309e2d513ecf7.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:536
.from(chatEndpointResources)
.where(
and(
eq(chatEndpointResources.id, conversation.resourceId!),
eq(chatEndpointResources.companyId, binding.companyId),
eq(chatEndpointResources.endpointId, endpoint.id),
),
);
return lockMode === "read"
? query.then((rows) => rows[0] ?? null)
: lockMode === "nonblocking"
? query
.for("update", { noWait: true })
.then((rows) => rows[0] ?? null)
: query.for("update").then((rows) => rows[0] ?? null);
})()
: null;
if (!destinationAllowed(endpoint, conversation, resource)) {
throw new Error("paperclip_runner_chat_attachment_destination_denied");
}
for (const principalId of new Set(links.map((row) => row.principalId!))) {
if (!(await principalAuthorized(tx, endpoint, principalId, lockMode))) {
throw new Error("paperclip_runner_chat_attachment_principal_denied");
}
}
return { conversationId: conversation.id, endpointId: endpoint.id };
}
function externalChatWaitCandidate(
contextSnapshot: unknown,
binding: ChatReuseBinding,
): { provider: string; commentIds: string[] } | null {
const context = record(contextSnapshot);
const source = typeof context.source === "string" ? context.source : "";
const provider = [
"slack",
"github",View on GitHub (pinned to 3f1d897a7c)