paperclipai/paperclip · error
paperclip_runner_chat_attachment_binding_denied
paperclip_runner_chat_attachment_binding_denied
Error message
paperclip_runner_chat_attachment_binding_denied
What it means
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.
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.
Example fix
// before: stale snapshot reused during recovery
await authorizeChatConversationForBoundRun(tx, binding, staleContext);
// after: guard before calling
if (staleContext.source === "issue.interaction.respond") {
const pending = await findPendingExternalChatQuestion(tx, binding, staleContext);
if (!pending) throw new SkipAttachmentReuse(); // don't force authorization
}
await authorizeChatConversationForBoundRun(tx, binding, await refreshContext(staleContext)); Defensive patterns
Strategy: try-catch
Validate before calling
// Before authorizing interaction-response reuse:
if (context.source === "issue.interaction.respond") {
const pending = await db
.select({ id: issueInteractionComments.id })
.from(issueInteractionComments)
.where(and(
eq(issueInteractionComments.issueId, binding.issueId),
isNull(issueInteractionComments.answeredAt),
));
if (pending.length === 0) throw new Error("no pending external chat question for binding");
} Type guard
function hasPendingExternalChatQuestion(ctx: Record<string, unknown>): boolean {
return ctx.source === "issue.interaction.respond" &&
Array.isArray(ctx.wakeCommentIds) && ctx.wakeCommentIds.length > 0;
} Try / catch
try {
const conv = await authorizeChatConversationForBoundRun(tx, binding, ctx);
} catch (err) {
if ((err as Error).message === "paperclip_runner_chat_attachment_binding_denied") {
// skip attachment reuse; deliver without reused attachments
} else throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
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_destination_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/630f5a1aedb6e492.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:417
};
if (link?.status === "linked") {
return Boolean(link.userId && (await activeMember(link.userId)));
}
if (!endpoint.allowUnlinkedPeople) return false;
return endpoint.sponsorUserId ? activeMember(endpoint.sponsorUserId) : true;
}
/** Caller must independently verify the current run/issue execution owner. */
export async function authorizeChatConversationForBoundRun(
tx: Db,
binding: ChatReuseBinding,
contextSnapshot: unknown,
lockMode: AuthorizationLockMode = "blocking",
): Promise<AuthorizedConversation> {
let context = record(contextSnapshot);
if (context.source === "issue.interaction.respond") {
const answer = await resolveExternalChatQuestionResponse(tx, binding, context, lockMode);
if (!answer) throw new Error("paperclip_runner_chat_attachment_binding_denied");
context = answer.authorizationContext;
}
const source = typeof context.source === "string" ? context.source : "";
const provider = [
"slack",
"github",
"discord",
"microsoft-teams",
"telegram",
"imessage-photon",
].find(
(candidate) =>
source === `chat:${candidate}` || source === `chat:${candidate}:recovery`,
);
const commentIds = wakeCommentIds(context);
if (
!provider ||
(context.paperclipHarnessCheckedOut !== true &&View on GitHub (pinned to 3f1d897a7c)