paperclipai/paperclip · error · UnsafeChatPublicationError

Chat publication source must be explicitly classified for…

Error message

Chat publication source must be explicitly classified for external delivery

What it means

projectSafeChatPublication is the only API allowed to build provider-bound external chat payloads, and it requires the caller to explicitly opt in: input.classification must equal "external" AND input.source must be one of PUBLICATION_SOURCES ("agent_comment", "explicit_board_send", "safe_milestone", "issue_interaction", "task_control"). If either check fails, it throws, preventing internal-only messages (agent chatter, logs) from leaking to external chat providers via a missing or default classification.

Solutions

  1. Set classification: "external" explicitly on every projection input intended for external delivery
  2. Use one of the five valid sources: "agent_comment", "explicit_board_send", "safe_milestone", "issue_interaction", "task_control"
  3. If you added a new legitimate source type, register it in PUBLICATION_SOURCES in server/src/services/chat-publication-projection.ts
  4. Check retry/enqueue code paths (e.g. enqueueFailedChatRetryPublications) to ensure they preserve the original classification and source fields

Example fix

// before
projectSafeChatPublication({ text, source: internalSource });
// after
projectSafeChatPublication({ text, classification: "external", source: "agent_comment" });
Defensive patterns

Strategy: try-catch

Validate before calling

const PUBLICATION_SOURCES = new Set(["agent_comment","explicit_board_send","safe_milestone","issue_interaction","task_control"]);
if (input.classification !== "external" || !PUBLICATION_SOURCES.has(input.source)) {
  throw new Error(`Publication not classified for external delivery: ${input.classification}/${input.source}`);
}

Type guard

type ExternalSource = "agent_comment" | "explicit_board_send" | "safe_milestone" | "issue_interaction" | "task_control";
function isExternalProjectionInput(i: { classification?: string; source: string }): i is { classification: "external"; source: ExternalSource } {
  return i.classification === "external" && PUBLICATION_SOURCES.has(i.source as ExternalSource);
}

Try / catch

try {
  const payload = projectSafeChatPublication(input);
} catch (err) {
  if (err instanceof UnsafeChatPublicationError && err.message.includes("explicitly classified for external delivery")) {
    logger.warn("dropping publication missing external classification", { source: input.source });
    return null; // never guess a classification for external delivery
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling projectSafeChatPublication with classification left undefined/default (anything other than "external"), or with a source string outside the allowed set — e.g. "internal_comment", "debug", "agent_log", a typo like "agent_Comment", or a newly added source not registered in PUBLICATION_SOURCES.

Common situations: A new publication source was added elsewhere in the codebase but PUBLICATION_SOURCES in chat-publication-projection.ts was not updated; a caller forgot to set classification after a refactor; a retry/enqueue path reconstructs the input and drops the classification field.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/7cbd18d3c34053ec. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/chat-publication-projection.ts:386

      title,
      ...(body ? { body } : {}),
      ...(actions.length ? { actions } : {}),
    },
  };
}

/**
 * Builds the complete provider-bound payload. This is intentionally the only
 * API that accepts attachments or rich interaction metadata.
 */
export function projectSafeChatPublication(
  input: ChatPublicationProjectionInput,
): ProjectedSafeChatPublicationPayload {
  if (
    input.classification !== "external" ||
    !PUBLICATION_SOURCES.has(input.source)
  ) {
    throw new UnsafeChatPublicationError(
      "Chat publication source must be explicitly classified for external delivery",
    );
  }
  if (input.progressState && !PROGRESS_STATES.has(input.progressState)) {
    throw new UnsafeChatPublicationError(
      "External chat progress state is invalid",
    );
  }
  const attachmentIds = projectAttachmentIds(input.attachmentIds);
  const interaction = input.interaction ? projectCard(input.interaction) : null;

  return {
    text: projectSafeChatPublicationText(input.text),
    ...(attachmentIds ? { attachmentIds } : {}),
    ...(input.progressState ? { progressState: input.progressState } : {}),
    ...(interaction ?? {}),
  };
}

View on GitHub (pinned to 3f1d897a7c)