{"record":{"id":"b309bdcf49ee5223","repo":"nexu-io/open-design","slug":"pluginworkflowid-requires-a-validated-externalplug","errorCode":null,"errorMessage":"pluginWorkflowId requires a validated externalPluginContext","messagePattern":"pluginWorkflowId requires a validated externalPluginContext","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/mcp-brief.ts","lineNumber":376,"sourceCode":"      const localeSource =\n        typeof input.locale === 'string' && input.locale.trim().length > 0\n          ? 'provided'\n          : 'fallback';\n      const locale = readBriefLocale(input.locale);\n      const knownAnswers = readAnswerRecord(input.knownAnswers, 'knownAnswers');\n      const skip = input.skip === true;\n      const hasPluginContext = Object.prototype.hasOwnProperty.call(\n        input,\n        'externalPluginContext',\n      );\n      const externalPluginContext = hasPluginContext\n        ? validateExternalPluginContext(input.externalPluginContext)\n        : undefined;\n      const pluginWorkflowId = hasPluginContext\n        ? validatePluginWorkflowId(input.pluginWorkflowId)\n        : undefined;\n      if (!hasPluginContext && input.pluginWorkflowId !== undefined) {\n        throw new Error(\n          'pluginWorkflowId requires a validated externalPluginContext',\n        );\n      }\n      const decision = collectOpenDesignBrief({\n        artifactType,\n        knownAnswers,\n        skip,\n      });\n      const briefDraftId = randomUUID();\n      const nonce = randomBytes(24).toString('hex');\n      const expiresAt = at + ttlMs;\n      drafts.set(briefDraftId, {\n        artifactType,\n        projectTitle,\n        locale,\n        localeSource,\n        knownAnswers: { ...decision.answers },\n        nonce,","sourceCodeStart":358,"sourceCodeEnd":394,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/mcp-brief.ts#L358-L394","documentation":"Thrown by the brief collector's collect() when the caller passes `pluginWorkflowId` without also passing `externalPluginContext`. The collector treats plugin correlation as a paired contract: a workflow id is only meaningful when attributed to a validated external plugin, so providing one without the other is rejected. The check uses hasOwnProperty('externalPluginContext') so explicitly passing undefined as the context still counts as 'provided' and goes through validateExternalPluginContext instead.","triggerScenarios":"Calling the brief collect API with `{ pluginWorkflowId: '<uuid>' }` but no `externalPluginContext` field. Or copying fields from a partial request that included the workflow id but dropped the plugin context object.","commonSituations":"External plugin omits the context object but sends its workflow id for analytics; a shim/adapter forwards pluginWorkflowId unconditionally; refactoring removes externalPluginContext but leaves pluginWorkflowId; client assumes pluginWorkflowId is independently optional.","solutions":["Pass both fields together: include a fully-validated externalPluginContext whenever you pass pluginWorkflowId.","If you have no plugin context, omit pluginWorkflowId entirely (do not pass it as undefined alongside a missing context).","Build the externalPluginContext via validateExternalPluginContext() before sending so it cannot be dropped by a validation gap.","Audit request payloads at the boundary to enforce the pairing (the two fields are either both present or both absent)."],"exampleFix":"// before\ncollect({ artifactType: 'deck', pluginWorkflowId: '01HZX...' });\n\n// after — pair the workflow id with its plugin context\ncollect({\n  artifactType: 'deck',\n  externalPluginContext: {\n    id: OPEN_DESIGN_PLUGIN_ID,\n    version: '1.0.0',\n    distributionMechanism: 'official',\n    publisherClass: 'first-party',\n  },\n  pluginWorkflowId: '01HZX...',\n});","handlingStrategy":"type-guard","validationCode":"function hasPluginContextPair(input: unknown): boolean {\n  if (!input || typeof input !== 'object') return true;\n  const hasContext = Object.prototype.hasOwnProperty.call(input, 'externalPluginContext');\n  const hasWorkflow = Object.prototype.hasOwnProperty.call(input, 'pluginWorkflowId');\n  // Either both present or both absent.\n  return hasContext === hasWorkflow;\n}\n\nif (!hasPluginContextPair(input)) {\n  throw new Error('pluginWorkflowId and externalPluginContext must be supplied together.');\n}","typeGuard":"import { validateExternalPluginContext, validatePluginWorkflowId } from './mcp-observability';\n\ntype PluginCorrelation = { externalPluginContext: ExternalPluginContext; pluginWorkflowId: string };\n\nfunction resolvePluginCorrelation(input: Record<string, unknown>): PluginCorrelation | undefined {\n  const hasContext = Object.prototype.hasOwnProperty.call(input, 'externalPluginContext');\n  const hasWorkflow = Object.prototype.hasOwnProperty.call(input, 'pluginWorkflowId');\n  if (!hasContext && !hasWorkflow) return undefined;\n  if (hasContext !== hasWorkflow) {\n    throw new Error('pluginWorkflowId requires externalPluginContext (and vice versa).');\n  }\n  return {\n    externalPluginContext: validateExternalPluginContext(input.externalPluginContext),\n    pluginWorkflowId: validatePluginWorkflowId(input.pluginWorkflowId),\n  };\n}","tryCatchPattern":null,"preventionTips":["Always pass pluginWorkflowId and externalPluginContext as a pair, or omit both.","Build externalPluginContext via validateExternalPluginContext at the boundary.","Audit request payloads to enforce the pairing before reaching the collector."],"tags":["mcp","brief","plugin","validation","contract"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}