nexu-io/open-design · error · Error

pluginWorkflowId requires a validated externalPluginContext

Error message

pluginWorkflowId requires a validated externalPluginContext

What it means

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.

Source

Thrown at apps/daemon/src/mcp-brief.ts:376

      const localeSource =
        typeof input.locale === 'string' && input.locale.trim().length > 0
          ? 'provided'
          : 'fallback';
      const locale = readBriefLocale(input.locale);
      const knownAnswers = readAnswerRecord(input.knownAnswers, 'knownAnswers');
      const skip = input.skip === true;
      const hasPluginContext = Object.prototype.hasOwnProperty.call(
        input,
        'externalPluginContext',
      );
      const externalPluginContext = hasPluginContext
        ? validateExternalPluginContext(input.externalPluginContext)
        : undefined;
      const pluginWorkflowId = hasPluginContext
        ? validatePluginWorkflowId(input.pluginWorkflowId)
        : undefined;
      if (!hasPluginContext && input.pluginWorkflowId !== undefined) {
        throw new Error(
          'pluginWorkflowId requires a validated externalPluginContext',
        );
      }
      const decision = collectOpenDesignBrief({
        artifactType,
        knownAnswers,
        skip,
      });
      const briefDraftId = randomUUID();
      const nonce = randomBytes(24).toString('hex');
      const expiresAt = at + ttlMs;
      drafts.set(briefDraftId, {
        artifactType,
        projectTitle,
        locale,
        localeSource,
        knownAnswers: { ...decision.answers },
        nonce,

View on GitHub (pinned to 5be4028344)

Solutions

  1. Pass both fields together: include a fully-validated externalPluginContext whenever you pass pluginWorkflowId.
  2. If you have no plugin context, omit pluginWorkflowId entirely (do not pass it as undefined alongside a missing context).
  3. Build the externalPluginContext via validateExternalPluginContext() before sending so it cannot be dropped by a validation gap.
  4. Audit request payloads at the boundary to enforce the pairing (the two fields are either both present or both absent).

Example fix

// before
collect({ artifactType: 'deck', pluginWorkflowId: '01HZX...' });

// after — pair the workflow id with its plugin context
collect({
  artifactType: 'deck',
  externalPluginContext: {
    id: OPEN_DESIGN_PLUGIN_ID,
    version: '1.0.0',
    distributionMechanism: 'official',
    publisherClass: 'first-party',
  },
  pluginWorkflowId: '01HZX...',
});
Defensive patterns

Strategy: type-guard

Validate before calling

function hasPluginContextPair(input: unknown): boolean {
  if (!input || typeof input !== 'object') return true;
  const hasContext = Object.prototype.hasOwnProperty.call(input, 'externalPluginContext');
  const hasWorkflow = Object.prototype.hasOwnProperty.call(input, 'pluginWorkflowId');
  // Either both present or both absent.
  return hasContext === hasWorkflow;
}

if (!hasPluginContextPair(input)) {
  throw new Error('pluginWorkflowId and externalPluginContext must be supplied together.');
}

Type guard

import { validateExternalPluginContext, validatePluginWorkflowId } from './mcp-observability';

type PluginCorrelation = { externalPluginContext: ExternalPluginContext; pluginWorkflowId: string };

function resolvePluginCorrelation(input: Record<string, unknown>): PluginCorrelation | undefined {
  const hasContext = Object.prototype.hasOwnProperty.call(input, 'externalPluginContext');
  const hasWorkflow = Object.prototype.hasOwnProperty.call(input, 'pluginWorkflowId');
  if (!hasContext && !hasWorkflow) return undefined;
  if (hasContext !== hasWorkflow) {
    throw new Error('pluginWorkflowId requires externalPluginContext (and vice versa).');
  }
  return {
    externalPluginContext: validateExternalPluginContext(input.externalPluginContext),
    pluginWorkflowId: validatePluginWorkflowId(input.pluginWorkflowId),
  };
}

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/b309bdcf49ee5223. Report an issue: GitHub.