paperclipai/paperclip · error

paperclip_runner_chat_attachment_principal_denied

paperclip_runner_chat_attachment_principal_denied

Error message

paperclip_runner_chat_attachment_principal_denied

What it means

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.

Solutions

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Example fix

// before: reusing attachment from a chat message whose author was removed
const result = await reuseChatAttachment({ binding, sourceCommentId, attachmentId });
// throws paperclip_runner_chat_attachment_principal_denied

// after: re-authorized participant re-sends the file, then reuse succeeds
// operator re-invites/authorizes the participant on the endpoint, message is re-delivered
const result = await reuseChatAttachment({ binding, sourceCommentId: newCommentId, attachmentId: newAttachmentId });
Defensive patterns

Strategy: try-catch

Validate before calling

// before the tool call, confirm the wake participants are still authorized on the endpoint
const auth = await resolveExternalChatResponseWaitAuthorization({ db, binding });
// if this pre-check throws paperclip_runner_chat_attachment_principal_denied, skip the reuse call

Type guard

function isPrincipalDenied(err: unknown): err is Error {
  return err instanceof Error && err.message === "paperclip_runner_chat_attachment_principal_denied";
}

Try / catch

try {
  await reuseChatAttachment({ binding, sourceCommentId, attachmentId });
} catch (err) {
  if (err instanceof Error && err.message === "paperclip_runner_chat_attachment_principal_denied") {
    // treat the wait state as 'revoked'; stop the run's external-chat wait and notify the operator
    return { status: "revoked" as const };
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/5979fc19c1a7e7ca. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:540

              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",
    "discord",
    "microsoft-teams",
    "telegram",
    "imessage-photon",

View on GitHub (pinned to 3f1d897a7c)