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

  1. Verify the interaction comment being responded to still has a pending, unanswered external chat question matching this binding (same issueId, agentId, companyId).
  2. Re-capture a fresh contextSnapshot at the time of authorization instead of replaying a stale one.
  3. Confirm the run is actually bound to the chat conversation (paperclipHarnessCheckedOut / paperclipExternalChatExecutionBound flags set on a sibling code path).
  4. 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

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


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)