{"record":{"id":"7cbd18d3c34053ec","repo":"paperclipai/paperclip","slug":"chat-publication-source-must-be-explicitly-classified-for","errorCode":null,"errorMessage":"Chat publication source must be explicitly classified for external delivery","messagePattern":"Chat publication source must be explicitly classified for external delivery","errorType":"validation","errorClass":"UnsafeChatPublicationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-publication-projection.ts","lineNumber":386,"sourceCode":"      title,\n      ...(body ? { body } : {}),\n      ...(actions.length ? { actions } : {}),\n    },\n  };\n}\n\n/**\n * Builds the complete provider-bound payload. This is intentionally the only\n * API that accepts attachments or rich interaction metadata.\n */\nexport function projectSafeChatPublication(\n  input: ChatPublicationProjectionInput,\n): ProjectedSafeChatPublicationPayload {\n  if (\n    input.classification !== \"external\" ||\n    !PUBLICATION_SOURCES.has(input.source)\n  ) {\n    throw new UnsafeChatPublicationError(\n      \"Chat publication source must be explicitly classified for external delivery\",\n    );\n  }\n  if (input.progressState && !PROGRESS_STATES.has(input.progressState)) {\n    throw new UnsafeChatPublicationError(\n      \"External chat progress state is invalid\",\n    );\n  }\n  const attachmentIds = projectAttachmentIds(input.attachmentIds);\n  const interaction = input.interaction ? projectCard(input.interaction) : null;\n\n  return {\n    text: projectSafeChatPublicationText(input.text),\n    ...(attachmentIds ? { attachmentIds } : {}),\n    ...(input.progressState ? { progressState: input.progressState } : {}),\n    ...(interaction ?? {}),\n  };\n}","sourceCodeStart":368,"sourceCodeEnd":404,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/chat-publication-projection.ts#L368-L404","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set classification: \"external\" explicitly on every projection input intended for external delivery","Use one of the five valid sources: \"agent_comment\", \"explicit_board_send\", \"safe_milestone\", \"issue_interaction\", \"task_control\"","If you added a new legitimate source type, register it in PUBLICATION_SOURCES in server/src/services/chat-publication-projection.ts","Check retry/enqueue code paths (e.g. enqueueFailedChatRetryPublications) to ensure they preserve the original classification and source fields"],"exampleFix":"// before\nprojectSafeChatPublication({ text, source: internalSource });\n// after\nprojectSafeChatPublication({ text, classification: \"external\", source: \"agent_comment\" });","handlingStrategy":"try-catch","validationCode":"const PUBLICATION_SOURCES = new Set([\"agent_comment\",\"explicit_board_send\",\"safe_milestone\",\"issue_interaction\",\"task_control\"]);\nif (input.classification !== \"external\" || !PUBLICATION_SOURCES.has(input.source)) {\n  throw new Error(`Publication not classified for external delivery: ${input.classification}/${input.source}`);\n}","typeGuard":"type ExternalSource = \"agent_comment\" | \"explicit_board_send\" | \"safe_milestone\" | \"issue_interaction\" | \"task_control\";\nfunction isExternalProjectionInput(i: { classification?: string; source: string }): i is { classification: \"external\"; source: ExternalSource } {\n  return i.classification === \"external\" && PUBLICATION_SOURCES.has(i.source as ExternalSource);\n}","tryCatchPattern":"try {\n  const payload = projectSafeChatPublication(input);\n} catch (err) {\n  if (err instanceof UnsafeChatPublicationError && err.message.includes(\"explicitly classified for external delivery\")) {\n    logger.warn(\"dropping publication missing external classification\", { source: input.source });\n    return null; // never guess a classification for external delivery\n  }\n  throw err;\n}","preventionTips":["Make classification a required field in the type constructing projection inputs so it cannot be omitted","Centralize publication creation in one helper that always stamps a valid classification/source","When adding a new source kind, update PUBLICATION_SOURCES in the same PR and add a test","Audit retry paths to confirm they copy classification and source, not rebuild the input from partial data"],"tags":["validation","chat-publication","classification","config"],"backgroundTag":"missing-required-config-field","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}