Skip to content

Releases: adcontextprotocol/adcp-client

v5.13.0

22 Apr 12:57
9c7c221

Choose a tag to compare

Minor Changes

  • be0d60b: Envelope hygiene: colocate the two error-envelope allowlists into a
    single source of truth, and flip wrapEnvelope to a fail-closed default
    for unregistered error codes.

    Security-review follow-ups from #788 (M3 + M4):

    • #800 (M4): ERROR_ENVELOPE_FIELD_ALLOWLIST (sibling-keys allowlist
      used by wrapEnvelope) and the former CONFLICT_ALLOWED_ENVELOPE_KEYS
      (inside-adcp_error allowlist used by the
      idempotency.conflict_no_payload_leak invariant) now live side-by-side
      in the new src/lib/server/envelope-allowlist.ts module. The latter
      is renamed to CONFLICT_ADCP_ERROR_ALLOWLIST to make the "keys inside
      the adcp_error block" scope obvious. Both are exported from
      @adcp/client/server so callers with custom error envelopes can
      inspect / extend the sets.
    • #799 (M3): wrapEnvelope now fails closed on unregistered error
      codes. A code with no explicit entry in ERROR_ENVELOPE_FIELD_ALLOWLIST
      uses DEFAULT_ERROR_ENVELOPE_FIELDScontext only — instead of
      inheriting success-envelope semantics. Sellers that want replayed
      or operation_id on a bespoke error code must register it explicitly.
      The fail-closed posture matches the framework's own internal behavior:
      create-adcp-server.ts error paths only ever echo context via
      finalize(); injectReplayed is never called on error responses.

    Who is affected: consumers calling wrapEnvelope with an
    adcp_error.code other than IDEMPOTENCY_CONFLICT (the only code
    registered today) AND relying on replayed or operation_id to
    round-trip. On upgrade, those fields silently drop — only context
    echoes. IDEMPOTENCY_CONFLICT is unchanged.

    Upgrade path: for bespoke error codes that genuinely need
    replayed or operation_id on the envelope, build the envelope
    directly instead of calling wrapEnvelope, or open an issue so the
    code can be added to ERROR_ENVELOPE_FIELD_ALLOWLIST. The allowlist
    is intentionally frozen at the module level — extending it requires a
    spec-and-SDK conversation, not a local override.

    Breaking change (minor — wrapEnvelope was just shipped in 5.11.0):
    narrow external surface, days-old on npm.

    Closes #799, closes #800.

  • e5ef1be: Pin to AdCP 3.0.0 GA.

    ADCP_VERSION flips from the rolling latest alias to the published
    3.0.0 release. Generated types, Zod schemas, compliance storyboards,
    and schemas-data/ are now locked to the 3.0.0 registry instead of
    tracking whatever the registry serves next. COMPATIBLE_ADCP_VERSIONS
    adds '3.0.0' alongside the existing v3 alias and the beta.1 /
    beta.3 wire-compat entries so mixed-version traffic keeps working.

    Supply-chain: the 3.0.0 tarball is cosign-verified against
    adcontextprotocol/adcp's release workflow OIDC identity, which is a
    stricter trust boundary than the checksum-only latest alias used
    before.

    Side effects of the pin:

    • validate_property_delivery response now uses its generated
      ValidatePropertyDeliveryResponseSchema (upstream shipped the
      registry entry in 3.0.0 GA). The schema requires list_id,
      summary, results, and validated_at; compliant is optional.
      The previous hand-written stub accepted a bare {compliant} OR a
      bare {errors} fallback; the {errors} branch is gone — error
      responses now flow through the protocol's async error channel
      rather than the response body. Callers reading compliant still
      work; callers that consumed .errors from the response must switch
      to the standard TaskResult.adcpError path.
    • compliance/cache/3.0.0/ is populated (cosign-verified) and
      replaces compliance/cache/latest/ as the storyboard source.

Patch Changes

  • 22b44c4: Fix governance.denial_blocks_mutation to allow expected-denial recovery
    paths.

    The invariant anchored any governance denial (GOVERNANCE_DENIED,
    TERMS_REJECTED, POLICY_VIOLATION, etc.) and then flagged any later
    successful mutation in the same run as a silent bypass. That fired on
    first-party storyboards whose whole purpose is to test recovery —
    media_buy_seller/governance_denied_recovery (buyer shrinks the buy
    and retries) and media_buy_seller/measurement_terms_rejected (buyer
    relaxes terms and retries) — because the retry step succeeded against
    the same plan and tripped the anchor.

    A denial step that the storyboard marks expect_error: true is the
    author explicitly acknowledging the denial. The subsequent mutation is
    a recovery path, not a silent bypass, so the invariant no longer
    anchors when the denial step is expected. The silent-bypass signal is
    preserved for check_governance 200s with status: denied and for
    adcp_error responses the author did not declare expected.

    When the invariant does fire on a wire-error denial, the failure
    message now points the author at the expect_error: true escape so
    the next author doesn't have to re-derive it from source. The hint is
    suppressed on check_governance 200 denials where the flag has no
    effect.

    Closes #811.

v5.12.0

22 Apr 08:34
71fdad9

Choose a tag to compare

Minor Changes

  • 054d37a: Expose wrapEnvelope from @adcp/client/server — a public helper for attaching AdCP envelope fields (replayed, context, operation_id) to handler responses, with error-code-specific field allowlists (e.g., IDEMPOTENCY_CONFLICT drops replayed). Promoted for sellers that wire their own MCP / A2A handlers without the framework.

    Parity with the framework's internal injectContextIntoResponse: opts.context is NOT attached when the inner payload already carries a context the handler placed itself (handler wins). The per-error-code allowlist now lists context explicitly rather than short-circuiting — a module-load invariant asserts every allowlist entry includes context so future error codes can't silently drop correlation echo. Return type widened to surface the envelope fields (replayed?, context?, operation_id?) for caller autocomplete.

  • 8d86be7: Add runAgainstLocalAgent to @adcp/client/testing — a one-call compliance harness that composes createAdcpServer + serve + seedComplianceFixtures + the webhook receiver + the storyboard runner. Sellers iterating on their handlers no longer need to hand-roll the 300-line bootstrap (ephemeral port, fixtures, webhook receiver, loop, teardown) from adcp's server/tests/manual/run-storyboards.ts.

    Programmatic surface. @adcp/client/testing now exports runAgainstLocalAgent({ createAgent, storyboards, fixtures?, webhookReceiver?, authorizationServer?, runStoryboardOptions?, onListening?, onStoryboardComplete?, bail? }). The caller's createAgent must close over a stable stateStore so seeds persist across the factory calls serve() makes per request. storyboards accepts 'all' (every storyboard in the cache), AgentCapabilities (the same resolution the live assessment runner does), string[] (storyboard or bundle ids), or Storyboard[].

    CLI surface. adcp storyboard run --local-agent <module> [id|bundle] is a thin wrapper over the programmatic helper. The module must export createAgent as default or named. --format junit emits a JUnit XML report on stdout for single-storyboard and --local-agent runs — each storyboard becomes a <testsuite>, each step a <testcase>.

    Test authorization server. @adcp/client/compliance-fixtures now exports createTestAuthorizationServer({ subjects?, issuer?, algorithm? }) — an in-process OAuth 2.0 AS that serves RFC 8414 metadata, JWKS, and a client-credentials token endpoint. Pairs with runAgainstLocalAgent({ authorizationServer: true }) to grade security_baseline, signed-requests, and other auth-requiring storyboards locally without reaching an external IdP. RS256 by default (ES256 available); HS* is refused to match verifyBearer's asymmetric-only allowlist.

    New guide. docs/guides/VALIDATE-LOCALLY.md walks the ten-line pattern, the stable-stateStore rule, the CLI equivalent, and the auth-server integration.

    Closes adcp-client#786.

  • 39e661f: Add seed fixture merge helpers and a get_products test-controller bridge so Group A compliance storyboards can seed fixtures end-to-end without seller boilerplate.

    Seed merge helpers (@adcp/client/testing):

    • Generic mergeSeed<T>(base, seed) — permissive merge: undefined/null in seed preserves base; every other leaf (including 0, false, "", []) overrides. Arrays replace by default; Map/Set throw.
    • Typed per-kind wrappers (mergeSeedProduct, mergeSeedPricingOption, mergeSeedCreative, mergeSeedPlan, mergeSeedMediaBuy) layer by-id overlay on well-known id-keyed arrays so seeding a single entry doesn't drop the rest: pricing_options[] by pricing_option_id, publisher_properties[] by (publisher_domain, selection_type), packages[] by package_id, creative assets[] by asset_id, plan findings[] by policy_id, plan checks[] by check_id.
    • Shared overlayById(base, seed, identity) helper so sellers can apply the same overlay rule to domain-specific fields.

    get_products bridge (@adcp/client):

    • createAdcpServer({ testController: { getSeededProducts } }) — seeded products append to handler output on sandbox requests (account.sandbox === true, context.sandbox === true, and — when resolveAccount returns an account — ctx.account.sandbox === true). Production traffic or a resolved non-sandbox account skips the bridge entirely. product_id collisions resolve with the seeded entry winning. Returns that are non-arrays or entries missing product_id are logged and dropped rather than thrown. Handler-declared sandbox: false stays authoritative (the bridge does not overwrite it).
    • bridgeFromTestControllerStore(store, productDefaults) — one-liner that wraps any Map<string, unknown> seed store into a TestControllerBridge; each stored fixture is merged onto productDefaults via mergeSeedProduct.
    • Opt-in via presence of getSeededProducts; the previous augmentGetProducts flag is dropped (one-rule opt-in).

Patch Changes

  • f86afe4: Storyboard runner: honor step.sample_request in
    list_creative_formats request builder.

    Prior behavior hardcoded list_creative_formats() { return {}; }, so
    any storyboard step declaring format_ids: ["..."] (or any other
    query param) in its sample_request hit the wire as an empty request.
    The agent returned unfiltered results and downstream round-trip /
    substitution-observer assertions failed silently (the agent looked
    non-conformant, but the filter had never been sent).

    Mirrors the pattern used by peer builders (build_creative,
    sync_creatives, etc.). No other API change.

    Closes #780.

  • b8b7fb2: Storyboard runner: fix spec-violating shapes and sample_request
    precedence across the SI + governance request builders. All affected
    builders now honor step.sample_request first (matching peer builders),
    and their synthetic fallbacks conform to the generated Zod schemas so
    framework-dispatch agents running strict validation at the MCP boundary
    no longer reject them with -32602 invalid_type.

    • si_get_offering: drop the string context and the out-of-schema
      identity; emit the prose string as optional intent (per
      si-get-offering-request.json, context is a ref to an object).
    • si_initiate_session: move prose from context (which must be an
      object) to required intent; default the identity fallback to the
      realistic anonymous handoff shape (consent_granted: false +
      anonymous_session_id) instead of consent_granted: true with an
      empty consented user — spec-legal either way, but the anonymous shape
      is what a host that hasn't obtained PII consent actually sends.
    • si_send_message / si_terminate_session: honor sample_request so
      storyboards can drive action_response, handoff_transaction,
      termination_context, and non-default reason paths without the
      fallback stomping the scenario.
    • sync_governance: lengthen default authentication.credentials to
      meet minLength: 32, and honor sample_request so fixtures like
      signal-marketplace/scenarios/governance_denied.yaml that author
      url: $context.governance_agent_url flow through.

    Closes #802.

  • 8d58987: Fix unbounded re-execution when a buyer SDK retries a mutating request against a handler whose response fails strict-mode validation (issue #758).

    Under the strict response-validation default, a drifted handler produced a VALIDATION_ERROR and released its idempotency claim on the way out, so the next retry re-entered the handler with the same drift — looping as fast as the buyer's retry budget allowed. The dispatcher now caches the VALIDATION_ERROR envelope under the same (principal, key, payloadHash) tuple for 10 seconds; retries on the same key short-circuit to the cached error instead of re-running side effects, and the cache clears itself before a handler fix would be gated on TTL expiry.

    A retry with a different canonical payload still produces IDEMPOTENCY_CONFLICT (the cache scopes on payload hash, same as the success cache), and a buyer that generates a fresh idempotency key per retry is not short-circuited — both behaviors are intentional. Same-key retry storms are the dominant failure mode; fresh-key loops already have the buyer's backoff as the correct control point.

    New IdempotencyStore.saveTransientError(...) method is optional on the interface — custom store implementations that want retry-storm protection can implement it; omitting it preserves the prior release-on-error behavior. Stores built via createIdempotencyStore pick it up automatically.

    Operational note. A drifted handler reachable by a hostile buyer is a cache-fill vector (every fresh key writes a 10s entry). Alert on sustained VALIDATION_ERROR rates per principal — steady-state should be zero.

  • c6bced1: Testing: schema-driven round-trip invariant for every storyboard request builder, plus fallback fixes so each builder's fallback round-trips through the generated Zod schema.

    Adds test/lib/request-builder-schema-roundtrip.test.js that iterates every task in TOOL_REQUEST_SCHEMAS (plus creative_approval and update_rights) and asserts the fallback request — empty context, empty sample_request, synthetic idempotency_key where required — parses cleanly against the matching schema from src/lib/types/schemas.generated.ts. New builders are picked up automatically.

    Running the invariant surfaced eight pre-existing fallbacks that had drifted out of spec. Fixed:

    • update_media_buy packages fallback now sets package_id.
    • update_rights / creative_approval fallbacks use rights_id (the spec field) instead of rights_grant_id; creative_approval now emits creative_url + `creat...
Read more

v5.11.0

22 Apr 07:24
372cd72

Choose a tag to compare

Minor Changes

  • 740f609: Add typed factory helpers for creative asset construction that inject the asset_type discriminator: imageAsset, videoAsset, audioAsset, textAsset, urlAsset, htmlAsset, javascriptAsset, cssAsset, markdownAsset, webhookAsset, plus a grouped Asset namespace (Asset.image({...})) over the same functions.

    Each helper takes the asset shape without asset_type and returns an object tagged with the canonical literal — imageAsset({ url, width, height }) produces { url, width, height, asset_type: 'image' } — eliminating the boilerplate at every construction site. The discriminator is written last in the returned object so a runtime bypass (cast that slips asset_type into the input) cannot overwrite it.

    Return type is Omit<T, 'asset_type'> & { asset_type: '<literal>' } (intersection) rather than the raw generated interface, so the builders compile regardless of whether the generated TypeScript types currently carry the discriminator — a defensive choice that makes the helpers stable across schema regenerations.

  • 828c112: Add createDefaultTestControllerStore to @adcp/client/testing — a default factory that wires every force_*, simulate_*, seed_* scenario against a generic DefaultSessionShape. Sellers provide loadSession / saveSession and get a conformance-ready TestControllerStore without hand-rolling 300+ lines of boilerplate. Supports partial overrides for sellers who need to customize specific handlers.

  • dd04ae9: Add @adcp/client/express-mcp middleware that rewrites JSON-only Accept headers so they pass the MCP SDK's StreamableHTTPServerTransport check when enableJsonResponse: true. Local escape hatch pending upstream SDK fix (modelcontextprotocol/typescript-sdk#1944).

  • 4dc4743: Storyboard cross-step invariants are now default-on. Bundled assertions (status.monotonic, idempotency.conflict_no_payload_leak, context.no_secret_echo, governance.denial_blocks_mutation) apply to every run unless a storyboard opts out — forks and new specialisms no longer ship with zero cross-step gating silently.

    • Storyboard.invariants now accepts an object form { disable?: string[]; enable?: string[] }. disable is the escape hatch that removes a specific default; enable adds a consumer-registered (non-default) assertion on top of the baseline. The legacy invariants: [id, ...] array form still works and is treated as additive on top of the defaults.
    • Behavior change for direct-API callers: resolveAssertions(['id']) now returns [...defaults, ...named] instead of exactly the named ids. Callers that relied on the array-only return shape (e.g., snapshotting resolveAssertions([...]).length) should switch to resolveAssertions({ enable: [...], disable: listDefaultAssertions() }) to reproduce the old semantics.
    • AssertionSpec gained an optional default?: boolean flag. Consumers registering custom assertions via registerAssertion(...) can opt their own specs into the default-on path.
    • resolveAssertions(...) fails fast on unknown ids in enable / the legacy array, and on disable ids that aren't registered as defaults (typo guard — a silent no-op would mask coverage gaps). Errors name the registered set and emit a Did you mean "..."? suggestion when one of the unknown ids is within Levenshtein distance 2 of a known id.
    • Unknown top-level keys on the object form (e.g. invariants: { disabled: [...] } — trailing d typo) throw instead of silently normalising to an empty disable set.
    • New export listDefaultAssertions() (re-exported from @adcp/client/testing) enumerates the default-on set for tooling / diagnostics.

    status.monotonic failure messages now include the legal next states from the anchor status and a link to the canonical enum schema, e.g.
    media_buy mb-1: active → pending_creatives (step "create" → step "regress") is not in the lifecycle graph. Legal next states from "active": "canceled", "completed", "paused". See https://adcontextprotocol.org/schemas/latest/enums/media-buy-status.json for the canonical lifecycle.
    Terminal states render as (none — terminal state) so the message is unambiguous.

  • 6a2c2c5: Add typed factory helpers for preview_creative render objects: urlRender, htmlRender, bothRender, plus a grouped Render namespace. Each helper takes the render payload without output_format and returns an object tagged with the canonical discriminator — urlRender({ render_id, preview_url, role }) produces a valid url-variant render without repeating output_format: 'url' at every call site.

    Mirrors the imageAsset / videoAsset pattern shipped in #771. PreviewRender is a oneOf on output_format (url / html / both) where the discriminator decides which sibling field becomes required. Matrix runs consistently surfaced renders missing either output_format or its required sibling — the helpers make the wrong shape syntactically harder to express because the input type requires the matching preview_url / preview_html per variant.

    Return type uses Omit<Variant, 'output_format'> & { output_format: <literal> } so the builders stay robust across schema regenerations. Discriminator is spread last so a runtime cast cannot overwrite the canonical tag.

    Skill pitfall callouts in build-creative-agent and build-generative-seller-agent now recommend the render helpers alongside the asset helpers.

  • 9d583aa: Extend the bundled status.monotonic default assertion to track the audience lifecycle alongside the seven resource types it already guards (adcontextprotocol/adcp#2836). sync_audiences responses carry per-audience status values (processing | ready | too_small) drawn from the newly-named spec enum at /schemas/enums/audience-status.json, and the assertion now rejects off-graph transitions across storyboard steps for every observed audience_id.

    Transition graph — fully bidirectional across the three states, matching the spec's permissive "MAY transition" hedging:

    • processing → ready | too_small on matching completion.
    • ready ↔ processing on re-sync (new members → re-match).
    • too_small → processing | ready on re-sync (more members → re-match, directly back to ready when the re-matched count clears the minimum).
    • ready ↔ too_small as counts cross minimum_size across re-syncs.

    Observations are drawn from sync_audiences responses only — discovery-only calls (request omits the audiences[] array) still return audiences[], so the extractor covers both write and read paths under the single task name. No separate list_audiences task exists in the spec. Actions deleted and failed omit status entirely on the response envelope; the extractor's id+status guard makes those rows silent (nothing to observe, nothing to check).

    Resource scoping is (audience, audience_id), independent from the other tracked resources. Unknown enum values drift-reset the anchor rather than failing — response_schema remains the gate for enum conformance.

    8 new unit tests cover the forward flow, the too_small → processing → ready re-sync path, bidirectional ready ↔ too_small, ready → processing on re-sync, self-edge silent pass, deleted/failed silent pass, per-audience-id scoping, and enum-drift tolerance. The assertion description now enumerates audience alongside the other resource types.

    Follow-up: wiring audience-sync/index.yaml with invariants: [status.monotonic] in the adcp spec repo once this release lands.

  • eca55c5: Storyboard runner auto-fires comply_test_controller seed scenarios from the fixtures: block (adcp-client#778).

    When a storyboard declares prerequisites.controller_seeding: true and carries a top-level fixtures: block, the runner now issues a comply_test_controller call per fixture entry before phase 1:

    • fixtures.products[]seed_product
    • fixtures.pricing_options[]seed_pricing_option
    • fixtures.creatives[]seed_creative
    • fixtures.plans[]seed_plan
    • fixtures.media_buys[]seed_media_buy

    Each entry's id field(s) ride on params; every other field is forwarded verbatim as params.fixture. The seed pass surfaces as a synthetic __controller_seeding__ phase in StoryboardResult.phases[] so compliance reports distinguish pre-flight setup from per-step buyer behavior.

    Grading semantics:

    • Seed failure cascade-skips remaining phases with detailed skip_reason: 'controller_seeding_failed' and canonical skip.reason: 'prerequisite_failed' — respects the runner-output-contract's six canonical skip reasons (controller_seeding_failed is a new RunnerDetailedSkipReason, not a new canonical value).
    • Agent not advertising comply_test_controller → cascade-skips with canonical skip.reason: 'missing_test_controller', implementing the spec's fixture_seed_unsupported not_applicable grade. No wire calls are issued.
    • Multi-pass mode seeds exactly once at the run level (inside runMultiPass) instead of N times inside each pass — avoids inflating failed_count / skipped_count by N when a fixture breaks.

    Closes the spec-side/seller-side gap. The fixtures: block (adcontextprotocol/adcp#2585, rolled out in adcontextprotocol/adcp#2743) and the seed_* scenarios (adcontextprotocol/adcp#2584, implemented here as SEED_SCENARIOS + createSeedFixtureCache) shipped without runner glue. Storyboards like sales_non_guaranteed, creative_ad_server, governance_delivery_monitor, media_buy_governance_escalation, and governance_spend_authority go from red to green against sellers that implement the matching seed* adapters.

    New StoryboardRunOptions.skip_controller_seeding. Opt out of the pre-flight for agents that load fixt...

Read more

v5.10.0

22 Apr 04:46
167c635

Choose a tag to compare

Minor Changes

  • 8f9260b: feat(server): request validation defaults to 'warn' outside production

    createAdcpServer({ validation: { requests } }) previously defaulted to
    'off' everywhere. It now defaults to 'warn' when
    NODE_ENV !== 'production', mirroring the asymmetric default already in
    place for responses ('strict' in dev/test, 'off' in production).

    Production behaviour is unchanged: the default stays 'off' when
    NODE_ENV === 'production', so prod request paths pay no AJV cost.

    What operators will see: in dev/test/CI, each incoming request that
    doesn't match the bundled AdCP request schema logs a single
    Schema validation warning (request) line through the configured
    logger, with the tool name and the field pointer. Nothing is rejected —
    the request still flows to the handler exactly as before. Node's test
    runner does not set NODE_ENV, so suites running under node --test
    fall into the dev/test bucket and will start emitting these warnings.

    How to opt out: pass validation: { requests: 'off' } on the server
    config, or set NODE_ENV=production for the process.

    Why: keeps request and response defaults symmetric, and prepares seller
    operators for upstream AdCP schema tightenings (e.g. adcp#2795, which
    introduces a required asset_type discriminator — buyer agents still
    on RC3 fixtures will lack it). Surfacing those drifts as warnings
    during development beats discovering them in a downstream consumer's
    VALIDATION_ERROR after deploy.

    Related: #694 (original intent for requests: 'warn') and #727 A
    (response-side default precedent).

  • 86a0fde: Register the fourth default cross-step assertion status.monotonic (adcontextprotocol/adcp#2664). Resource statuses observed across storyboard steps MUST transition only along edges in the spec-published lifecycle graph for their resource type. Catches regressions like active → pending_creatives on a media_buy, or approved → processing on a creative asset, that per-step validations cannot detect.

    Tracked lifecycles (one transition table per resource type, hardcoded against the enum schemas in static/schemas/source/enums/*-status.json in the spec repo, with bidirectional edges listed explicitly):

    • media_buy — forward flow pending_creatives → pending_start → active, active ↔ paused reversible, terminals completed | rejected | canceled.
    • creative (asset lifecycle) — forward flow processing → pending_review → approved | rejected, approved ↔ archived reversible, rejected → processing | pending_review allowed on re-sync, no terminals.
    • creative_approval — per-assignment on a package, forward pending_review → approved | rejected, rejected → pending_review allowed on re-sync.
    • accountactive ↔ suspended and active ↔ payment_required reversible, terminals rejected | closed.
    • si_session — forward active → pending_handoff → complete | terminated, terminals complete | terminated.
    • catalog_item — forward pending → approved | rejected | warning, approved ↔ warning reversible, rejected → pending allowed on re-sync.
    • proposal — one-way draft → committed.

    Observations are drawn from task-aware extractors on stepResult.response: create_media_buy / update_media_buy / get_media_buys, sync_creatives / list_creatives, nested .packages[].creative_approvals[], sync_accounts / list_accounts, si_initiate_session / si_send_message / si_terminate_session, sync_catalogs / list_catalogs (per-item), get_products (when the response carries a proposal). Unknown tasks produce no observations.

    State is scoped (resource_type, resource_id) so independent resources don't interfere. Self-edges (same status re-read) are silent pass. Skipped / errored / expect_error: true steps don't record observations. Unknown enum values (drift) reset the anchor without failing — response_schema catches enum violations.

    Failure output names the resource, the illegal transition, and the two step ids: media_buy mb-1: active → pending_creatives (step "create" → step "regress") is not in the lifecycle graph. Consumers who need a stricter variant can registerAssertion(spec, { override: true }).

    18 new unit tests cover forward flows, terminal enforcement, bidirectional edges, skip semantics, (resource_type, resource_id) scoping, nested creative_approval arrays, adcp_error-gated observations, enum-drift tolerance.

  • 573a176: Improve OAuth ergonomics for adcp storyboard run.

    • Fix classification: capability-discovery failures whose error message says "requires OAuth authorization" (the wording NeedsAuthorizationError emits) now classify as auth_required with the Save credentials: adcp --save-auth <alias> <url> --oauth remediation hint, instead of falling through to overall_status: 'unreachable' with no actionable advice. The keyword list in detectAuthRejection now matches "authorization" and "oauth" in addition to 401/unauthorized/authentication/jws/jwt/signature verification.
    • Surface the hint earlier: the OAuth remediation observation now fires whenever the error text looks OAuth-shaped, not only when discoverOAuthMetadata successfully walks the well-known chain — an agent that 401s before its OAuth metadata is resolvable still gets a useful hint.
    • Inline OAuth flow: adcp storyboard run <alias> --oauth now opens the browser to complete PKCE when the saved alias has no valid tokens, then proceeds with the run. Matches the existing adcp <alias> get_adcp_capabilities --oauth behavior so the two-step dance (--save-auth --oauth then storyboard run) is no longer required. Raw URLs still need --save-auth first; MCP only.

    Docs: docs/CLI.md and docs/guides/VALIDATE-YOUR-AGENT.md document both flows and add a troubleshooting row for the Agent requires OAuth failure.

  • 86ccc99: feat(server): response validation defaults to 'strict' outside production

    createAdcpServer({ validation: { responses } }) previously defaulted to
    'warn' when NODE_ENV !== 'production'. It now defaults to 'strict'
    in dev/test/CI so handler-returned schema drift fails with
    VALIDATION_ERROR (with the offending field path in details.issues)
    instead of logging a warning the caller can silently ignore.

    Production behaviour is unchanged: the default stays 'off' when
    NODE_ENV === 'production', so prod request paths pay no validation
    cost. Pass validation: { responses: 'warn' } to restore the previous
    dev-mode behaviour; validation: { responses: 'off' } opts out
    entirely.

    Why: the compliance:skill-matrix harness has repeatedly surfaced
    SERVICE_UNAVAILABLE from agents whose responses fail the wire schema.
    The dispatcher's response validator catches this drift with a clear
    field pointer, one layer that every tool inherits automatically. Making
    that the default catches it during handler development rather than in a
    downstream consumer.

    Migration: handler tests that use sparse fixtures (e.g.
    { products: [{ product_id: 'p1' }] }) will start returning
    VALIDATION_ERROR. Either fill in the missing required fields to match
    the AdCP schema, or set validation: { responses: 'off' } on the test
    server to keep the fixture intentionally minimal. Note that Node's
    test runner does not set NODE_ENV, so test suites running under
    node --test (with NODE_ENV=undefined) fall into the dev/test
    bucket and will start validating responses — this is intentional.

    Also: the VALIDATION_ERROR envelope's details.issues[].schemaPath
    is now gated behind exposeErrorDetails (same policy as the existing
    SERVICE_UNAVAILABLE.details.reason field). Production responses no
    longer leak #/oneOf/<n>/properties/... paths that fingerprint the
    handler's internal oneOf branch selection — buyers still get
    pointer, message, and keyword, which is sufficient to fix a
    drifted payload.

    Closes #727 (A).

  • f64007c: fix(server): tighten handler return types so schema drift fails tsc

    Two related tightenings close #727 (B).

    1. AdcpToolMap brand rights results. acquire_rights,
    get_rights, and get_brand_identity had result: Record<string, unknown> — a stale scaffold from before the response types were
    code-generated. Replaced with the proper generated types:

    • acquire_rightsAcquireRightsAcquired | AcquireRightsPendingApproval | AcquireRightsRejected

    • get_rightsGetRightsSuccess

    • get_brand_identityGetBrandIdentitySuccess

      2. DomainHandler return type. The handler return union
      previously included | Record<string, unknown> as a general escape
      hatch, so any handler could return any shape. Sparse returns like
      { rights_id, status: 'acquired' } passed tsc and only failed at
      wire-level validation. Handler return type is now just
      AdcpToolMap[K]['result'] | McpToolResponse, so drift fails at
      compile time. adcpError(...) still works — it returns
      McpToolResponse.

      Migration. If a handler returns a plain object literal without
      spelling out the full success shape, tsc will now flag the drift
      with an error like:

      Type '{ products: [{ product_id: 'p1' }] }' is not assignable to type
      'McpToolResponse | GetProductsResponse'.
        Property 'reporting_capabilities' is missing in type
        '{ product_id: 'p1' }' but required in type 'Product'.
      

      Two ways to fix:

    • Fill in the missing required fields to match the AdCP schema (what
      the wire-level validator would have demanded anyway). Use
      `DEFAULT_REPORTING...

Read more

v5.9.1

22 Apr 01:51
9acd1b3

Choose a tag to compare

Patch Changes

  • b1497f9: ci: consolidate pipeline and drop redundant jobs

    CI-only change, no runtime/library behaviour affected. Published package contents are unchanged.

    • ci.yml: collapse test / quality / security into a single job. Each was re-running checkout + setup-node + npm ci, wasting ~1–2 min of setup per PR. Also removes the clean && build:lib re-build in the old quality job and the redundant build step (alias of build:lib).
    • ci.yml: drop publish-dry-run. release.yml's prepublishOnly already validates packaging on the actual release PR.
    • ci.yml: drop dead develop branch from the push trigger.
    • schema-sync.yml: drop the PR-triggered validate-schemas job — ci.yml already syncs schemas and diffs generated files on every PR. Scheduled auto-update job preserved.
    • commitlint.yml: use npm ci instead of npm install --save-dev; the @commitlint/* packages are already in devDependencies.
  • 933eb2d: Two response-layer fixes for agents built from partial skill coverage:

    buildCreativeResponse / buildCreativeMultiResponse no longer crash on missing fields. The default summary previously dereferenced data.creative_manifest.format_id.id without guards — handlers that drop format_id (required by creative-manifest.json) crashed the dispatcher with Cannot read properties of undefined (reading 'id'), swallowing the real schema violation behind an opaque SERVICE_UNAVAILABLE. Now the summary optional-chains through the field chain and falls back to a generic string, so the response reaches wire-level validation and the buyer sees the actual missing-field error.

    replayed: false is no longer injected on fresh executions. protocol-envelope.json permits the field to be "omitted when the request was executed fresh"; emitting false violates strict task response schemas that declare additionalProperties: false (create-property-list-response, etc.). Fresh responses now drop any prior replayed marker; replays still carry replayed: true. The existing test/lib/idempotency-client.test.js "replayed omitted is surfaced as undefined" test aligns with this shift.

    Surfaced by matrix v10: six creative_generative pairs crashed with the dereference, and every property_lists pair hit the additionalProperties violation.

  • 5eb2ae9: fix(testing): context.no_secret_echo walks structured TestOptions.auth, and registerAssertion accepts { override: true }

    • The default context.no_secret_echo assertion in @adcp/client/testing
      previously treated options.auth as a string and added the whole
      discriminated-union object to its secret set. String.includes(obj)
      against [object Object] matched nothing, so the assertion was
      effectively a no-op for every consumer passing structured auth (bearer,
      basic, oauth, oauth_client_credentials). It now extracts the leaf
      secrets across every variant:

      • bearer: token
      • basic: username, password, and the base64 user:pass blob an
        Authorization: Basic header would carry
      • oauth: tokens.access_token, tokens.refresh_token,
        client.client_secret (confidential clients)
      • oauth_client_credentials: credentials.client_id and
        credentials.client_secret — resolving $ENV:VAR references to their
        runtime values so echoes of the real secret (not the reference string)
        are caught — plus tokens.access_token / tokens.refresh_token

      A minimum-length guard (8 chars) skips substring matching on fixture
      values that would otherwise collide with benign JSON.

    • registerAssertion(spec, { override: true }) now replaces an existing
      registration instead of throwing. Lets consumers swap in a stricter
      version of an SDK default (e.g. their own context.no_secret_echo)
      without calling clearAssertionRegistry() and re-registering every other
      default. Default behaviour ({ override: false } / no options) is
      unchanged and still throws on duplicate ids.

  • afc01f1: Widen two bundled default assertions per security-review feedback on adcontextprotocol/adcp#2769.

    idempotency.conflict_no_payload_leak — flip the denylist-of-5-fields to an allowlist of 7 envelope keys (code, message, status, retry_after, correlation_id, request_id, operation_id). The previous implementation only flagged payload, stored_payload, request_body, original_request, original_response — a seller inlining budget, start_time, product_id, or account_id at the adcp_error root slipped past, turning idempotency-key reuse into a read oracle for stolen-key attackers. Allowlisting closes the hole: anything a seller adds beyond the 7 envelope fields now fails the assertion.

    context.no_secret_echo — scan the full response body recursively (not just .context), add a bearer-token literal regex (/\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/i), add recursive suspect-property-name match (authorization, api_key, apikey, bearer, x-api-key), and pick up options.test_kit.auth.api_key as a verbatim-secret source. The previous scope (response.context only, verbatim options.auth_token/.auth/.secrets[] only) missed the common cases where sellers echo credentials into error.message, audit.incoming_auth, nested debug fields, or as header-shaped properties. All caller-supplied secrets gate on a minimum length (8 chars) to avoid false positives on placeholder values.

    Both changes are patch-level — the assertion ids, public registration API, and passing-case behavior are unchanged; the narrowing on main was fresh in 5.9 and had no adopters broad enough for the strictening to break in practice.

    governance.denial_blocks_mutation is unchanged.

    16 new unit tests cover both widenings: allowlist hits (valid envelope passes), denylist vestigial names still fail, non-allowlisted field leaks (including stable sorted error output), plus bearer literals, verbatim options.auth_token echo, options.secrets[] echo, test_kit.auth.api_key echo, suspect property names at any depth, array walking, short-value false-positive guard, and prose-"bearer" ignore.

v5.9.0

22 Apr 00:34
956e9cc

Choose a tag to compare

Minor Changes

  • 6180150: Fix A2A multi-turn session continuity + add pendingTaskId retention for HITL flows. Mirrors adcp-client-python#251.

    The bug. The A2A adapter (callA2ATool) never put contextId or taskId on the Message envelope — every send opened a fresh server-side session regardless of caller state. AgentClient compounded the error by storing result.metadata.taskId into currentContextId on every success, so the field that was supposed to carry the conversation id was actually carrying a per-task correlation id. Multi-turn A2A conversations against sellers that key state off contextId (ADK-based agents, session-scoped reasoning, any HITL flow) silently fell back to new-session-every-call.

    The fix.

    • callA2ATool takes a new session arg and injects contextId / taskId onto the Message per the @a2a-js/sdk type.
    • ProtocolClient.callTool threads session ids through to the A2A branch (MCP unaffected — no session concept there).
    • TaskExecutor stops aliasing options.contextId to the client-minted correlation taskId. The local taskId is now always a fresh UUID; the caller's contextId rides on the wire envelope only.
    • TaskResultMetadata gains contextId (server-returned A2A session id) and serverTaskId (server-tracked task id), populated from the response by ProtocolResponseParser.getContextId / getTaskId.
    • AgentClient retains contextId across sends (auto-adopted from server responses so ADK-style id rewriting is transparent) and tracks pendingTaskId only while the last response was non-terminal (input-required / working / submitted / auth-required / deferred). Terminal states clear pendingTaskId so the next call starts fresh.

    Public API (AgentClient).

    client.getContextId(); // read retained contextId
    client.getPendingTaskId(); // read pending server taskId (HITL resume)
    client.resetContext(); // wipe session state
    client.resetContext(id); // rehydrate persisted contextId across process restart

    setContextId(id) and clearContext() still exist for backwards compatibility (clearContext now delegates to resetContext()).

    One AgentClient per conversation. Sharing an instance across concurrent conversations interleaves session ids (last-write-wins) — create a fresh AgentClient or call resetContext() per logical conversation. Callers needing resume-across-process-restart should persist getContextId() / getPendingTaskId() after non-terminal responses and seed them back via resetContext(id) + direct setContextId on rehydration.

    Behavior change to note. TaskOptions.contextId no longer overrides the client-minted correlation taskId (which was its unintended side effect). Callers who were reading result.metadata.taskId expecting to see their caller-supplied contextId should now read result.metadata.contextId.

  • 0e7c1c9: createAdcpServer's dispatcher now auto-unwraps throw adcpError(...) into the normal response path. Handlers that throw an envelope (instead of return-ing it) used to surface as SERVICE_UNAVAILABLE: Tool X handler threw: [object Object] — the thrown value is a plain object, not an Error, so err.message is undefined and String(err) yields the [object Object] literal. The dispatcher now detects the envelope shape ({ isError: true, content: [...], structuredContent: { adcp_error: { code } } }) and returns it directly, preserving the typed code / field / suggestion exactly as if the handler had written return.

    Driver: matrix v8 showed this pattern persisting across fresh-Claude builds even when the skill examples use return. Fixing it at the dispatcher closes the class of bugs once, instead of hoping every skill-corpus update lands. A logger.warn still fires on unwrap so agent authors see they should switch to return, but buyers stop paying for the mistake.

    Idempotency claims are released on unwrap (same as any other thrown path) so retries proceed normally. Non-envelope throws (TypeError, custom errors, strings, objects without the full envelope shape) still surface as SERVICE_UNAVAILABLE with the underlying cause in details.reason — the existing handler-throw disclosure from PR #735 is unchanged.

  • 8c64d65: Bundle the governance.denial_blocks_mutation default assertion and auto-register the existing defaults on any @adcp/client/testing import (adcontextprotocol/adcp#2639, #2665 closed as superseded).

    New default assertion (default-invariants.ts):

    governance.denial_blocks_mutation — once a plan receives a denial signal (GOVERNANCE_DENIED, CAMPAIGN_SUSPENDED, PERMISSION_DENIED, POLICY_VIOLATION, TERMS_REJECTED, COMPLIANCE_UNSATISFIED, or check_governance returning status: "denied"), no subsequent step in the run may acquire a resource for that plan. Plan-scoped via plan_id (pulled from response body or the runner's recorded request payload — never stale step context). Sticky within a run: a later successful check_governance does not clear the denial. Write-task allowlist excludes sync_* batch shapes for now. Silent pass when no denial signal appears.

    Auto-registration wiring:

    storyboard/index.ts now side-imports default-invariants so any consumer of @adcp/client/testing picks up all three built-ins (idempotency.conflict_no_payload_leak, context.no_secret_echo, governance.denial_blocks_mutation). Previously only comply() triggered registration; direct runStoryboard callers against storyboards declaring invariants: [...] would throw unregistered assertion on resolve. Consumers who want to replace the defaults can clearAssertionRegistry() and re-register.

    Supersedes #2665 (the sibling @adcp/compliance-assertions package proposal): shipping these in-band is the lower-ceremony path and makes storyboards that reference the ids work out of the box against a fresh @adcp/client install.

  • 7aca3fa: Add typed CapabilityResolutionError for resolveStoryboardsForCapabilities (and by extension comply()). Addresses #734.

    The problem. The resolver threw plain Error instances for two distinct, actionable agent-config faults — "specialism has no bundle" and "specialism's parent protocol isn't declared in supported_protocols". Callers (AAO's compliance heartbeat, evaluate_agent_quality, the public applicable-storyboards REST endpoint) could only distinguish them by regexing the message, which broke if wording drifted and caused agent-config faults to page observability as system errors.

    The fix. Export CapabilityResolutionError extends ADCPError with a code discriminator and structured fields so callers can branch without parsing messages:

    import { CapabilityResolutionError } from '@adcp/client/testing';
    
    try {
      resolveStoryboardsForCapabilities(caps);
    } catch (err) {
      if (err instanceof CapabilityResolutionError) {
        switch (err.code) {
          case 'unknown_specialism':
            // err.specialism
            break;
          case 'specialism_parent_protocol_missing':
            // err.specialism, err.parentProtocol
            break;
        }
      }
    }

    Existing message text is preserved so regex-based callers keep working during the migration. The unknown_protocol code is reserved for future use — today an unknown supported_protocols entry still logs a console.warn and is skipped (fail-open), not thrown.

  • 7f27e8f: createAdcpServer now defaults validation.responses to 'warn' when process.env.NODE_ENV !== 'production'. Previously both sides defaulted to 'off', leaving schema drift to surface downstream as cryptic SERVICE_UNAVAILABLE or oneOf discriminator errors far from where the offending field lives.

    The new default catches handler-returned drift at wire-validation time with a clear field path, in dev/test/CI, where you want the signal. Production behavior is unchanged — set NODE_ENV=production and both sides stay 'off'.

    Override explicitly via createAdcpServer({ validation: { responses: 'off' | 'warn' | 'strict', requests: ... } }) — an explicit config always wins over the environment-derived default.

    This is the first half of the architecture fix tracked in #727 — validation belongs at the wire layer, not in response builders. Tightening generated TS discriminated unions so tsc catches sparse shapes is the remaining half.

    Cost: one AJV compile per tool on cold start + one validator invocation per response in dev. No effect on production.

  • 0cc20df: createAdcpServer's exposeErrorDetails now defaults to true outside NODE_ENV=production. Handler throws emit the underlying cause message and handler name in adcp_error.details + the human-readable text, so agent authors see SERVICE_UNAVAILABLE: Tool acquire_rights handler threw: Cannot find module '@adcp/client/foo' instead of the opaque encountered an internal error we used to ship.

    • Production behavior is unchanged (errors stay redacted for live agents).
    • Explicit exposeErrorDetails: false still wins — production deployments that want the redaction without relying on NODE_ENV should keep setting it.
    • logger.error('Handler failed', ...) now includes the full stack (err.stack) so server logs point at the exact line that blew up, not just the message.

    Matrix-harness debuggability was the driver: every SERVICE_UNAVAILABLE in matrix v5–v7 was an opaque black box that required re-running with --keep-workspaces and inspecting Claude-generated code to figure out why a handler threw. With this default, the matrix log shows the fault lin...

Read more

v5.8.2

21 Apr 18:59
9a9cd1b

Choose a tag to compare

Patch Changes

  • 2942e58: Fix createAdcpServer context echo for Sponsored Intelligence tools. si_get_offering and si_initiate_session define context as a domain-specific string on the request but require the protocol echo object on the response. The response auto-echo now only copies request.context when it is a plain object, so SI responses no longer fail with /context: must be object.

  • 56bbc59: Follow-up to the skill schema refresh (PR #716) targeting matrix failures that persisted:

    • DEFAULT_REPORTING_CAPABILITIES over hand-rolled literals — seller, generative-seller, and retail-media skill product examples previously hand-rolled reporting_capabilities: { ... } which drifts every time the spec adds a required field (most recently date_range_support in AdCP latest). Skills now use the SDK-provided constant and flag the drift tax explicitly.
    • create_media_buy must persist currency + total_budget — seller skill's createMediaBuy example flattens request total_budget: { amount, currency } into top-level currency + total_budget fields on the persisted buy, so subsequent get_media_buys responses pass the new required-field schema check. The old example stored only packages[].budget and the required top-level fields weren't reconstructable.
    • update_media_buy.affected_packages must be Package[], not string[] — seller skill's updateMediaBuy example now returns package objects ({ package_id, ... }) instead of bare IDs. The update-media-buy-response oneOf discriminator rejects string arrays with /affected_packages/0: must be object.
  • 7e04fa0: Option B (structural) groundwork — stop treating response shapes as hand-written forever:

    • generate-agent-docs.ts now extracts response schemas and emits a _Response (success branch):_ block under every tool in docs/TYPE-SUMMARY.md. For tools whose response is a oneOf success/error discriminator (e.g., update_media_buy), the generator picks the success arm (no errors required field) so builders see the happy-path shape. _Request:_ and _Response_ are now visually separated.
    • TYPE-SUMMARY.md is regenerated; every tool now carries both sides of the wire.
    • Seller + creative skills: added explicit top-level currency in getMediaBuyDelivery and getCreativeDelivery examples. The response schemas require it; the old examples omitted it and fresh-Claude agents built under those skills failed /currency: must have required property validation.

    Builders can now cross-reference hand-written skill examples against an auto-updating TYPE-SUMMARY response block. When the spec adds a required field, the generated doc updates immediately while the skill example may lag — that's the drift-detection signal.

    Next logical step (not in this PR): replace the hand-written **tool** — Response Shape blocks in skills with direct See [TYPE-SUMMARY.md § tool](…) pointers so the skill narrative focuses on logic and the shape stays generated.

v5.8.1

21 Apr 15:56
e9d437d

Choose a tag to compare

Patch Changes

  • f61f284: Re-export the storyboard assertion registry (registerAssertion,
    getAssertion, listAssertions, clearAssertionRegistry,
    resolveAssertions, and types AssertionSpec, AssertionContext,
    AssertionResult) from @adcp/client/testing so authors of invariant
    modules can import them from the documented package entry point. The
    underlying module (./storyboard/assertions) already exported these;
    only the parent ./testing index was missing the re-exports. Closes
    the gap introduced by #692.

  • bdebac9: refs_resolve scope: canonicalize $agent_url by stripping transport
    suffixes instead of comparing raw target URL to bare agent origins.

    Before this fix, storyboards using scope: { key: 'agent_url', equals: '$agent_url' } silently graded every source ref out_of_scope on MCP
    and A2A runners, because $agent_url expanded to the runner's target
    URL (with /mcp, /a2a, or /.well-known/agent.json suffixes) while
    refs carried the bare agent URL per AdCP convention. Net effect: the
    check degraded from integrity enforcement to a no-op on every MCP agent.

    The scope comparator now mirrors SingleAgentClient.computeBaseUrl:
    strip /mcp, /a2a, /sse, and /.well-known/agent[-card].json
    suffixes; lowercase scheme and host; drop default ports; strip
    userinfo, query, and fragment. Path below the transport suffix is
    preserved, so sibling agents at different subpaths on a shared host
    (e.g. https://publisher.com/.well-known/adcp/sales vs
    /.well-known/adcp/creative) remain distinguishable. Closes #710.

  • bdebac9: refs_resolve: harden grader-visible observation and actual.missing
    payloads against hostile agent responses.

    Compliance reports may be published or forwarded to third parties, so
    every ref field emitted by the runner is now:

    • Userinfo-scrubbed on URL-keyed fields via WHATWG URL parsing plus
      a regex fallback that scrubs scheme://user:pass@ shapes embedded
      in non-URL fields. Credentials planted in agent_url values can no
      longer leak through compliance output.
    • Scheme-restricted on URL-keyed fields: non-http(s) schemes
      (e.g. javascript:, data:, file:) are replaced with a
      <non-http scheme: …> placeholder so downstream UIs rendering
      agent_url as a link cannot inherit a stored-XSS vector.
    • Length-capped at 512 code points per string field, with a
      code-point-boundary truncation that preserves surrogate pairs.
    • Count-capped at 50 observations per check, with an
      observations_truncated marker when the cap fires. Meta
      observations (scope_excluded_all_refs, target_paginated)
      precede per-ref entries so the cap never drops primary signal.

    Match and dedup behavior is unchanged: the internal projection used
    for ref comparison is kept separate from the sanitized projection used
    for user-facing output, so truncation never false-collapses dedup
    keys. refsMatch and projectRef also now use hasOwnProperty to
    prevent storyboard authors from accidentally drawing match keys from
    Object.prototype. Closes #714.

  • bdebac9: refs_resolve: emit a scope_excluded_all_refs meta-observation when
    a scope filter partitions every source ref out. The integrity check
    enforces nothing when no ref falls in-scope; graders previously got a
    silent pass. The meta-observation surfaces the structural smell without
    changing pass/fail semantics. Suppressed under on_out_of_scope: 'ignore'
    (which explicitly opts out of scope warnings). Closes #711.

  • bdebac9: refs_resolve: detect paginated current-step targets and demote
    unresolved refs to observations instead of failing the check.

    Previously, when the target response carried pagination.has_more: true, any ref legitimately defined on a later page graded as
    missing — a false-positive failure against a conformant paginating
    seller. The runner now emits a target_paginated meta-observation and
    reports each would-be-missing ref as an unresolved_with_pagination
    observation, letting the check pass until the spec-level resolution
    lands (compliance mode requiring sellers to return everything
    referenced by products in a single response). Closes #712.

  • c4ff3e6: Skill example refresh to match recent upstream schema changes and fix a brand-rights coverage gap surfaced by the compliance:skill-matrix dogfood harness:

    • list_creative_formats.renders[]: upstream restructured renders to require role plus exactly one of dimensions (object) or parameters_from_format_id: true under oneOf. Updated seller, creative, generative-seller, and retail-media skill examples; flagged renders: [{ width, height }] as the canonical wrong shape.
    • get_media_buys.media_buys[]: currency and total_budget are now required per row. Seller skill example now shows both; added a persistence note (save these fields on create_media_buy so subsequent queries can echo them).
    • context response field: schema-typed as object. Across all 8 skills, rewrote the "Context and Ext Passthrough" section to stop recommending context: args.context echo (which fabricates string values when args.context is undefined or confused with domain fields like campaign_context). Explicit guidance: leave the field out of your return — createAdcpServer auto-injects the request's context object; hand-setting a non-object string fails validation and the framework does not overwrite.
    • Brand-rights governance flow: the brand_rights/governance_denied scenario expects the brand agent to call check_governance before issuing a license. Added accounts: { syncAccounts, syncGovernance } handlers and a checkGovernance() call in the acquireRights example, returning GOVERNANCE_DENIED with findings propagated from the governance agent.
    • Seller idempotency section: referenced adcontextprotocol/adcp-client#678 as a known grader-side limitation on the missing-key probe (MCP Accept header negotiation), so builders don't chase a skill fix for what's actually a grader issue.

v5.8.0

21 Apr 14:23
8f6b673

Choose a tag to compare

Minor Changes

  • 809d02e: adcp storyboard run gains --invariants <module[,module...]>. The flag
    dynamic-imports each specifier before the runner resolves
    storyboard.invariants, giving operators a way to populate the assertion
    registry (adcp#2639) without editing the CLI. Relative paths resolve against
    the current directory; bare specifiers resolve as npm packages.

    Modules are expected to call registerAssertion(...) at import time. The
    flag runs before the --dry-run gate so bad specifiers surface immediately
    during preview, not after agent resolution and auth.

    Applies to adcp storyboard run, adcp comply (deprecated alias), and
    adcp storyboard run --url multi-instance dispatch.

  • 46de887: Add createComplyController to @adcp/client/testing — a domain-grouped
    seller-side scaffold for the comply_test_controller tool. Takes typed
    seed / force / simulate adapters and returns { toolDefinition, handle, handleRaw, register } so a seller can wire the tool with a single
    controller.register(server) call.

    import { createComplyController } from '@adcp/client/testing';
    
    const controller = createComplyController({
      // Gate on something the SERVER controls — env var, resolved tenant flag,
      // TLS SNI match. Never trust caller-supplied fields like input.ext.
      sandboxGate: () => process.env.ADCP_SANDBOX === '1',
      seed: {
        product: ({ product_id, fixture }) => productRepo.upsert(product_id, fixture),
        creative: ({ creative_id, fixture }) => creativeRepo.upsert(creative_id, fixture),
      },
      force: {
        creative_status: ({ creative_id, status }) => creativeRepo.transition(creative_id, status),
      },
    });
    controller.register(server);

    The helper owns scenario dispatch, param validation, typed error
    envelopes (UNKNOWN_SCENARIO, INVALID_PARAMS, FORBIDDEN), MCP
    response shaping, and seed re-seed idempotency (same id + equivalent
    fixture returns previous_state: "existing"; divergent fixture returns
    INVALID_PARAMS without touching the adapter). Transition enforcement
    stays adapter-side so the controller and the production path share a
    single state machine.

    Hardened against common misuse: sandbox gate requires strict === true
    (a gate that returns a truthy non-boolean denies, not allows); fixture
    keys __proto__ / constructor / prototype are rejected with
    INVALID_PARAMS; the default seed-fixture cache is capped at 1000
    net-new keys to bound memory under adversarial seeding; and the
    toolDefinition.inputSchema is shallow-copied so multiple controllers
    on one process don't share a mutable shape.

    list_scenarios bypasses the sandbox gate so capability probes always
    succeed — buyer tooling can distinguish "controller exists but locked"
    from "controller missing", while state-mutating scenarios remain gated.
    register() emits a console.warn when no sandboxGate is configured
    and no ADCP_SANDBOX=1 / ADCP_COMPLY_CONTROLLER_UNGATED=1 env flag is
    set, so silent fail-open misuse becomes loud without breaking the
    optional-gate API shape.

    Also extends TestControllerStore with the five seed methods
    (seedProduct, seedPricingOption, seedCreative, seedPlan,
    seedMediaBuy) and exports SEED_SCENARIOS, SeedScenario,
    SeedFixtureCache, and createSeedFixtureCache. Existing
    registerTestController callers now pick up the seed surface and an
    internal idempotency cache for free. Closes #701.

  • d8fd93f: Add runConformance(agentUrl, opts) — property-based fuzzing against an
    agent's published JSON schemas, exposed as a new @adcp/client/conformance
    subpath export so fast-check and the schema bundle stay off the runtime
    client path. Closes #691.

    Under the hood: fast-check arbitraries derived from the bundled draft-07
    schemas at schemas/cache/latest/bundled/, paired with a two-path oracle
    that classifies every response as accepted (validates the response
    schema), rejected (well-formed AdCP error envelope with a spec-enum
    reason code — the accepted rejection shape), or invalid (schema
    mismatch, stack-trace leak, credential echo, lowercase reason code,
    mutated context, or missing reason code). Responses that cleanly reject
    unknown references count as passes, not failures.

    Stateless tier covers 11 discovery tools across every protocol:
    get_products, list_creative_formats, list_creatives,
    get_media_buys, get_signals, si_get_offering,
    get_adcp_capabilities, tasks_list, list_property_lists,
    list_content_standards, get_creative_features. Self-contained-state
    and referential-ID tiers are tracked for follow-up releases.

    import { runConformance } from '@adcp/client/conformance';
    
    const report = await runConformance('https://agent.example.com/mcp', {
      seed: 42,
      turnBudget: 50,
      authToken: process.env.AGENT_TOKEN,
    });
    if (report.totalFailures > 0) process.exit(1);

    See docs/guides/CONFORMANCE.md for the full options reference.

  • 7c0b146: Conformance fuzzer Phase 2 (#698) — referential tools, fixture injection,
    and adcp fuzz CLI.

    • Referential stateless tools: 6 new tools in the default run —
      get_media_buy_delivery, get_property_list, get_content_standards,
      get_creative_delivery, tasks_get, preview_creative. Random IDs
      exercise the rejection surface (agents must return
      REFERENCE_NOT_FOUND, not 500).
    • Fixtures: new RunConformanceOptions.fixtures option. When a
      request property name matches a pool (creative_id/creative_ids,
      media_buy_id/media_buy_ids, list_id, task_id, plan_id,
      account_id, package_id/package_ids), the arbitrary draws from
      fc.constantFrom(pool) instead of random strings — testing the
      accepted path on referential tools.
    • adcp fuzz <url> CLI: new subcommand with --seed, --tools,
      --turn-budget, --protocol, --auth-token, --fixture name=a,b,
      --format human|json, --max-failures, --max-payload-bytes, and
      --list-tools. Exits non-zero on failure. Reproduction hint on every
      failure: --seed <seed> --tools <tool>.
    adcp fuzz https://agent.example.com/mcp --seed 42
    adcp fuzz https://agent.example.com/mcp --fixture creative_ids=cre_a,cre_b --format json | jq

    New public exports: REFERENTIAL_STATELESS_TOOLS, DEFAULT_TOOLS,
    ConformanceFixtures, SkipReason.

  • 73db0ac: Conformance fuzzer Stage 4 — creative seeding, configurable brand,
    broader stack-trace detection, additionalProperties probing, and stricter
    context-echo enforcement.

    Coverage (A)

    • sync_creatives auto-seeder: preflights list_creative_formats,
      picks the first format whose required assets are all of a simple type
      (image, video, audio, text, url, html, javascript, css, markdown),
      synthesizes placeholder values, and captures creative_ids from the
      response. Now runs as part of seedFixtures / autoSeed.
    • seedBrand option + --seed-brand <domain> CLI flag: overrides
      the mutating-seeder brand reference. Defaults to
      { domain: 'conformance.example' }, which sellers with brand
      allowlists reject. Configurable per run.

    Oracle (D)

    • JVM + .NET stack-trace signatures: at com.foo.Bar.method(Bar.java:42)
      and at Foo.Bar() in X.cs:line 42 shapes detected alongside the
      existing V8/Python/Go/PHP patterns.
    • additionalProperties injection: when a schema permits extra keys
      (additionalProperties: true), the generator sometimes injects one
      (~15% frequency, single extra key from a fixed vocabulary). Exercises
      the unknown-field tolerance surface — a common crash source where
      agents deserialize into strict structs and reject unexpected keys.
    • Stricter context-echo: when a response schema declares a
      top-level context property, dropping it entirely is now an invariant
      violation. Silent tolerance preserved for tools whose response schema
      omits the field.

    New public exports: extended SeederName with 'sync_creatives',
    SeedOptions.brand, RunConformanceOptions.seedBrand.

  • 6b2a3b9: Conformance fuzzer Tier 3 — auto-seeding + update-tool fuzzing.

    • seedFixtures(agentUrl, opts) helper — creates a property list,
      a content-standards config, and (after a get_products preflight) a
      media buy on the agent, captures the returned IDs, and returns a
      ConformanceFixtures bag ready to pass to runConformance. Each
      seeder is best-effort: failures degrade to a recorded warning and an
      empty pool, never a thrown exception.
    • runConformance({ autoSeed: true }) — runs the seeder first,
      merges results into options.fixtures (explicit fixtures win on
      conflict), and includes Tier-3 update tools (update_media_buy,
      update_property_list, update_content_standards) in the default
      tool list. The report carries autoSeeded: boolean and a
      seedWarnings array.
    • adcp fuzz --auto-seed CLI flag. --list-tools now marks
      Tier-3 tools with (update — needs --auto-seed or --fixture). The
      human-readable report surfaces seeded IDs and any seed warnings.
    • New standards_ids fixture pool — content_standards uses
      standards_id, not list_id, so it gets its own key.

    ⚠️ Auto-seed mutates agent state. Point at a sandbox tenant — the
    fuzzer creates artifacts that t...

Read more

v5.7.0

20 Apr 23:06
92f97b1

Choose a tag to compare

Minor Changes

  • 7d33a92: AdCP 3.0 release blockers — SDK-level wiring for conformance-runner integration.

    New subpath exports

    • @adcp/client/compliance-fixtures — canonical COMPLIANCE_FIXTURES data for every hardcoded ID storyboards reference (test-product, sports_ctv_q2, video_30s, native_post, native_content, campaign_hero_video, gov_acme_q2_2027, mb_acme_q2_2026_auction, cpm_guaranteed, etc.) plus a seedComplianceFixtures(server) helper that writes fixtures into the state store under well-known compliance:* collections. Closes #663.
    • @adcp/client/schemas — re-exports every generated Zod request schema plus TOOL_INPUT_SHAPES (ready-to-register inputSchema map covering non-framework tools like creative_approval and update_rights) and a customToolFor(name, description, shape, handler) helper. Closes #667.

    Server (@adcp/client/server)

    • createExpressAdapter({ mountPath, publicUrl, prm, server }) returns the four pieces an Express-mounted agent needs: rawBodyVerify (captures raw bytes for RFC 9421), protectedResourceMiddleware (RFC 9728 PRM at the origin root), getUrl (mount-aware URL reconstruction for the signature verifier), and resetHook (delegates to server.compliance.reset()). Closes #664.
    • requireAuthenticatedOrSigned({ signature, fallback, requiredFor, resolveOperation }) bundles presence-gated signature composition with required_for enforcement on the no-signature path. requireSignatureWhenPresent grew an options parameter that carries the same requiredFor + resolveOperation semantics. Unsigned requests with no credentials on a required_for operation throw AuthError whose cause is RequestSignatureError('request_signature_required'); valid bearer bypass stays valid. Closes #665.
    • respondUnauthorized({ signatureError }) emits a WWW-Authenticate: Signature error="<code>" challenge when the rejection comes from the RFC 9421 verifier. serve() auto-detects this via signatureErrorCodeFromCause(err) — the signed_requests negative-vector grader reads the error code off the challenge, so previously callers had to override the 401 response by hand.
    • AdcpServer.compliance.reset({ force? }) drops session state and the idempotency cache between storyboards. Refuses to run in production-like deployments unless force: true is passed. IdempotencyStore.clearAll is now an optional method on the store; memoryBackend implements it, production backends leave it undefined. Closes #666.

    Testing (@adcp/client/testing)

    • Request-signing grader accepts an agentCapability option. When present, vectors whose verifier_capability can't coexist with the agent's declared profile (covers_content_digest disagreement, vector-asserted required_for not in agent's list) auto-skip with skip_reason: 'capability_profile_mismatch'. skipVectors stays available for operator-driven overrides. Closes #668.
  • 5b2ebb3: v3 audit follow-ups — tightened per expert review:

    Build pipeline

    • build:lib now runs sync-version before tsc so src/lib/version.ts can't drift from package.json across changeset-driven bumps. sync-version now validates both version strings against /^[0-9A-Za-z.\-+]+$/ to prevent template injection into the generated TS file.

    sync_creatives validator

    • New SyncCreativesItemSchema, SyncCreativesSuccessStrictSchema, and SyncCreativesResponseStrictSchema exports. The strict schema enforces: required creative_id + action; spec's conditional that status MUST be absent when action ∈ {failed, deleted}; preview_url limited to http(s): URLs; ISO-8601 expires_at; assignment_errors key regex. Wired into TOOL_RESPONSE_SCHEMAS so pipeline-level strict validation catches per-item drift for sync_creatives responses automatically.

    V3 guard

    • New VersionUnsupportedError with typed reason ('version' | 'idempotency' | 'synthetic'). Agent URL stays on the instance property but is omitted from the default message to prevent leakage into shared log sinks.
    • client.requireV3() now corroborates the v3 claim: requires majorVersions.includes(3), adcp.idempotency.replayTtlSeconds present, and rejects synthetic capabilities. Closes the "lying seller" bypass path.
    • New allowV2 config option on SingleAgentClientConfig — per-client bypass; ADCP_ALLOW_V2=1 env fallback only applies when allowV2 is undefined. Enables safe use in multi-tenant deployments.
    • requireV3ForMutations: true opt-in gates mutating calls before dispatch.