Skip to content

Releases: adcontextprotocol/adcp-client

@adcp/sdk@6.12.0

05 May 15:53
a2dbf9a

Choose a tag to compare

Minor Changes

  • a0e8e2e: CLI: -H / --header KEY=VALUE flag for arbitrary outbound HTTP headers (closes adcp-client#1563).

    npx adcp previously accepted --auth TOKEN but had no way to attach the routing/context headers that multi-tenant agents require alongside the bearer. Verify scripts couldn't reach a freshly-provisioned tenant on http://localhost:8000 because strategy 1 (Host-header → virtual host) falls through on localhost, leaving strategy 2 (x-adcp-tenant) the only option — and the CLI couldn't send it.

    The flag is repeatable, persists via --save-auth, and composes with every existing auth method:

    # Ad-hoc invocation
    npx adcp http://localhost:8000/mcp/ get_products '{...}' \
      --auth TOKEN \
      -H x-adcp-tenant=acme \
      -H Apx-Incoming-Host=tenant-acme.example.com
    
    # Persist on the saved alias
    npx adcp --save-auth tenant-acme http://localhost:8000/mcp/ \
      --auth TOKEN \
      -H x-adcp-tenant=acme
    
    # Saved headers flow through every subsequent invocation, including storyboard runs
    npx adcp tenant-acme storyboard run media_buy_seller

    Authorization and x-adcp-auth are reserved — --auth always wins on conflict. Custom values for those keys are dropped with a stderr warning rather than silently overriding the bearer (acceptance criterion #3 from the issue).

    Saved headers display as NAMES only in --list-agents (mirrors the redaction posture for oauth_client_credentials); values may carry tenant-routing tokens.

    Plumbing details:

    • parseHeaderFlags(args) (in bin/adcp.js) is the shared parser — used by the main one-shot tool call, --save-auth, and parseAgentOptions (so storyboard run and the capability-driven assessment also pick up CLI-supplied headers and merge with the saved alias).
    • AgentConfig.headers already existed in the SDK and is plumbed end-to-end through both MCP and A2A transports (src/lib/protocols/{mcp,a2a}.ts, src/lib/core/SingleAgentClient.ts); the change is purely CLI/config wiring on top.
    • TestOptions.headers added so storyboard runs honor saved/CLI headers via createTestClientagentConfig.headers. Composes with auth.basic (Basic auth still wins on Authorization).
    • Acceptance criteria from the issue:
      • -H K=V works for ad-hoc invocations (repeatable; both -H and --header, plus --header=K=V).
      • ✅ Per-agent headers field in ~/.adcp/config.json is honored.
      • ✅ Auth wins on conflict (Authorization / x-adcp-auth dropped with warning).

    Companion gap in the Python SDK (uvx adcp and AgentConfig.headers) is tracked separately in adcontextprotocol/adcp-client-python.

  • 65333ea: hello_seller_adapter_proposal_mode — canonical reference adapter for the v1.5 ProposalManager + DecisioningPlatform two-platform composition. New file at examples/hello_seller_adapter_proposal_mode.ts, ~550 LOC including all imports, types, comments, and boot wiring.

    Validates the v1.5 design end-to-end:

    • The full proposal lifecycle (brief → draft → refine → finalize → committed → accept) lives behind ProposalManager (~120 LOC of substantive logic). The framework's InMemoryProposalStore carries the draft → committed → consuming → consumed state machine; the adapter just wraps the upstream's /v1/proposals* endpoints.
    • sales.createMediaBuy(proposal_id) reads ctx.recipes (populated by the framework from the committed proposal) and uses recipe.upstream_ids.line_item_template_id to drive order creation. There's no second round-trip to the upstream's proposal store — the recipe IS the contract between the proposal-side and execution-side platforms.
    • Smoke-tested end-to-end against the sales-guaranteed mock-server: brief → draft proposal with 3 allocations → refine ("shift more to ctv") biases the mix → finalize commits with expires_atcreate_media_buy(proposal_id) creates an upstream order + 2 line items keyed off the recipe's ad_unit_ids and line_item_template_id.

    LOC comparison vs. existing direct-buy hello_seller_adapter_guaranteed: 549 LOC including the entire proposal lifecycle, vs. 1213 LOC for the direct-buy agent that has no proposal lifecycle. The v1.5 surface absorbs the lifecycle ceremony into the framework — adopters write business logic against a typed recipe instead of hand-rolling state machines.

    Companion CI gate: test/examples/hello-seller-adapter-proposal-mode.test.js runs the standard three-gate suite (strict tsc, storyboard pass, façade upstream-traffic check) against media_buy_seller/proposal_finalize. All three gates pass. Setup, brief_with_proposals, finalize_proposal, and accept_proposal pass; one allowlisted failure on refine_proposal traces to a spec-side gap (the proposal_finalize.yaml scenario lacks context_outputs / context_inputs declarations to chain the seller-minted proposal_id from brief_with_proposals into the refine step — the runner sends the literal placeholder balanced_reach_q2 from the spec's sample_request). The lifecycle works end-to-end when a real buyer threads the prior proposal_id, as confirmed by the manual smoke test in the commit message of the previous commit.

  • 65333ea: Mock-server recipes + proposal lifecycle — publish canonical {@link Recipe} shapes per sales specialism, plus add the proposal lifecycle endpoints to the sales-guaranteed mock. First step toward validating the v1.5 ProposalManager design against real adopter shapes.

    New canonical recipes (re-exported from @adcp/sdk/mock-server):

    • GAMLikeRecipe { recipe_kind: 'gam', network_code, ad_unit_ids[], line_item_priority, pricing, delivery_type, availability_window?, min_spend?, upstream_ids? } — for hello adapters wrapping a GAM-style guaranteed-direct upstream. Plus GAM_LIKE_OVERLAP (canonical CapabilityOverlap) and buildGAMLikeRecipe(mockProduct) builder.
    • KevelLikeRecipe { recipe_kind: 'kevel', network_code, zone_ids[], weight, pricing: { floor_cpm, target_cpm? }, goal_type, min_spend?, upstream_ids? } — for hello adapters wrapping a Kevel/OpenRTB-style auction-cleared remnant upstream. Plus KEVEL_LIKE_OVERLAP and buildKevelLikeRecipe(mockProduct) builder.

    The recipes are the typed contract between the adapter's ProposalManager and DecisioningPlatform: hello agents project these onto Product.implementation_config, the framework persists them through the proposal lifecycle, and ctx.recipes carries them back to sales.createMediaBuy / sales.updateMediaBuy so adapter code can drive the upstream off recipe fields without re-fetching.

    New sales-guaranteed mock-server endpoints (the lifecycle-aware specialism):

    • POST /v1/proposals — create draft from a brief; auto-allocates across guaranteed products (or filters to supplied product_ids); returns indicative pricing + draft state.
    • GET /v1/proposals/{id} — read state.
    • POST /v1/proposals/{id}/refine — apply allocation overrides + free-text steering hints (e.g. ask: "shift to ctv"); rejected on committed proposals.
    • POST /v1/proposals/{id}/finalize — promote draft → committed, lock pricing (indicative_cpm → locked_cpm), allocate upstream_line_item_template_id per allocation, and set a 24h expires_at inventory hold. Idempotent on re-finalize.

    The hello adapter's proposalManager will wrap these endpoints; getProducts becomes "list catalog + create draft proposal", refineProducts becomes "POST /refine", finalizeProposal becomes "POST /finalize". sales.createMediaBuy(proposal_id) reads the recipes from ctx.recipes (hydrated by the v1.5 framework dispatch wiring) to drive the upstream order creation against the locked line-item template ids.

    sales-non-guaranteed stays catalog-only — no draft → committed lifecycle, since auction-cleared remnant sells "right of first refusal at floor" without a finalize step. The Kevel-like recipe still applies (it captures how to flight a bid into the auction); just no proposal stages.

    Lesson surfaced by the validation play: the recipe capability_overlap.pricingModels and deliveryTypes axes must be derived per-product from the product's actual wire shape, not pulled from a static "what the platform supports" constant. The framework's validateOverlapSubsetOfWire correctly rejects the latter — a CPM-only product can't carry an overlap that claims cpv and cpcv even if the upstream platform supports them on other products. The buildGAMLikeRecipe / buildKevelLikeRecipe helpers now derive overlap fields from product.pricing.model and product.delivery_type. Lowercase pricing-model literals ('cpm', 'cpv') match the AdCP wire enum.

  • 4642329: ProposalManager v1.5 follow-ups — addresses every actionable concern from the parallel expert review on PR #1557 (code-reviewer / ad-tech-protocol-expert / security-reviewer / adtech-product-expert).

    Wire-leak prevention: strip Product.implementation_config

    Recipe data (network_code, ad_unit_ids, line_item_template_id, GAM line-item priority) rides on Product.implementation_config server-side as the typed contract between ProposalManager and DecisioningPlatform. The wire schema is additionalProperties: true so the field is technically legal on the wire — but emitting it leaks publisher topology to buyers. The framework now runs stripImplementationConfig at the dispatcher response-boundary chokepoint right after stripCtxMetadata, parallel pattern. New helpers stripImplementationConfig + hasImplementationConfig re-exported from @adcp/sdk/server. Test coverage in test/lib/proposal-implementation-config-strip.test.js.

    Also fixes a pre-existing latent leak surfaced by security review on this PR: dispatchHitl (the framework's task-handoff completion path used by `cr...

Read more

@adcp/sdk@6.11.0

04 May 14:38
6f3f1dc

Choose a tag to compare

Minor Changes

  • f2ae766: feat(server): ctx.handoffToTask accepts optional task_id override

    Adds options?: { task_id?: string } to ctx.handoffToTask(fn, options?).
    When options.task_id is set, the framework uses that exact string as the
    submitted task_id instead of minting a fresh one. Validates non-empty, ≤ 128
    characters. Closes #1554.

    Required for the force_create_media_buy_arm comply_test_controller scenario,
    which asserts the seller echoes the directive-supplied task_id verbatim on
    the create_media_buy submitted arm.

Patch Changes

  • c812000: fix(comply): pass extension params through simulate_delivery and simulate_budget_spend dispatchers

    comply_test_controller's params field is spec-canonical additionalProperties: true, but the SIMULATE_DELIVERY and SIMULATE_BUDGET_SPEND dispatcher cases in handleTestControllerRequest silently dropped all keys not in their fixed typed sets. Extension params like vendor_metric_values (used by the vendor_metric_accountability storyboard) never reached seller adapters.

    Both cases now spread the full params object verbatim. TestControllerStore.simulateDelivery and simulateBudgetSpend, along with SimulateDeliveryParams and SimulateBudgetSpendParams in createComplyController, gain [key: string]: unknown index signatures so extension fields are accessible to adapter authors without casting.

@adcp/sdk@6.10.0

04 May 09:38
75349bd

Choose a tag to compare

Minor Changes

  • cb1776c: credentialPolicy.tools now accepts a granular { allow: string[] } shape per-tool. Storefronts that legitimately accept ONE specific buyer-presented credential field (e.g. delivery.api_token on activate_signal) can permit only that path while still rejecting other credential-shaped keys — defense-in-depth scaling with the size of the exception, instead of opening the entire tool with 'lax'.

    credentialPolicy: {
      policy: 'authInfo-only',
      tools: {
        // Coarse: every credential-shaped key passes
        legacy_tool: 'lax',
    
        // Granular: ONLY the listed paths pass; other credential-shaped
        // keys still reject. Recommended over 'lax' wherever feasible.
        activate_signal: { allow: ['delivery.api_token'] },
      },
    }

    Allowlist entries are exact-match dotted paths (the same shape the scanner emits in details.credential_paths). Construction-time validation throws on empty allow lists, non-string entries, or unregistered tool names. Closes #1538.

  • f57a9c7: Security: add opt-in credentialPolicy server config that scans incoming buyer args for credential-shaped keys at any depth and rejects with PERMISSION_DENIED (details.scope: 'credentials') when configured 'authInfo-only'. Closes the buyer-args credential-smuggling vector class (top-level, nested context, nested ext) observed across three rounds of review on PR scope3data/agentic-adapters#248. Default 'lax' preserves existing behavior; opt in to enforce.

    Default patterns cover the common credential vocabulary: _token, _secret, _password, api_key, private_key, authorization, cookie, bearer, accessToken, refreshToken (case-insensitive). Patterns extensible via credentialPolicy.patterns.extend or fully replaceable via credentialPolicy.patterns.matcher. Per-tool overrides via credentialPolicy.tools (typo-validated against the registered tool set at construction).

    Rejection envelope reports paths only (never values) and bypasses params.context echo so the offending value does not round-trip through the response. Walker hardened against accessor-property getters: credential-named getters are flagged by name without invoking the getter, defending against throw / side-effect attacks on hand-built non-JSON inputs. /g and /y regex flags in adopter-supplied extend patterns are stripped to prevent lastIndex-based skip-alternation. See #1529.

  • c92390f: credentialPolicy.scanAuthInfo (default false) extends the credential-shaped scan to cover ctx.authInfo.extra at any depth, using the same pattern set as the args scan. Closes the leak surface where custom authenticators stamp credential-shaped values into authInfo.extra (token-introspection responses, JWT claim sets, OAuth scope blobs) and adopter handler code or log lines propagate them.

    createAdcpServer({
      credentialPolicy: {
        policy: 'authInfo-only',
        scanAuthInfo: true, // NEW: extend perimeter to authInfo.extra
      },
    });

    Fully orthogonal to policy mode. Adopters can mix policy: 'lax' + scanAuthInfo: true (trust args, defend authInfo log propagation) or any combination. Per-tool 'lax' overrides only affect the args scan — scanAuthInfo fires regardless.

    Wire-envelope discipline. Args-bag hits report in details.credential_paths (existing behavior). authInfo.extra hits are LOG-ONLY — paths surface in logger.warn server-side; the wire envelope reports a coarse signal (details.scope: 'credentials', recovery: 'terminal') without enumerating which extra field tripped the scan. Prevents an info-disclosure oracle on internal authInfo structure the buyer has no read access to.

    Default false because OAuth introspection blobs and JWT claims in extra will false-positive on default patterns like /_token$/i. Adopters opt in only when their authenticator keeps extra credential-clean. Closes #1539.

  • 1054e2f: Add createDynamicRegistry<TRegistries> — multi-registry plumbing with atomic-bundle-swap, in-flight refresh coalescing, and pinned-carry-forward semantics. Packages the multi-registry-atomicity idiom every adopter that hot-reloads tenants from a database independently rebuilds (the shim in scope3data/agentic-adapters built this three times before the pattern crystallized).

    Five lessons baked in: single-pointer atomic swap (concurrent readers see consistent snapshots across await), in-flight refresh coalescing (parallel refresh() calls share one Promise), pinned-carry-forward (entries with { pinned: true } survive every refresh; pin always wins over pending writes), lock-step unregister (clears across all registries), per-registry typed get.

    Two design refinements over the original issue: pinned: true flag at register time replaces the parallel staticIds() Set (single source of truth, no drift hazard); duplicate registration throws by default ({ overwrite: true } opt-in) so silent tenant clobbering doesn't ship to production. See #1531.

  • a9e658e: Add OperationalPlatform interface and defineOperationalPlatform factory for in-process consumers (price-optimization pollers, audience-sync task pollers, scheduled jobs, storefront fan-out paths) that don't carry an MCP request. Distinct from DecisioningPlatform (buyer-facing dispatch with RequestContext).

    Five-method surface: extractContext (synthesize per-call context from a stored token), updateMediaBuy (required), getMediaBuyDelivery (required, takes mediaBuyIds: readonly string[] matching the wire-spec plural field), pollAudienceStatuses (optional, returns Map<string, AudienceStatus> aligned with AudiencePlatform.pollAudienceStatuses), getProducts (optional). Methods throw AdcpError for structured rejection, matching DecisioningPlatform's convention.

    Type parameter OperationalPlatform<TCtx extends OperationalContext> carries adopter-specific context fields (advertiser id, sandbox mode, region) through every method without escape hatches.

    The named contract eliminates the seam every operational adopter would otherwise reinvent. v5 adapters duck-type-satisfy extractContext's shape (signature matches v5 PlatformAdapter.extractContext); methods that returned Result<T, E> in v5 / shim code need a Result-to-throw migration during adoption — replace if (r.err) handle(r.err) with try { ... } catch (e) { if (e instanceof AdcpError) handle(e); }. See #1530.

  • df023bb: Add an always-on storyboard summary surface and adcp specialism show for pre-flight inspection.

    Always-on summary at end of adcp storyboard run. Every run now writes a compact summary block to stderr with three status-driven markers: STORYBOARD-OK (passing), STORYBOARD-PARTIAL (wired but partly unexercised — silent tracks), and STORYBOARD-FAIL (failing / unreachable / auth_required, or any individual step failed). The marker is status-driven, not just failure-count-driven: an unreachable agent now correctly renders STORYBOARD-FAIL run ended unreachable instead of the silent green pass it produced before. CI authors can grep -q STORYBOARD-FAIL to surface failures regardless of --json mode or workflow continue-on-error: true wiring. When $GITHUB_STEP_SUMMARY is set (GitHub Actions), the same content is appended as a markdown table so PR reviewers see failures in the run summary panel without opening the log.

    Status-aware exit code. adcp storyboard run now exits 3 when overall_status is failing, unreachable, or auth_required — previously, an unreachable agent or auth-blocked run silently exited 0 because tracks_failed was zero. partial is preserved as exit 0 (some tracks ran silent — reportable but not a CI block). Runs that throw before comply() produces a result still exit 1 with a synthetic pre-flight/comply failure in the summary artifact (see below).

    Crash-path summary. When comply() itself throws (network down, capabilities parse error, TLS-policy refusal), the catch block now emits the same stderr block, $GITHUB_STEP_SUMMARY markdown, and --summary-output JSON via a synthetic buildCrashSummary artifact (overall_status: unreachable, single failure with storyboard_id: 'pre-flight', step_id: 'comply', reason_kind: 'error'). The always-on promise now holds on the path where it matters most: a Slack bot reading summary.json sees a valid schema_version: 1 payload precisely when the agent is broken hardest, instead of nothing.

    --summary-output <path>. New flag on storyboard run. Writes a narrow, schema-stable JSON artifact for downstream tooling (badges, Slack bots, dashboards):

    {
      "schema_version": 1,
      "agent_url": "...",
      "sdk_version": "6.9.0",
      "adcp_version": "3.0.6",
      "overall_status": "failing",
      "passed": 12,
      "failed": 2,
      "skipped": 1,
      "failures": [
        { "track": "media_buy", "storyboard_id": "...", "step_id": "...", "reason": "...", "reason_kind": "validation" }
      ]
    }

    failures[].reason_kind is a stable discriminator (error | validation | expected_mismatch | unspecified) so a Slack bot can color-code without regexing the reason string. Pin downstream tooling to this contract. The full ComplianceResult on stdout in --json mode evolves with the protocol; the summary doesn't.

    adcp specialism show <slug> (new top-level verb). Prints the resolved required scenarios, required tools, storyboard phases, and invariants for a specialism — answers "what is CI actually exercising against my server?" before runtime. adcp specialism list enumerates every specialism the compliance cache knows about. Both subcommands support --json. Specialisms get a top-level verb (parallel to storyboard show) rather than a flag on storyboard show so t...

Read more

@adcp/sdk@6.9.0

03 May 23:16
64445df

Choose a tag to compare

6.9.0 supersedes 6.8.0 (deprecated same-day). Adopters bumping from 6.7 should follow the curated path documented in docs/migration-6.7-to-6.9.md — two breaking recipes (framework-side comply_test_controller gate auto-wiring, compliance:skill-matrixcompliance:fork-matrix) and thirteen additive recipes covering Account.mode helpers, createAdcpServer.instructions async, BuyerAgentRegistry.extra forwarding, ConformanceClient Socket Mode, ComplyControllerConfig.force extension + queryUpstreamTraffic adapter, createDerivedAccountStore (Shape D), AdCP 3.0.6 schema bump (with tasks/get slash-form fix + AgentClient.executor), storyboard runner symmetric account resolution, codegen asset_type discriminator on Individual*Asset slots, skill prose collapse + skills/cross-cutting.md, five new worked references / mock-servers, and more. Self-grade checklist included.

Skipping 6.8? Yes — recommended path. 6.8.0 was published earlier with mixed-readiness content; 6.9.0 wraps the same surface plus the post-6.8 follow-up fixes that close it cleanly.


Full changelog (auto-generated)

The complete per-changeset detail follows below. The migration doc is the curated reading order; the changelog is for reference.### Minor Changes

  • dfa503b: Add queryUpstreamTraffic adapter to ComplyControllerConfig so adopters using the high-level complyTest: opts surface on createAdcpServerFromPlatform can wire query_upstream_traffic (spec PR adcontextprotocol/adcp#3816) without dropping to the lower-level registerTestController API.

    complyTest: {
      queryUpstreamTraffic: (params, _ctx) => {
        const result = recorder.query({
          principal: RECORDER_PRINCIPAL,
          ...(params.since_timestamp !== undefined && { sinceTimestamp: params.since_timestamp }),
          ...(params.endpoint_pattern !== undefined && { endpointPattern: params.endpoint_pattern }),
          ...(params.limit !== undefined && { limit: params.limit }),
        });
        return toQueryUpstreamTrafficResponse(result);
      },
    }

    The adapter forwards through to the existing TestControllerStore.queryUpstreamTraffic slot — no wire-shape change, no scenario-projection change. advertisedScenarios() includes 'query_upstream_traffic' when the adapter is set, so list_scenarios reports it.

    hello_signals_adapter_marketplace.ts migrated to use this surface — drops the manual registerTestController call after createAdcpServerFromPlatform. Closes the Phase 3 follow-up that punted this migration as "non-mechanical because the recorder integration would need a comply-adapter shape upstream." It does now.

    The framework gate inside createAdcpServerFromPlatform (Phase 2 of #1435) covers comply_test_controller end-to-end here too — admits on resolver-stamped mode: 'sandbox' | 'mock', refuses live-mode dispatch.

  • ee63e0d: feat(server): ConformanceClient — outbound-WebSocket Socket Mode primitive that lets adopter dev/staging MCP servers connect to a remote AdCP runner (today, Addie at agenticadvertising.org) without public DNS or inbound exposure. Three-line integration: new ConformanceClient({ url, token, server }).start(). Reverse-RPC at the TCP level only — MCP semantics unchanged. Dev/staging only by design (per AdCP #3986 deployment-scoped controller rule). Exposed from @adcp/sdk/server.

Patch Changes

  • e680866: Add audio creative-template support to the mock-server and hello adapter (Path 3 from the PR #1496 follow-up).

    The fork-matrix collapse claimed audio creative-template patterns but didn't ship runnable code for them. This patch fills that gap end-to-end without forking a separate adapter:

    • src/lib/mock-server/creative-template/seed-data.ts — extended output_kind union with 'audio_url'; seeded tpl_audiostack_spot_30s_v1 modeling the TTS / mix / master pipeline (text script → optional voice + music_bed → 30s mastered MP3). No dimensions; the existing queued → running → complete state machine already simulates the multi-minute render time real audio platforms (AudioStack, ElevenLabs, Resemble) take.
    • src/lib/mock-server/creative-template/server.ts — added an audio_url branch to synthesizeOutput that returns { audio_url: '<previewBase>.mp3', preview_url, assets: [{ kind: 'audio_url', mime_type: 'audio/mpeg' }] }.
    • examples/hello_creative_adapter_template.ts — extended UpstreamTemplate.output_kind and UpstreamRender.output to include the audio shape; added else if (out.audio_url) branch in projectRenderToManifest that wraps the URL with audioAsset({ url }) (the framework injects the asset_type: 'audio' discriminator into the creative-manifest oneOf).
    • skills/build-creative-agent/SKILL.md — replaced the "audio templates" paragraph with a worked-reference one citing the seeded audio template plus the audioAsset() projection. Notes that storyboard coverage for audio is not yet upstream (filed as adcontextprotocol/adcp#4015).

    Adopters integrating an audio creative platform now have a runnable round-trip path from npx adcp mock-server creative-template through the adapter to a complete creative-manifest with an audio asset. Validated via:

    const handle = await bootMockServer({ specialism: 'creative-template', port: 0 });
    const r = await fetch(handle.url + '/v3/workspaces/ws_acme_studio/renders', {
      method: 'POST',
      body: JSON.stringify({
        template_id: 'tpl_audiostack_spot_30s_v1',
        mode: 'build',
        inputs: [{ slot_id: 'script', value: '...' }],
        client_request_id: '1',
      }),
    });
    // queued → running → complete with { audio_url, preview_url, assets: [{ kind: 'audio_url' }] }

    Storyboard-grader coverage for audio is tracked at adcontextprotocol/adcp#4015 — the existing creative_template storyboard's build_creative step is hardcoded to display assets (image + headline + click_url), so audio adopters can't pass it today. Until that ships, audio adopters validate via npm run compliance:fork-matrix -- --test-name-pattern="hello-creative-adapter-template" (display + video gate inherited) plus the manual round-trip above.

    Pure additive — no breaking changes. Existing display + video templates unchanged; fork-matrix 23/23 still green.

  • 8361c7e: Add 3.0.6 to COMPATIBLE_ADCP_VERSIONS and bump 3.0.5 → 3.0.6 schema citations across user-facing surfaces.

    PR #1510 bumped ADCP_VERSION to 3.0.6 but didn't add 3.0.6 to the COMPATIBLE_ADCP_VERSIONS literal union in scripts/sync-version.ts. Callers passing { adcpVersion: '3.0.6' } to constructor options fell through the literal-union autocomplete to the (string & {}) escape hatch — still accepted at runtime, but no editor completion or compile-time signal. Closes the gap by appending '3.0.6' to the list and regenerating src/lib/version.ts via sync-version.

    Bumped citations elsewhere so the docs / examples / source comments line up with the pinned version:

    • skills/cross-cutting.md § Spec reference — schemas/cache/3.0.5/bundled/<protocol>/3.0.6
    • skills/SHAPE-GOTCHAS.md §7 — schemas/cache/3.0.5/enums/signal-catalog-type.json3.0.6
    • examples/hello_seller_adapter_social.ts/schemas/3.0.5/core/audience.json3.0.6
    • src/lib/server/decisioning/runtime/from-platform.tsschemas/cache/3.0.5/core/account-ref.json3.0.6

    docs/migration-6.6-to-6.7.md 3.0.5 references are historical (the migration target was 3.0.5) and stay unchanged.

    Tracked at adcp-client #1523 (waiting on adcp#4015 — audio variant phase for creative_template storyboard) and #1525 (waiting on adcp#4021 — output_only flag on format.assets[]). Both upstream issues remain OPEN.

    Validated: fork-matrix 23/23, typecheck + format clean.

    Pure additive at the wire and at the type level — COMPATIBLE_ADCP_VERSIONS extends backward compatibility, no existing strings change.

  • dd6cd4f: Bump pinned AdCP version from 3.0.5 to 3.0.6, drain the worked-example storyboard allowlist, and close three round-trip gaps surfaced while running end-to-end against the new fixtures.

    Spec sync. AdCP 3.0.6 ships two upstream fixture fixes that close the last storyboard gaps for the worked guaranteed example:

    • adcontextprotocol/adcp#3989media_buy_seller/inventory_list_targeting adds sandbox: true to all 5 account blocks. SDK side: createMediaBuyStore auto-echo (PR #1424).
    • adcontextprotocol/adcp#3990sales_guaranteed/create_media_buy uses task_completion.media_buy_id for context_outputs.path. SDK side: runner's task_completion.<path> prefix (PR #1426).

    #1416 (NOT_CANCELLABLE) closed in Phase 4 (assertMediaBuyTransition wired into the guaranteed adapter's update path).

    SDK fixes for the round-trip. Surfacing 3.0.6's new wire shape against hello_seller_adapter_guaranteed exposed three latent SDK gaps:

    1. tasks_get registered only under the underscore name. The buyer-side TaskExecutor.getTaskStatus calls the spec's slash form tasks/get via ProtocolClient.callTool. The framework now registers the tool under both tasks/get (spec) and tasks_get (legacy snake_case alias) so MCP buyers using the spec name reach the handler. Fixes the tasks/get poll timed out failure on every guaranteed HITL flow.
    2. tasks_get input schema accepted only {account_id} and rejected the natural-key {brand, operator, sandbox} arm — same shape gap that bit comply_test_controller in Phase 2. Schema is now the full canonical AccountReference (either arm passes; resolvers narrow at dispatch). Top-level .strict() preserved.
    3. AgentClient (the public client returned by multiClient.agent(id)) didn't expose its underlying...
Read more

@adcp/sdk@6.8.0

03 May 17:39
aaacd72

Choose a tag to compare

Minor Changes

  • 0dd3ca7: Add Account.mode convention + sandbox-authority helpers from @adcp/sdk/server.

    Phase 1 of the three-account-mode rollout (see docs/proposals/lifecycle-state-and-sandbox-authority.md). Establishes the type and the gate primitive; auto-wiring into the comply controller dispatch lands alongside mock-mode routing in Phase 2 (#1435).

    New surface:

    • AccountMode type — 'live' | 'sandbox' | 'mock'. Resolved-account convention; default 'live' when unspecified (fail-closed).
    • getAccountMode(account) — reads mode off any account-shaped value, with back-compat for legacy sandbox: boolean.
    • isSandboxOrMockAccount(account) — predicate: is the account non-production?
    • assertSandboxAccount(account, opts?) — throws AdcpError('PERMISSION_DENIED') (with details: { scope: 'sandbox-gate' }) for live-mode or missing accounts. Use to gate test-only surfaces.

    Pure additive: existing account.sandbox === true adopters keep working — the helpers infer mode: 'sandbox' from the legacy flag automatically. No behavior change for shipped code.

    Adopters who want stronger gating today can wire assertSandboxAccount(ctx.account, { tool: 'comply_test_controller' }) inside their sandboxGate(input) (after resolving the account). Phase 2 ships SDK-side auto-wiring so this becomes invisible.

  • d359c70: feat(server): createAdcpServer.instructions accepts an async function (#1393)

    The function form of instructions now supports returning a Promise<string | undefined>. The framework awaits the result during the MCP initialize handshake — the session does not proceed until the promise settles. This enables per-session prose fetched from an async source (brand-manifest registries, KV stores, real-time policy docs) without blocking server construction.

    createAdcpServer({
      // Async function — resolved at MCP initialize time, not at factory construction.
      instructions: async ctx => {
        const manifest = await brandManifests.get(ctx.tenant);
        return manifest?.intro ?? defaultProse;
      },
      onInstructionsError: 'skip', // or 'fail' for load-bearing policy
    });

    onInstructionsError: 'skip' | 'fail' governs async rejections identically to sync throws. Existing string-form and sync-function-form adopters are unaffected.

    New export: MaybePromise<T> type alias (T | Promise<T>) for use in async-optional callback signatures.

  • 54d2537: BuyerAgentRegistry: surface authenticator-stamped extra to resolveByCredential (issue #1484)

    BuyerAgentRegistry.bearerOnly and .mixed now forward authInfo.extra as a second
    optional argument to resolveByCredential. Adopters using prefix-based bearer conventions
    (e.g. demo tokens, tenant-encoded keys) can stamp extension data in their verifyApiKey.verify
    callback and recover it in the resolver without a pre-registered hash lookup.

    attachAuthInfo in serve.ts is also updated to propagate principal.extra from the
    AuthPrincipal returned by authenticate() into info.extra, closing the forwarding gap
    at the authenticator boundary.

    ResolveBuyerAgentByCredential gains an optional second parameter
    extra?: Record<string, unknown>. Existing single-argument implementations continue to
    satisfy the widened type without changes.

  • a6c71fb: Extend ComplyControllerConfig.force with create_media_buy_arm and task_completion slots, closing the gap between the low-level dispatcher and the structured-config façade.

    What was missing. ComplyControllerConfig.force (the typed config surface for createComplyController and createAdcpServerFromPlatform({ complyTest })) previously only exposed four slots — creative_status, account_status, media_buy_status, session_status. The dispatcher in test-controller.ts already handled force_create_media_buy_arm and force_task_completion (they are in CONTROLLER_SCENARIOS, SCENARIO_MAP, and the switch dispatch), but buildStore and advertisedScenarios had no bridge from the typed config to those store methods. Adopters on the structured config who implemented the underlying logic still hit UNKNOWN_SCENARIO every time.

    What's new.

    • ForceCreateMediaBuyArmParams{ arm: 'submitted' | 'input-required'; task_id?: string; message?: string }
    • ForceTaskCompletionParams{ task_id: string; result: Record<string, unknown> }
    • DirectiveAdapter<P> — adapter type returning ForcedDirectiveSuccess (distinct from ForceAdapter<P> which returns StateTransitionSuccess; create_media_buy_arm registers a pre-call directive, not a state transition)
    • ComplyControllerConfig.force.create_media_buy_arm?: DirectiveAdapter<ForceCreateMediaBuyArmParams>
    • ComplyControllerConfig.force.task_completion?: ForceAdapter<ForceTaskCompletionParams>
    • buildStore wires both adapters to store.forceCreateMediaBuyArm / store.forceTaskCompletion
    • advertisedScenarios pushes FORCE_CREATE_MEDIA_BUY_ARM / FORCE_TASK_COMPLETION when the corresponding adapter is present
    • testing/test-controller.ts client-side ControllerScenario union extended with 'force_create_media_buy_arm' and 'force_task_completion'

    All changes are additive (new optional slots, new exported types). No existing API is modified.

    Fixes #1472. Unblocks the media_buy_seller/create_media_buy_async storyboard and any other storyboard that drives force_create_media_buy_arm or force_task_completion through createComplyController.

  • 5fd83f1: Auto-wire the framework-side sandbox-authority gate inside createAdcpServerFromPlatform. Phase 2 of #1435.

    The framework now bypasses controller.register(server) for comply_test_controller and registers the tool itself, threading extra.authInfo through platform.accounts.resolve BEFORE dispatching. Under no circumstances does the controller operate on a live-mode account, regardless of what the caller claims on the wire — the resolved account is the trust boundary, not buyer-supplied flags like account.sandbox === true.

    What the gate does, in order:

    1. list_scenarios is exempt — capability probe, no state mutation.
    2. Resolve the account through platform.accounts.resolve(ref, { authInfo, toolName }). Reads the ref from top-level account (extended shape) or context.account (canonical AdCP routing).
    3. Admit when the resolved account's mode is 'sandbox' or 'mock' (legacy sandbox: true honored).
    4. Admit when no account resolves AND context.sandbox === true (migration window).
    5. Admit when process.env.ADCP_SANDBOX === '1' (deprecated env-fallback for back-compat).
    6. Otherwise refuse with a FORBIDDEN controller envelope.

    Fail-closed guard on the env fallback. ADCP_SANDBOX=1 was never meant to coexist with a resolver that names live accounts. The framework tracks every explicit mode value returned from platform.accounts.resolve in this process; if ADCP_SANDBOX=1 is set AND any live-mode account has been resolved, the gate THROWS loudly so operators notice in their logs. Remove ADCP_SANDBOX from your prod env and gate via mode: 'sandbox' on resolved accounts instead.

    Back-compat. Existing test platforms relying on process.env.ADCP_SANDBOX === '1' continue to work without modification — the env-fallback admits when the resolver doesn't stamp an explicit mode. Implicit-default-to-live (legacy adopter shape) does NOT trip the fail-closed guard; only deliberate mode: 'live' from the resolver does.

    Migration path. Stop setting ADCP_SANDBOX in production. Stamp mode: 'sandbox' (or 'live') on accounts your accounts.resolve returns; the gate then enforces strictly without the env fallback. The fallback emits no warning yet — a future minor will warn on each gate-permission grant; a future major will remove it entirely.

    Non-MCP transports (rare) keep the v5 behavior: when getSdkServer(server) returns null, the controller's own register(server) runs and the gate is a no-op for that surface. The gate is an MCP-side concern; A2A and other transports are wired separately.

  • d7c5767: feat(server): variadic composeMethod(inner, ...hooks) overload for stacking multiple guards without nesting

    Adds a variadic form of composeMethod so adopters can write composeMethod(inner, hookA, hookB, hookC) instead of composeMethod(composeMethod(composeMethod(inner, hookC), hookB), hookA). Semantics are identical to right-to-left manual nesting: before hooks run left-to-right, after hooks run right-to-left. The two-argument form is unchanged.

    Also adds a "When to use which approach" decision matrix to docs/recipes/composeMethod-testing.md covering preset-vs-inline tradeoffs, the requireOrgScope undefined-org gotcha, and onDeny evaluation-order implications.

    Closes #1444.

  • de0becf: Add creative-ad-server upstream-shape mock-server. Closes #1459 (sub-issue of #1381 hello-adapter-family completion).

    Pattern: GAM-creative / Innovid / Flashtalking / CM360 model — stateful creative library, format auto-detection, tag generation with macro substitution, real /serve/{id} HTML preview, synth delivery reporting with format-specific CTR baselines.

    Routes:

    • GET /_lookup/network — operator-from-domain routing (auth-free).
    • GET /_debug/traffic — façade-detection counters (auth-free).
    • GET /v1/formats — per-network format catalog.
    • POST /v1/creatives — write to library; format auto-detected from upload_mime + dimensions when format_id omitted; client_request_id idempotency with conflict-on-body-mismatch.
    • GET /v1/creatives — list with filters (advertiser_id, format_id, status, created_after, creative_ids); cursor...
Read more

@adcp/sdk@6.6.0

01 May 16:30
1f0df20

Choose a tag to compare

Minor Changes

  • a5da93e: feat(cli): adcp mock-server creative-template — second specialism in the matrix v2 family

    Adds a Celtra/Innovid/AudioStack-shaped creative platform mock alongside the existing signal-marketplace mock. Different multi-tenant pattern (URL-path workspace scoping vs the signals mock's X-Operator-Id header), different gotcha (async render lifecycle: queued → running → complete via polling).

    Headline characteristics:

    • Workspace-scoped paths: /v3/workspaces/{workspace_id}/.... Two seeded workspaces (ws_acme_studio for acmeoutdoor.example, ws_summit_studio for summit-media.example) with overlapping template visibility — Acme has 4 templates including video preroll; Summit has 3 display-only templates.
    • Async render pipeline: POST /renders returns 202 with status: queued; subsequent GETs progress through runningcomplete (or failed). Adapters have to poll, not assume sync. Idempotent on client_request_id per workspace, with 409 on body mismatch.
    • Templates as upstream-flavored format catalog: 4 seeded templates (300x250 medrec, 728x90 leaderboard, 320x50 mobile banner, 15s video preroll). Slot definitions use upstream vocabulary (slot_id) rather than AdCP's asset_role so the adapter does the projection.
    • Synthetic output: rendered HTML / JavaScript / VAST XML by output_kind. Plausible-looking but not real — the matrix tests adapter projection, not actual creative rendering.

    Refactors MockServerHandle to expose a unified principalMapping + principalScope shape so the matrix harness can build prompts for either specialism without specialism-specific seed-data introspection. Both signals (account.operatorX-Operator-Id) and creative-template (account.advertiserpath /v3/workspaces/...) flow through the same adapter prompt template.

    The matrix harness's bootUpstreamForHarness now consumes the unified handle shape; skill-matrix.json adds upstream: "creative-template" to the build-creative-agent × creative_template pair.

    Run with:

    npx @adcp/sdk mock-server creative-template --port 4501
    # or as part of the skill-matrix:
    npm run compliance:skill-matrix -- --filter creative_template

    12 new smoke tests in test/lib/mock-server/creative-template.test.js cover auth gating, workspace scoping, channel filtering, render lifecycle, idempotency replay, 409 on body mismatch, cross-workspace isolation, malformed JSON / unknown template error paths, VAST output for video templates, and the unified principal-mapping handle shape.

    Refs #1155.

  • ec19514: feat(cli): adcp mock-server <specialism> boots a fake upstream platform fixture for skill-matrix testing

    Adds npx @adcp/sdk mock-server signal-marketplace (currently the only specialism), which boots a CDP/DMP-shaped HTTP server modeled on LiveRamp / Lotame / Oracle Data Cloud. The mock represents the upstream platform an adopter wraps, not an AdCP-shaped agent — it has its own native API (cohorts/destinations/activations rather than signals/deployments) so skill-matrix runs test whether Claude can map an unfamiliar upstream to AdCP using the SDK + skill, not whether Claude can invent a decisioning platform from scratch.

    Headline characteristics of the signal-marketplace mock:

    • Multi-operator API key pattern — single Authorization: Bearer <api_key> shared across operator seats; per-request X-Operator-Id header determines cohort visibility, pricing, and activation scope. Real signal marketplaces all work this way; the mock surfaces the question "where does the SDK want me to put the principal-to-operator mapping?" as a real adopter would surface it.
    • Two seeded operators with overlapping cohort visibility — op_pinnacle (4 cohorts, all data providers) vs op_summit (2 Trident cohorts, +$1 CPM premium rate card). Forces the adapter to genuinely thread the operator from the AdCP account.operator field through to the upstream API or fail with empty/wrong data.
    • Activation lifecycle state machine — DSP/CTV destinations start pending, advance through in_progressactive on poll; agent destinations are synchronously active on create. Idempotent on client_request_id per operator (different operators using the same key are independent).
    • Cross-operator isolation — fetching another operator's activation returns 403 instead of 404 to prevent existence-oracle probing.

    The matrix harness (scripts/manual-testing/agent-skill-storyboard.ts, run-skill-matrix.ts) now accepts an optional upstream field per pair in skill-matrix.json. When set, it boots the mock-server before handing the workspace to Claude and surfaces the OpenAPI spec path + operator mapping table to Claude in the build prompt.

    Run with:

    npx @adcp/sdk mock-server signal-marketplace --port 4500
    # or as part of the skill-matrix:
    npm run compliance:skill-matrix -- --filter signal-marketplace

    Files added:

    • src/lib/mock-server/index.ts — specialism dispatcher
    • src/lib/mock-server/signal-marketplace/openapi.yaml — upstream API spec
    • src/lib/mock-server/signal-marketplace/seed-data.ts — operators, cohorts, destinations
    • src/lib/mock-server/signal-marketplace/server.ts — HTTP handlers
    • test/lib/mock-server/signal-marketplace.test.js — 8 smoke tests covering auth, operator scoping, pricing overrides, activation lifecycle, cross-operator isolation
    • bin/adcp.jsmock-server subcommand routing

    Background and design rationale: #1155.

  • 245089d: Storyboard runner: per-phase cascade scoping via phase.depends_on (#1161).

    Stateful cascade is now scoped per-phase rather than per-storyboard. Phases declare which prior phases they actually depend on for state; only those phases tripping their cascade gates the current phase's stateful steps. Independent phases run normally even when other phases tripped.

    Field shape: phase.depends_on?: string[] on StoryboardPhase. Default semantics (field absent) preserves the storyboard-scope behavior — implicit "depends on all prior phases." Backward-compatible with every existing storyboard, including the F6 round-2 cross-phase pattern (signal_marketplace/governance_denied).

    Two new modes:

    • depends_on: [] declares the phase independent. Runs even if every prior phase tripped its cascade. Use for phases whose state derives from the request body alone (e.g., audience_sync carrying its own account ref via brand+operator) rather than from prior-phase state.
    • depends_on: ['phase_id', ...] declares targeted dependencies. Only the named phases gate this phase's cascade; other tripped phases are irrelevant.

    Within-phase cascade preserved: stateful steps later in a phase still cascade-skip when an earlier stateful step in the same phase trips. Intra-phase state dependency is a storyboard authoring intent that depends_on (which scopes inter-phase cascade) doesn't override.

    Loader validation: forward references, self-references, and unknown phase IDs in depends_on fail loud at parse time. Empty list ([]) is legal.

    Companion spec issue: needs to be filed at adcontextprotocol/adcp to add the field to the storyboard schema and audit each specialism storyboard for which phases are functionally independent (notably sales-social where audience_sync / creative_push / event_setup / event_logging / financials are arguably independent of account_setup for explicit-mode platforms — the citrusad-shape 1/9/0 case).

    Diagnostic improvement: cascade-detail messages now reference the trigger from the specific dependency phase rather than a storyboard-scope first-trip, so reports show which dependency actually gated the skip.

Patch Changes

  • 84e25f1: fix(types): add 'advertiser' to DecisioningCapabilities.supportedBillings

    The billing-party schema enum allows 'operator' | 'agent' | 'advertiser', but the TypeScript type only declared the first two. Adopters building platforms that bill advertisers directly (Google Ads direct, Meta direct, retail-media-adjacent) could not declare this billing model via the typed interface. The runtime projection (from-platform.ts) already passes the value through verbatim, so this is a type-only fix.

  • 49747d5: feat(server): catalog-backed auto-seed for comply_test_controller

    When createAdcpServerFromPlatform is called with complyTest and a sales
    platform that wires getProducts, but without explicit seed.product or
    seed.pricing_option adapters, the framework now auto-derives those adapters
    from an in-memory store and wires a testController bridge so seeded products
    appear in get_products responses on sandbox requests.

    This removes the footgun where LLM-generated platforms fail comply storyboards
    because the slim skill guide doesn't mention that seed_product requires an
    explicit adapter. Publishers wiring getProducts now get free comply-sandbox
    seeding without writing any seed adapter code.

    The store is keyed by account.account_id so two sandbox accounts on the
    same server (multi-tenant TenantRegistry hosts, or single-tenant servers with
    multiple sandbox accounts) never share a seed namespace. Adopters who need
    tighter scoping (per-session, per-brand) wire bridgeFromSessionStore
    explicitly.

    Explicit seed.product / seed.pricing_option adapters and explicit
    testController bridges always take priority — the auto-seed is only applied
    when neither is present.

  • 14f011c: fix(server): default account.supported_billing to ['agent'] in createAdcpServerFromPlatform

    When the v6 framework emits the account block in `get_a...

Read more

@adcp/sdk@6.5.0

01 May 14:40
afbaebe

Choose a tag to compare

Minor Changes

  • e114e3a: Reverts #1142, removing storyboardContext?: StoryboardContext from AssertionContext and the per-step / final-step shallow-copy threading in the runner.

    The field was preemptive surface with no consumer. None of the bundled invariants (status.monotonic, idempotency.conflict_no_payload_leak, context.no_secret_echo, governance.denial_blocks_mutation) read it; programmatic assertions already accumulate cross-step state through ctx.state (which is exactly how status.monotonic's history works). Issue #1140 was about YAML validators (the check: clause), not assertion handlers — that scope is fully covered by #1141 (field_less_than / field_equals_context reading from ValidationContext.storyboardContext). The asymmetry is correct: declarative validators need context exposure, imperative assertions don't.

    Custom invariants that need cross-step state should continue using ctx.state (set in onStart, mutated in onStep, read in onEnd).

    Breaking-shape change to a public optional field, shipped as minor while 6.x is still in its breaking phase. Window-of-removal is hours old — the field landed in 6.4.0, no docs advertised it, no third-party consumer has had time to depend on it.

@adcp/sdk@6.4.1

01 May 14:36
2cf906a

Choose a tag to compare

Patch Changes

  • 4fada67: executeTask now returns a structured TaskResult instead of throwing for pre-flight errors (fixes #1148).

    Symptom: agent.executeTask('list_authorized_properties', {}) against a v2.5 MCP seller threw TypeError: Cannot read properties of undefined (reading 'status') instead of returning { success: false, status: 'failed', error: '...' }.

    Root cause: SingleAgentClient.executeTask (the public generic path used for tasks without a named wrapper) had no top-level try/catch. Pre-flight steps — feature validation, endpoint discovery, schema validation, version detection, and request adaptation — could escape as raw exceptions. The internal TaskExecutor.executeTask already wraps network-layer errors; SingleAgentClient had no matching safety net for the steps it runs before delegating to the executor.

    list_authorized_properties is the common trigger because it has no named helper method on AgentClient (deprecated in favour of get_adcp_capabilities) so all callers go through executeTask. On a v2.5 MCP seller, the response shape is unexpected and a TypeError escapes during pre-flight processing.

    Fix: Wrap the full SingleAgentClient.executeTask body in a try/catch. Structured protocol errors that carry typed fields callers use for recovery decisions are rethrown: AuthenticationRequiredError (and its subclass NeedsAuthorizationError), TaskTimeoutError, VersionUnsupportedError, and FeatureUnsupportedError. Unexpected errors (TypeErrors, schema-parser panics, etc.) are converted to { success: false, status: 'failed', error: message } envelopes matching the declared return type Promise<TaskResult<T>>. The fluent .match() method works correctly on error envelopes via attachMatch.

    Callers that followed the TypeScript return type and checked result.success / result.status are unaffected. Callers that relied on executeTask throwing for non-protocol pre-flight errors will now receive a structured failure envelope instead — which is the correct behaviour per the declared type.

  • 49a6ec3: Fix storyboard runner cascade over-firing for sole-stateful-step phases (adcp-client#1144).

    The F6 cascade-skip fix (6.1.0) deferred not_applicable cascade decisions to phase end, checking whether any peer stateful step established substitute state. This worked for snap (sync_accounts: not_applicable + list_accounts: passes) but still cascaded for adapters with a single stateful step in the phase and no peer-substitute (citrusad, amazon, criteo, google showing 1/9/0 on sales_social).

    The cascade now only fires when the phase contained other stateful peer steps that could have established substitute state but didn't. When the not_applicable step is the sole stateful step in the phase, no cascade fires — the platform manages state implicitly through a different model, which is valid per AdCP protocol semantics.

  • f397f9e: Fix v2.5 response validator spuriously rejecting null on optional envelope fields.

    v2.5 sellers built on Pydantic commonly emit errors: null, context: null, and ext: null to signal "nothing here" rather than omitting the key. After #1137 pinned validateResponseSchema to the detected server version, Ajv correctly validated these responses against the v2.5 schema — but the v2.5 schemas declare those fields as type: 'array' or type: 'object' without a null union, so every such response failed with /errors: must be array; /context: must be object; /ext: must be object.

    The fix adds a stripEnvelopeNulls pre-processing step inside validateResponse that strips top-level optional fields whose value is null but whose declared schema type is not nullable. Gated to v2.x schema bundles only — in v3, errors is a required field on failure branches and must not be silently dropped.

    Surfaced against Wonderstruck (v2.5 MCP) by scripts/smoke-wonderstruck-v2-5.ts (issue #1149).

@adcp/sdk@6.4.0

01 May 13:30
520fee4

Choose a tag to compare

Minor Changes

  • e76ab7d: Add field_less_than and field_equals_context cross-step comparison validators to the storyboard runner.

    These two new StoryboardValidationCheck kinds let storyboard authors assert relationships between a current-step response field and a value captured from an earlier step via context_outputs. The runtime accumulator is the existing storyboardContext (option 2 / context-outputs style), consistent with the refs_resolve validator precedent.

    • field_less_than — asserts a numeric field is strictly less than a comparand. The comparand is either a runtime context value (context_key) or a literal (value). Emits a type error if either operand is non-numeric; passes with a context_key_absent observation if the referenced context key was never populated (prior step may have been legitimately skipped on a branch-set path).
    • field_equals_context — asserts a field deep-equals a context-captured runtime value. Requires context_key. Same skip-with-observation behavior when the key is absent.

    Both validators require path. Both add context_key?: string to StoryboardValidation (ignored by all other check types).

    Enables the runner side of adcp#2642, which adds these check kinds to the universal storyboard schema enum once this lands.

  • 7b804ea: feat(conformance): expose per-step accumulated context in AssertionContext for cross-step comparison validators

    Adds storyboardContext?: StoryboardContext to AssertionContext. The runner
    now threads the accumulated context (all prior steps' context_outputs and
    convention-extracted values) into every assertion's context object before each
    onStep call, using the Option 2 / context-outputs style (same key namespace
    as $context.* placeholders and context_outputs entries).

    Assertion implementations can now read ctx.storyboardContext?.['my_key'] to
    compare values from a prior step against the current step's result. Missing
    keys return undefined; individual assertion handlers decide whether to skip
    or fail on absence.

    Implements the runner side of adcp-client#1140 / adcontextprotocol/adcp#2642.

Patch Changes

  • 5d98910: Fix storyboard runner cascade over-applying prerequisite_failed to steps independently not_applicable or missing_tool (adcp-client#1169).

    When an upstream stateful step trips the cascade, the runner now evaluates each downstream stateful step's intrinsic skip-eligibility before applying the cascade reason. If the agent never advertised the step's tool, the step is classified as missing_tool (passed: true) rather than prerequisite_failed (passed: false). This makes the storyboard report honest for agents with reduced specialism surfaces: missing_tool means "this agent doesn't claim this surface, by design", while prerequisite_failed means "this agent has a real setup bug affecting state that should have materialized."

  • b8c0872: Use DEFAULT_REPORTING_CAPABILITIES in decisioning-platform worked examples and SKILL.md quickstart. Updates broadcast-tv, mock-seller, and programmatic examples to import and reference the exported constant rather than hand-rolling reporting_capabilities inline. Adds the constant to the build-decisioning-platform imports cheat sheet and getProducts product literal so codegen agents produce schema-valid products on first try.

  • 2e0cb46: Cross-link the merged spec decision (adcp#3742, "synchronous response bodies are not signed — by design") in TenantConfig.signingKey's JSDoc, and add a "Self-signed dev path" recipe to docs/guides/SIGNING-GUIDE.md.

    The field's prior doc described the signing scope as "RFC 9421 response signing" — that wording predated the spec decision and didn't match what the SDK actually does. Updated to reflect: scope is webhook-signing only; the synchronous tools/call reply is not signed at the body level by deliberate design (TLS for sync, signed webhooks for async); adopters needing attestable artifacts for synchronous flows use the request-the-webhook pattern. Doc points at docs/building/understanding/security-model.mdx § "What gets signed — and what doesn't" for the canonical reasoning.

    The signing guide now carries the worked recipe for the multi-tenant self-signed dev loop: createTenantRegistry + createSelfSignedTenantKey() + createNoopJwksValidator() (gated to NODE_ENV ∈ {test, development} unless ADCP_NOOP_JWKS_ACK=1). Production promotion path covered (publish JWK to brand.json, swap in-memory key for KMS-backed SigningProvider). Plus the omit-key path for adopters who aren't ready to sign yet.

    No behavior change.

  • 3a9b7fe: Fix adaptSyncCreativesRequestForV2 to pass the role-keyed assets manifest through unchanged.

    PR #1118 introduced a flatten step that extracted the first role's asset from the manifest and passed it as a flat payload ({ asset_type, url, … }). This was incorrect: the v2.5 creative-asset.json schema declares assets using patternProperties keyed by role string — the same manifest shape v3 uses — so the flat output failed v2.5 schema validation on every field. The adapter now passes assets through verbatim, and the sync_creatives conformance fixture in adapter-v2-5-conformance.test.js has been updated from an expected_failures pin to a standard passing assertion.

  • 9cf9f9a: transport.maxResponseBytes hygiene: thread per-call override through TaskExecutor secondary call sites, rename ResponseTooLargeError field, add MCP integration test. Closes #1177.

    • TaskExecutor.getTaskStatus, listTasksForAgent, listTasks, getTaskList, continueTaskWithInput, and
      pollTaskCompletion now accept a per-call transport? override that beats the constructor-level cap.
      SubmittedContinuation.track exposes the per-call override; waitForCompletion inherits the
      transport cap from task-submission time (intentional — polling loops run an indefinite number of
      requests and a per-loop override would be a footgun).
    • ResponseTooLargeError.declaredContentLength renamed to contentLengthHeader (pre-release fix;
      the field was introduced in the same release cycle and has zero published consumer surface).
    • test/unit/mcp-tool-size-limit.test.js — end-to-end integration test proving the cap fires through
      ProtocolClient.callToolconnectMCPWithFallbackImplwrapFetchWithSizeLimit for the
      non-OAuth MCP path.

@adcp/sdk@6.3.0

01 May 12:38
0c44aff

Choose a tag to compare

Minor Changes

  • 89af100: Make TenantConfig.signingKey optional + auto-wire it into webhook signing.

    The SDK was stricter than the AdCP 3.x spec: signed-requests is a preview specialism and CLAUDE.md § Protocol-Wide Requirements explicitly classifies RFC 9421 HTTP Signatures as "optional but recommended." Adopters were forced to fabricate a TenantSigningKey (and stand up a published /.well-known/brand.json) before they could even register a tenant — and even then, the field's privateJwk wasn't auto-plumbed into the actual webhook signing pipeline, so adopters had to wire the same key TWICE (once on TenantConfig.signingKey for JWKS validation, once on serverOptions.webhooks.signerKey for outbound signatures).

    This change does two things:

    1. signingKey is now optional. When omitted, runValidation skips the JWKS roundtrip entirely and the tenant transitions straight from pending to healthy with reason: 'unsigned (no signingKey)'. AdCP 3.x treats request signing as optional, so adopters spiking the SDK before standing up KMS or publishing brand.json can ship without signing material. AdCP 4.0 will flip this back to required.

    2. When signingKey IS set, the registry auto-wires it into outbound webhook signing. The privateJwk now flows into serverOptions.webhooks.signerKey automatically. Set the key once on TenantConfig, get JWKS validation + signed webhooks. Strict on adcp_use: the JWK MUST carry adcp_use: "webhook-signing" per AdCP key-purpose discriminator (adcp#2423). Adopters who wire their own webhook signer on serverOptions.webhooks (KMS-backed, distinct keys per tenant, etc.) pass through unaffected — explicit config wins and auto-wiring is skipped.

    Supported JWK shapes for the auto-wire path: Ed25519 (kty=OKP, crv=Ed25519) and ECDSA P-256 (kty=EC, crv=P-256). RSA / EC P-384 throw with a remediation hint at register time.

    Two helpers ship alongside:

    • createSelfSignedTenantKey({ keyId? }) — generates an Ed25519 keypair via jose and returns a TenantSigningKey already tagged with adcp_use: "webhook-signing" so it passes the auto-wire assertion out of the box. No env gating; generating a keypair isn't dangerous.
    • createNoopJwksValidator() — validator that always returns { ok: true }. Refuses to construct outside NODE_ENV ∈ {'test', 'development'} unless the operator sets ADCP_NOOP_JWKS_ACK=1. Mirrors the idempotency: 'disabled' allowlist gate — NODE_ENV defaults to unset in raw Lambda / custom containers / many K8s deployments, so a === 'production' check would no-op in exactly the environments where a silent skip-validation start is most dangerous. The ack value must be the literal string '1'; 'true' / 'yes' lookalikes intentionally don't satisfy.

    Migration: existing adopters who pass an Ed25519 / EC P-256 signingKey need to add adcp_use: "webhook-signing" to both publicJwk and privateJwk. Adopters with RSA keys must rotate to Ed25519 / EC P-256 (RSA isn't in the AdCP signing-algorithm set) OR wire their webhook signer explicitly on serverOptions.webhooks to bypass the auto-wire.

    Migration note added to docs/migration-5.x-to-6.x.md § Common gotchas. New describe blocks in test/server-decisioning-tenant-registry.test.js: "unsigned tenants" (3.x optional path), "createSelfSignedTenantKey", "createNoopJwksValidator — NODE_ENV allowlist", "webhook-signing auto-wire" (auto-wire happy path + adcp_use enforcement + explicit-override bypass).

  • d0e2fe6: Storyboard runner: declared peer substitution + AccountStore: ctx.account threading + refreshToken hook.

    AccountStore — AccountToolContext<TCtxMeta> (#1145 Gap 1). New strict-superset of ResolveContext carrying the resolved Account<TCtxMeta>. getAccountFinancials now receives (req, ctx: AccountToolContext<TCtxMeta>) so adopters fronting an upstream platform can read tokens / upstream IDs from ctx.account.ctx_metadata without re-resolving. Resolves the 7-adapter pain point where getAccountFinancials was stubbed to UNSUPPORTED_FEATURE solely because the v6 surface didn't thread the resolved account through. Breaking change for v6.x adopters who already implemented getAccountFinancials — update the second arg type to AccountToolContext<TCtxMeta> and read ctx.account.ctx_metadata directly. Resolve-step null surfaces ACCOUNT_NOT_FOUND (terminal) before the platform method runs.

    AccountStore — refreshToken hook (#1145 Gap 2). Optional refreshToken(account, reason: 'auth_required'): Promise<{ token; expiresAt? }>. When defined and a platform method throws AdcpError({ code: 'AUTH_REQUIRED' }), the framework refreshes via this hook, mutates account.authInfo.token, and retries the platform method exactly once. In-flight scope only — refreshed tokens are not echoed back to the buyer. Refresh-hook failure surfaces correctable AUTH_REQUIRED so the buyer re-links via their UI flow. Resolves UniversalAds' v5-OAuth-provider workaround. Wired into getAccountFinancials dispatch; broader call-site wiring (sales / creative / audience methods) is incremental work — adopters can request additional sites as they hit the issue.

    Storyboard runner — peer_substitutes_for (#1144). Stateful steps can opt into substitute-aware cascade deferral by declaring peer_substitutes_for: <step_id> | <step_id>[] on the substitute step. When a stateful step skips with missing_tool / missing_test_controller AND a peer in the same phase declares it as a substitute, the runner defers the cascade decision to phase end and waives it iff the declared substitute passes. Cascade-detail messages now name the declared substitute that didn't pass when the substitution chain fails, so adopters reading skip reports see the substitution chain rather than a bare missing_tool cascade origin. Loader rejects cross-phase references, self-references, and non-stateful targets at parse time.

    This is contract hygiene + diagnostic improvement rather than a fix for any specific failing adopter — it tightens the implicit "phase membership = substitutability" rule that the F6 fix relied on, replacing it with explicit declaration so future storyboards with multi-stateful-step phases can't silently rescue non-substitutes. The legacy not_applicable any-peer rescue is unchanged for backward compat. Without a peer_substitutes_for declaration, missing reasons keep tripping the cascade immediately.

    The companion spec change at adcontextprotocol/adcp (storyboard schema field + sales-social/index.yaml edit) is required before any storyboard exercises the new path.

  • e0b08d2: Adds 'silent' to TrackStatus so the compliance grader can distinguish a track that observed real lifecycle transitions from one that ran with zero observations. Closes #1139, paired with adcontextprotocol/adcp#2834 on the grader-side rendering.

    status.monotonic (and other observation-based invariants) today report passed: true whether they validated three transitions or none. That collapses two different states behind one icon: real protection vs. wired-but-not-exercised. Tracks like property-lists, collection-lists, and content-standards — where the invariant is wired eagerly but no current phase exercises a lifecycle-bearing resource — render as green checks even though no protection was actually asserted.

    Three changes land together:

    • TrackStatus widens to 'pass' | 'fail' | 'skip' | 'partial' | 'silent'. A track is silent when every observation-bearing assertion record reports observation_count: 0 and nothing failed. Skip/fail/partial precedence is preserved — silent only triggers on otherwise-clean runs.
    • AssertionResult.observation_count?: number carries the run-level count from observation-based invariants. status.monotonic now defines an onEnd hook that emits a single record with observation_count: history.size, giving the rollup a deterministic signal whether to demote.
    • ComplianceSummary.tracks_silent and an updated formatComplianceResults render silent rows distinctly (🔇, "no lifecycle observed") instead of the green check.

    computeOverallStatus treats silent tracks as attempted (they ran) but never as unambiguously passing — a run with any silent track surfaces as partial. computeOverallStatus tolerates summaries serialized before this release (registry cache, fixtures) by defaulting tracks_silent to 0 when absent.

    Why widen the union instead of adding observable: boolean on AssertionResult (the alternative the triage proposals settled on): a non-breaking optional field lets every grader keep mapping { passed: true, observation_count: 0 } to a green check forever — exactly the bug we're fixing. The widened union forces consumers with exhaustive switches to make a deliberate decision about silent vs. pass, which is the protocol-correct outcome. Spec-side, adcontextprotocol/adcp#2834 can now adopt the same vocabulary verbatim.