Skip to content

PackageRequest normalizer accepts pre-3.0 shape (product_ids[] / object budget) — should fail closed #1677

@bokelley

Description

@bokelley

Summary

The compliance harness (and other internal callers) appear to construct PackageRequest objects using pre-3.0 shape:

  • packages[].product_ids: string[] — plural array
  • packages[].budget: { total, currency } — object

Per the AdCP 3.0 spec (static/schemas/source/media-buy/package-request.json in adcontextprotocol/adcp), the required 3.0 shape is:

  • packages[].product_id: string (singular)
  • packages[].budget: number
  • packages[].pricing_option_id: string

request-normalizer.ts::normalizePackageParams (compiled at dist/lib/utils/request-normalizer.js:23-46) currently translates only:

  • optimization_goal → optimization_goals (scalar → array, safe rename)
  • catalog → catalogs (scalar → array, safe rename)
  • context.buyer_ref → buyer_ref (4.15+ → pre-4.15 compat)

It does NOT translate the pre-3.0 product_ids[] / object-budget shapes, so 3.0-strict sellers reject the request.

Why fail closed rather than translate

product_ids[]product_id is data loss: which id wins? budget: { total, currency }budget: number is data loss: which currency does the seller bill in? These aren't renames; the safe answer is to refuse the request and tell the caller to supply 3.0 shape.

Per v2 sunset policy: v2 unsupported as of 3.0 GA (April 2026), security-only until Aug 1 2026. No remaining obligation to translate pre-3.0 shapes.

Repro

Comply run against Wonderstruck. Wonderstruck's MCP schema enforces 3.0 PackageRequest. Outcome:

INVALID_REQUEST: create_media_buy failed:
  packages.0.product_id: Field required
  packages.0.budget: Input should be a valid number
  packages.0.pricing_option_id: Field required

Affected comply scenarios (10): idempotency/missing_key, pagination_integrity_list_accounts, schema_validation/temporal_validation, v3_envelope_integrity, proposal_finalize/brief_with_proposals, media_buy_state_machine/state_transitions + 2 cascades, media_buy_state_machine/terminal_enforcement + 3 cascades, error_compliance/error_responses (× 2 steps), error_compliance/version_negotiation.

Fix

In normalizePackageParams:

if (Array.isArray(normalized.product_ids)) {
  throw new ADCPValidationError(
    'packages[].product_ids is a pre-3.0 shape. Use product_id (singular) per AdCP 3.0.'
  );
}
if (normalized.budget && typeof normalized.budget === 'object') {
  throw new ADCPValidationError(
    'packages[].budget as object is a pre-3.0 shape. Use budget as a number per AdCP 3.0.'
  );
}

Then update the comply harness to emit 3.0 shape directly.

Cross-link

Same Wonderstruck probe as #1676 (account fabrication). Full findings in adcontextprotocol/adcp's .context/wonderstruck-findings.md.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions