Releases: adcontextprotocol/adcp-client
@adcp/sdk@6.12.0
Minor Changes
-
a0e8e2e: CLI:
-H/--header KEY=VALUEflag for arbitrary outbound HTTP headers (closes adcp-client#1563).npx adcppreviously accepted--auth TOKENbut 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 onhttp://localhost:8000because strategy 1 (Host-header → virtual host) falls through onlocalhost, 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
Authorizationandx-adcp-authare reserved —--authalways 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 foroauth_client_credentials); values may carry tenant-routing tokens.Plumbing details:
parseHeaderFlags(args)(inbin/adcp.js) is the shared parser — used by the main one-shot tool call,--save-auth, andparseAgentOptions(sostoryboard runand the capability-driven assessment also pick up CLI-supplied headers and merge with the saved alias).AgentConfig.headersalready 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.headersadded so storyboard runs honor saved/CLI headers viacreateTestClient→agentConfig.headers. Composes withauth.basic(Basic auth still wins on Authorization).- Acceptance criteria from the issue:
- ✅
-H K=Vworks for ad-hoc invocations (repeatable; both-Hand--header, plus--header=K=V). - ✅ Per-agent
headersfield in~/.adcp/config.jsonis honored. - ✅ Auth wins on conflict (Authorization / x-adcp-auth dropped with warning).
- ✅
Companion gap in the Python SDK (
uvx adcpandAgentConfig.headers) is tracked separately inadcontextprotocol/adcp-client-python. -
65333ea:
hello_seller_adapter_proposal_mode— canonical reference adapter for the v1.5 ProposalManager + DecisioningPlatform two-platform composition. New file atexamples/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'sInMemoryProposalStorecarries thedraft → committed → consuming → consumedstate machine; the adapter just wraps the upstream's/v1/proposals*endpoints. sales.createMediaBuy(proposal_id)readsctx.recipes(populated by the framework from the committed proposal) and usesrecipe.upstream_ids.line_item_template_idto 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-guaranteedmock-server: brief → draft proposal with 3 allocations → refine ("shift more to ctv") biases the mix → finalize commits withexpires_at→create_media_buy(proposal_id)creates an upstream order + 2 line items keyed off the recipe'sad_unit_idsandline_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.jsruns the standard three-gate suite (strict tsc, storyboard pass, façade upstream-traffic check) againstmedia_buy_seller/proposal_finalize. All three gates pass. Setup, brief_with_proposals, finalize_proposal, and accept_proposal pass; one allowlisted failure onrefine_proposaltraces to a spec-side gap (theproposal_finalize.yamlscenario lackscontext_outputs/context_inputsdeclarations to chain the seller-mintedproposal_idfrombrief_with_proposalsinto the refine step — the runner sends the literal placeholderbalanced_reach_q2from the spec'ssample_request). The lifecycle works end-to-end when a real buyer threads the priorproposal_id, as confirmed by the manual smoke test in the commit message of the previous commit. - The full proposal lifecycle (brief → draft → refine → finalize → committed → accept) lives behind
-
65333ea: Mock-server recipes + proposal lifecycle — publish canonical {@link Recipe} shapes per sales specialism, plus add the proposal lifecycle endpoints to the
sales-guaranteedmock. 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. PlusGAM_LIKE_OVERLAP(canonicalCapabilityOverlap) andbuildGAMLikeRecipe(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. PlusKEVEL_LIKE_OVERLAPandbuildKevelLikeRecipe(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, andctx.recipescarries them back tosales.createMediaBuy/sales.updateMediaBuyso adapter code can drive the upstream off recipe fields without re-fetching.New
sales-guaranteedmock-server endpoints (the lifecycle-aware specialism):POST /v1/proposals— create draft from a brief; auto-allocates across guaranteed products (or filters to suppliedproduct_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— promotedraft → committed, lock pricing (indicative_cpm → locked_cpm), allocateupstream_line_item_template_idper allocation, and set a 24hexpires_atinventory hold. Idempotent on re-finalize.
The hello adapter's
proposalManagerwill wrap these endpoints;getProductsbecomes "list catalog + create draft proposal",refineProductsbecomes "POST /refine",finalizeProposalbecomes "POST /finalize".sales.createMediaBuy(proposal_id)reads the recipes fromctx.recipes(hydrated by the v1.5 framework dispatch wiring) to drive the upstream order creation against the locked line-item template ids.sales-non-guaranteedstays 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.pricingModelsanddeliveryTypesaxes must be derived per-product from the product's actual wire shape, not pulled from a static "what the platform supports" constant. The framework'svalidateOverlapSubsetOfWirecorrectly rejects the latter — a CPM-only product can't carry an overlap that claimscpvandcpcveven if the upstream platform supports them on other products. ThebuildGAMLikeRecipe/buildKevelLikeRecipehelpers now derive overlap fields fromproduct.pricing.modelandproduct.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_configRecipe data (
network_code,ad_unit_ids,line_item_template_id, GAM line-item priority) rides onProduct.implementation_configserver-side as the typed contract betweenProposalManagerandDecisioningPlatform. The wire schema isadditionalProperties: trueso the field is technically legal on the wire — but emitting it leaks publisher topology to buyers. The framework now runsstripImplementationConfigat the dispatcher response-boundary chokepoint right afterstripCtxMetadata, parallel pattern. New helpersstripImplementationConfig+hasImplementationConfigre-exported from@adcp/sdk/server. Test coverage intest/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...
@adcp/sdk@6.11.0
Minor Changes
-
f2ae766: feat(server): ctx.handoffToTask accepts optional task_id override
Adds
options?: { task_id?: string }toctx.handoffToTask(fn, options?).
Whenoptions.task_idis 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_armcomply_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'sparamsfield is spec-canonicaladditionalProperties: true, but theSIMULATE_DELIVERYandSIMULATE_BUDGET_SPENDdispatcher cases inhandleTestControllerRequestsilently dropped all keys not in their fixed typed sets. Extension params likevendor_metric_values(used by thevendor_metric_accountabilitystoryboard) never reached seller adapters.Both cases now spread the full
paramsobject verbatim.TestControllerStore.simulateDeliveryandsimulateBudgetSpend, along withSimulateDeliveryParamsandSimulateBudgetSpendParamsincreateComplyController, gain[key: string]: unknownindex signatures so extension fields are accessible to adapter authors without casting.
@adcp/sdk@6.10.0
Minor Changes
-
cb1776c:
credentialPolicy.toolsnow accepts a granular{ allow: string[] }shape per-tool. Storefronts that legitimately accept ONE specific buyer-presented credential field (e.g.delivery.api_tokenonactivate_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
credentialPolicyserver config that scans incoming buyer args for credential-shaped keys at any depth and rejects withPERMISSION_DENIED(details.scope: 'credentials') when configured'authInfo-only'. Closes the buyer-args credential-smuggling vector class (top-level, nestedcontext, nestedext) 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 viacredentialPolicy.patterns.extendor fully replaceable viacredentialPolicy.patterns.matcher. Per-tool overrides viacredentialPolicy.tools(typo-validated against the registered tool set at construction).Rejection envelope reports paths only (never values) and bypasses
params.contextecho 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./gand/yregex flags in adopter-suppliedextendpatterns are stripped to preventlastIndex-based skip-alternation. See #1529. -
c92390f:
credentialPolicy.scanAuthInfo(defaultfalse) extends the credential-shaped scan to coverctx.authInfo.extraat any depth, using the same pattern set as the args scan. Closes the leak surface where custom authenticators stamp credential-shaped values intoauthInfo.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
policymode. Adopters can mixpolicy: 'lax' + scanAuthInfo: true(trust args, defend authInfo log propagation) or any combination. Per-tool'lax'overrides only affect the args scan —scanAuthInfofires regardless.Wire-envelope discipline. Args-bag hits report in
details.credential_paths(existing behavior).authInfo.extrahits are LOG-ONLY — paths surface inlogger.warnserver-side; the wire envelope reports a coarse signal (details.scope: 'credentials',recovery: 'terminal') without enumerating whichextrafield tripped the scan. Prevents an info-disclosure oracle on internal authInfo structure the buyer has no read access to.Default
falsebecause OAuth introspection blobs and JWT claims inextrawill false-positive on default patterns like/_token$/i. Adopters opt in only when their authenticator keepsextracredential-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 (parallelrefresh()calls share one Promise), pinned-carry-forward (entries with{ pinned: true }survive every refresh; pin always wins overpendingwrites), lock-step unregister (clears across all registries), per-registry typedget.Two design refinements over the original issue:
pinned: trueflag at register time replaces the parallelstaticIds()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
OperationalPlatforminterface anddefineOperationalPlatformfactory 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 fromDecisioningPlatform(buyer-facing dispatch withRequestContext).Five-method surface:
extractContext(synthesize per-call context from a stored token),updateMediaBuy(required),getMediaBuyDelivery(required, takesmediaBuyIds: readonly string[]matching the wire-spec plural field),pollAudienceStatuses(optional, returnsMap<string, AudienceStatus>aligned withAudiencePlatform.pollAudienceStatuses),getProducts(optional). Methods throwAdcpErrorfor structured rejection, matchingDecisioningPlatform'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 v5PlatformAdapter.extractContext); methods that returnedResult<T, E>in v5 / shim code need aResult-to-throw migration during adoption — replaceif (r.err) handle(r.err)withtry { ... } catch (e) { if (e instanceof AdcpError) handle(e); }. See #1530. -
df023bb: Add an always-on storyboard summary surface and
adcp specialism showfor 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), andSTORYBOARD-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 rendersSTORYBOARD-FAIL run ended unreachableinstead of the silent green pass it produced before. CI authors cangrep -q STORYBOARD-FAILto surface failures regardless of--jsonmode or workflowcontinue-on-error: truewiring. When$GITHUB_STEP_SUMMARYis 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 runnow exits 3 whenoverall_statusisfailing,unreachable, orauth_required— previously, an unreachable agent or auth-blocked run silently exited 0 becausetracks_failedwas zero.partialis preserved as exit 0 (some tracks ran silent — reportable but not a CI block). Runs that throw beforecomply()produces a result still exit 1 with a syntheticpre-flight/complyfailure 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_SUMMARYmarkdown, and--summary-outputJSON via a syntheticbuildCrashSummaryartifact (overall_status: unreachable, single failure withstoryboard_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 readingsummary.jsonsees a validschema_version: 1payload precisely when the agent is broken hardest, instead of nothing.--summary-output <path>. New flag onstoryboard 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_kindis 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 fullComplianceResulton stdout in--jsonmode 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 listenumerates every specialism the compliance cache knows about. Both subcommands support--json. Specialisms get a top-level verb (parallel tostoryboard show) rather than a flag onstoryboard showso t...
@adcp/sdk@6.9.0
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-matrix → compliance: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
queryUpstreamTrafficadapter toComplyControllerConfigso adopters using the high-levelcomplyTest:opts surface oncreateAdcpServerFromPlatformcan wirequery_upstream_traffic(spec PR adcontextprotocol/adcp#3816) without dropping to the lower-levelregisterTestControllerAPI.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.queryUpstreamTrafficslot — no wire-shape change, no scenario-projection change.advertisedScenarios()includes'query_upstream_traffic'when the adapter is set, solist_scenariosreports it.hello_signals_adapter_marketplace.tsmigrated to use this surface — drops the manualregisterTestControllercall aftercreateAdcpServerFromPlatform. 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) coverscomply_test_controllerend-to-end here too — admits on resolver-stampedmode: '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— extendedoutput_kindunion with'audio_url'; seededtpl_audiostack_spot_30s_v1modeling the TTS / mix / master pipeline (text script → optional voice + music_bed → 30s mastered MP3). No dimensions; the existingqueued → running → completestate machine already simulates the multi-minute render time real audio platforms (AudioStack, ElevenLabs, Resemble) take.src/lib/mock-server/creative-template/server.ts— added anaudio_urlbranch tosynthesizeOutputthat returns{ audio_url: '<previewBase>.mp3', preview_url, assets: [{ kind: 'audio_url', mime_type: 'audio/mpeg' }] }.examples/hello_creative_adapter_template.ts— extendedUpstreamTemplate.output_kindandUpstreamRender.outputto include the audio shape; addedelse if (out.audio_url)branch inprojectRenderToManifestthat wraps the URL withaudioAsset({ url })(the framework injects theasset_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 theaudioAsset()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-templatethrough 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_templatestoryboard'sbuild_creativestep is hardcoded to display assets (image + headline + click_url), so audio adopters can't pass it today. Until that ships, audio adopters validate vianpm 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.6toCOMPATIBLE_ADCP_VERSIONSand bump 3.0.5 → 3.0.6 schema citations across user-facing surfaces.PR #1510 bumped
ADCP_VERSIONto 3.0.6 but didn't add 3.0.6 to theCOMPATIBLE_ADCP_VERSIONSliteral union inscripts/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 regeneratingsrc/lib/version.tsviasync-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.6skills/SHAPE-GOTCHAS.md§7 —schemas/cache/3.0.5/enums/signal-catalog-type.json→3.0.6examples/hello_seller_adapter_social.ts—/schemas/3.0.5/core/audience.json→3.0.6src/lib/server/decisioning/runtime/from-platform.ts—schemas/cache/3.0.5/core/account-ref.json→3.0.6
docs/migration-6.6-to-6.7.md3.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_templatestoryboard) and #1525 (waiting on adcp#4021 —output_onlyflag onformat.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_VERSIONSextends 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#3989—media_buy_seller/inventory_list_targetingaddssandbox: trueto all 5 account blocks. SDK side:createMediaBuyStoreauto-echo (PR #1424).adcontextprotocol/adcp#3990—sales_guaranteed/create_media_buyusestask_completion.media_buy_idforcontext_outputs.path. SDK side: runner'stask_completion.<path>prefix (PR #1426).
#1416(NOT_CANCELLABLE) closed in Phase 4 (assertMediaBuyTransitionwired 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_guaranteedexposed three latent SDK gaps:tasks_getregistered only under the underscore name. The buyer-sideTaskExecutor.getTaskStatuscalls the spec's slash formtasks/getviaProtocolClient.callTool. The framework now registers the tool under bothtasks/get(spec) andtasks_get(legacy snake_case alias) so MCP buyers using the spec name reach the handler. Fixes thetasks/get poll timed outfailure on every guaranteed HITL flow.tasks_getinput schema accepted only{account_id}and rejected the natural-key{brand, operator, sandbox}arm — same shape gap that bitcomply_test_controllerin Phase 2. Schema is now the full canonicalAccountReference(either arm passes; resolvers narrow at dispatch). Top-level.strict()preserved.AgentClient(the public client returned bymultiClient.agent(id)) didn't expose its underlying...
@adcp/sdk@6.8.0
Minor Changes
-
0dd3ca7: Add
Account.modeconvention + 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:
AccountModetype —'live' | 'sandbox' | 'mock'. Resolved-account convention; default'live'when unspecified (fail-closed).getAccountMode(account)— readsmodeoff any account-shaped value, with back-compat for legacysandbox: boolean.isSandboxOrMockAccount(account)— predicate: is the account non-production?assertSandboxAccount(account, opts?)— throwsAdcpError('PERMISSION_DENIED')(withdetails: { scope: 'sandbox-gate' }) for live-mode or missing accounts. Use to gate test-only surfaces.
Pure additive: existing
account.sandbox === trueadopters keep working — the helpers infermode: '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 theirsandboxGate(input)(after resolving the account). Phase 2 ships SDK-side auto-wiring so this becomes invisible. -
d359c70: feat(server):
createAdcpServer.instructionsaccepts an async function (#1393)The function form of
instructionsnow supports returning aPromise<string | undefined>. The framework awaits the result during the MCPinitializehandshake — 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
extratoresolveByCredential(issue #1484)BuyerAgentRegistry.bearerOnlyand.mixednow forwardauthInfo.extraas a second
optional argument toresolveByCredential. Adopters using prefix-based bearer conventions
(e.g. demo tokens, tenant-encoded keys) can stamp extension data in theirverifyApiKey.verify
callback and recover it in the resolver without a pre-registered hash lookup.attachAuthInfoinserve.tsis also updated to propagateprincipal.extrafrom the
AuthPrincipalreturned byauthenticate()intoinfo.extra, closing the forwarding gap
at the authenticator boundary.ResolveBuyerAgentByCredentialgains an optional second parameter
extra?: Record<string, unknown>. Existing single-argument implementations continue to
satisfy the widened type without changes. -
a6c71fb: Extend
ComplyControllerConfig.forcewithcreate_media_buy_armandtask_completionslots, closing the gap between the low-level dispatcher and the structured-config façade.What was missing.
ComplyControllerConfig.force(the typed config surface forcreateComplyControllerandcreateAdcpServerFromPlatform({ complyTest })) previously only exposed four slots —creative_status,account_status,media_buy_status,session_status. The dispatcher intest-controller.tsalready handledforce_create_media_buy_armandforce_task_completion(they are inCONTROLLER_SCENARIOS,SCENARIO_MAP, and theswitchdispatch), butbuildStoreandadvertisedScenarioshad no bridge from the typed config to those store methods. Adopters on the structured config who implemented the underlying logic still hitUNKNOWN_SCENARIOevery 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 returningForcedDirectiveSuccess(distinct fromForceAdapter<P>which returnsStateTransitionSuccess;create_media_buy_armregisters a pre-call directive, not a state transition)ComplyControllerConfig.force.create_media_buy_arm?: DirectiveAdapter<ForceCreateMediaBuyArmParams>ComplyControllerConfig.force.task_completion?: ForceAdapter<ForceTaskCompletionParams>buildStorewires both adapters tostore.forceCreateMediaBuyArm/store.forceTaskCompletionadvertisedScenariospushesFORCE_CREATE_MEDIA_BUY_ARM/FORCE_TASK_COMPLETIONwhen the corresponding adapter is presenttesting/test-controller.tsclient-sideControllerScenariounion 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_asyncstoryboard and any other storyboard that drivesforce_create_media_buy_armorforce_task_completionthroughcreateComplyController. -
5fd83f1: Auto-wire the framework-side sandbox-authority gate inside
createAdcpServerFromPlatform. Phase 2 of #1435.The framework now bypasses
controller.register(server)forcomply_test_controllerand registers the tool itself, threadingextra.authInfothroughplatform.accounts.resolveBEFORE dispatching. Under no circumstances does the controller operate on alive-mode account, regardless of what the caller claims on the wire — the resolved account is the trust boundary, not buyer-supplied flags likeaccount.sandbox === true.What the gate does, in order:
list_scenariosis exempt — capability probe, no state mutation.- Resolve the account through
platform.accounts.resolve(ref, { authInfo, toolName }). Reads the ref from top-levelaccount(extended shape) orcontext.account(canonical AdCP routing). - Admit when the resolved account's
modeis'sandbox'or'mock'(legacysandbox: truehonored). - Admit when no account resolves AND
context.sandbox === true(migration window). - Admit when
process.env.ADCP_SANDBOX === '1'(deprecated env-fallback for back-compat). - Otherwise refuse with a
FORBIDDENcontroller envelope.
Fail-closed guard on the env fallback.
ADCP_SANDBOX=1was never meant to coexist with a resolver that names live accounts. The framework tracks every explicitmodevalue returned fromplatform.accounts.resolvein this process; ifADCP_SANDBOX=1is set AND any live-mode account has been resolved, the gate THROWS loudly so operators notice in their logs. RemoveADCP_SANDBOXfrom your prod env and gate viamode: '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 deliberatemode: 'live'from the resolver does.Migration path. Stop setting
ADCP_SANDBOXin production. Stampmode: 'sandbox'(or'live') on accounts youraccounts.resolvereturns; 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 ownregister(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 nestingAdds a variadic form of
composeMethodso adopters can writecomposeMethod(inner, hookA, hookB, hookC)instead ofcomposeMethod(composeMethod(composeMethod(inner, hookC), hookB), hookA). Semantics are identical to right-to-left manual nesting:beforehooks run left-to-right,afterhooks 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.mdcovering preset-vs-inline tradeoffs, therequireOrgScopeundefined-org gotcha, andonDenyevaluation-order implications.Closes #1444.
-
de0becf: Add
creative-ad-serverupstream-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 fromupload_mime+ dimensions whenformat_idomitted;client_request_ididempotency with conflict-on-body-mismatch.GET /v1/creatives— list with filters (advertiser_id,format_id,status,created_after,creative_ids); cursor...
@adcp/sdk@6.6.0
Minor Changes
-
a5da93e: feat(cli):
adcp mock-server creative-template— second specialism in the matrix v2 familyAdds a Celtra/Innovid/AudioStack-shaped creative platform mock alongside the existing
signal-marketplacemock. Different multi-tenant pattern (URL-path workspace scoping vs the signals mock'sX-Operator-Idheader), different gotcha (async render lifecycle:queued → running → completevia polling).Headline characteristics:
- Workspace-scoped paths:
/v3/workspaces/{workspace_id}/.... Two seeded workspaces (ws_acme_studioforacmeoutdoor.example,ws_summit_studioforsummit-media.example) with overlapping template visibility — Acme has 4 templates including video preroll; Summit has 3 display-only templates. - Async render pipeline:
POST /rendersreturns 202 withstatus: queued; subsequent GETs progress throughrunning→complete(orfailed). Adapters have to poll, not assume sync. Idempotent onclient_request_idper 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'sasset_roleso 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
MockServerHandleto expose a unifiedprincipalMapping+principalScopeshape so the matrix harness can build prompts for either specialism without specialism-specific seed-data introspection. Both signals (account.operator→X-Operator-Id) and creative-template (account.advertiser→path /v3/workspaces/...) flow through the same adapter prompt template.The matrix harness's
bootUpstreamForHarnessnow consumes the unified handle shape;skill-matrix.jsonaddsupstream: "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_template12 new smoke tests in
test/lib/mock-server/creative-template.test.jscover 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.
- Workspace-scoped paths:
-
ec19514: feat(cli):
adcp mock-server <specialism>boots a fake upstream platform fixture for skill-matrix testingAdds
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-marketplacemock:- Multi-operator API key pattern — single
Authorization: Bearer <api_key>shared across operator seats; per-requestX-Operator-Idheader 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) vsop_summit(2 Trident cohorts, +$1 CPM premium rate card). Forces the adapter to genuinely thread the operator from the AdCPaccount.operatorfield through to the upstream API or fail with empty/wrong data. - Activation lifecycle state machine — DSP/CTV destinations start
pending, advance throughin_progress→activeon poll; agent destinations are synchronouslyactiveon create. Idempotent onclient_request_idper 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 optionalupstreamfield per pair inskill-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-marketplaceFiles added:
src/lib/mock-server/index.ts— specialism dispatchersrc/lib/mock-server/signal-marketplace/openapi.yaml— upstream API specsrc/lib/mock-server/signal-marketplace/seed-data.ts— operators, cohorts, destinationssrc/lib/mock-server/signal-marketplace/server.ts— HTTP handlerstest/lib/mock-server/signal-marketplace.test.js— 8 smoke tests covering auth, operator scoping, pricing overrides, activation lifecycle, cross-operator isolationbin/adcp.js—mock-serversubcommand routing
Background and design rationale: #1155.
- Multi-operator API key pattern — single
-
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[]onStoryboardPhase. 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_synccarrying its own account ref viabrand+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_onfail loud at parse time. Empty list ([]) is legal.Companion spec issue: needs to be filed at
adcontextprotocol/adcpto add the field to the storyboard schema and audit each specialism storyboard for which phases are functionally independent (notablysales-socialwhereaudience_sync/creative_push/event_setup/event_logging/financialsare arguably independent ofaccount_setupfor 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'toDecisioningCapabilities.supportedBillingsThe
billing-partyschema 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
createAdcpServerFromPlatformis called withcomplyTestand a sales
platform that wiresgetProducts, but without explicitseed.productor
seed.pricing_optionadapters, the framework now auto-derives those adapters
from an in-memory store and wires atestControllerbridge so seeded products
appear inget_productsresponses on sandbox requests.This removes the footgun where LLM-generated platforms fail comply storyboards
because the slim skill guide doesn't mention thatseed_productrequires an
explicit adapter. Publishers wiringgetProductsnow get free comply-sandbox
seeding without writing any seed adapter code.The store is keyed by
account.account_idso 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) wirebridgeFromSessionStore
explicitly.Explicit
seed.product/seed.pricing_optionadapters and explicit
testControllerbridges always take priority — the auto-seed is only applied
when neither is present. -
14f011c: fix(server): default
account.supported_billingto['agent']increateAdcpServerFromPlatformWhen the v6 framework emits the
accountblock in `get_a...
@adcp/sdk@6.5.0
Minor Changes
-
e114e3a: Reverts #1142, removing
storyboardContext?: StoryboardContextfromAssertionContextand 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 throughctx.state(which is exactly howstatus.monotonic'shistoryworks). Issue #1140 was about YAML validators (thecheck:clause), not assertion handlers — that scope is fully covered by #1141 (field_less_than/field_equals_contextreading fromValidationContext.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 inonStart, mutated inonStep, read inonEnd).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
Patch Changes
-
4fada67:
executeTasknow returns a structuredTaskResultinstead of throwing for pre-flight errors (fixes #1148).Symptom:
agent.executeTask('list_authorized_properties', {})against a v2.5 MCP seller threwTypeError: 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 internalTaskExecutor.executeTaskalready wraps network-layer errors;SingleAgentClienthad no matching safety net for the steps it runs before delegating to the executor.list_authorized_propertiesis the common trigger because it has no named helper method onAgentClient(deprecated in favour ofget_adcp_capabilities) so all callers go throughexecuteTask. On a v2.5 MCP seller, the response shape is unexpected and a TypeError escapes during pre-flight processing.Fix: Wrap the full
SingleAgentClient.executeTaskbody in a try/catch. Structured protocol errors that carry typed fields callers use for recovery decisions are rethrown:AuthenticationRequiredError(and its subclassNeedsAuthorizationError),TaskTimeoutError,VersionUnsupportedError, andFeatureUnsupportedError. Unexpected errors (TypeErrors, schema-parser panics, etc.) are converted to{ success: false, status: 'failed', error: message }envelopes matching the declared return typePromise<TaskResult<T>>. The fluent.match()method works correctly on error envelopes viaattachMatch.Callers that followed the TypeScript return type and checked
result.success/result.statusare unaffected. Callers that relied onexecuteTaskthrowing 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_applicablecascade 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 showing1/9/0onsales_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_applicablestep 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, andext: nullto signal "nothing here" rather than omitting the key. After #1137 pinnedvalidateResponseSchemato the detected server version, Ajv correctly validated these responses against the v2.5 schema — but the v2.5 schemas declare those fields astype: 'array'ortype: 'object'without anullunion, so every such response failed with/errors: must be array; /context: must be object; /ext: must be object.The fix adds a
stripEnvelopeNullspre-processing step insidevalidateResponsethat strips top-level optional fields whose value isnullbut whose declared schema type is not nullable. Gated to v2.x schema bundles only — in v3,errorsis 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
Minor Changes
-
e76ab7d: Add
field_less_thanandfield_equals_contextcross-step comparison validators to the storyboard runner.These two new
StoryboardValidationCheckkinds let storyboard authors assert relationships between a current-step response field and a value captured from an earlier step viacontext_outputs. The runtime accumulator is the existingstoryboardContext(option 2 / context-outputs style), consistent with therefs_resolvevalidator 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 acontext_key_absentobservation 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. Requirescontext_key. Same skip-with-observation behavior when the key is absent.
Both validators require
path. Both addcontext_key?: stringtoStoryboardValidation(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?: StoryboardContexttoAssertionContext. The runner
now threads the accumulated context (all prior steps'context_outputsand
convention-extracted values) into every assertion's context object before each
onStepcall, using the Option 2 / context-outputs style (same key namespace
as$context.*placeholders andcontext_outputsentries).Assertion implementations can now read
ctx.storyboardContext?.['my_key']to
compare values from a prior step against the current step's result. Missing
keys returnundefined; 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_failedto steps independentlynot_applicableormissing_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 thanprerequisite_failed(passed: false). This makes the storyboard report honest for agents with reduced specialism surfaces:missing_toolmeans "this agent doesn't claim this surface, by design", whileprerequisite_failedmeans "this agent has a real setup bug affecting state that should have materialized." -
b8c0872: Use
DEFAULT_REPORTING_CAPABILITIESin decisioning-platform worked examples and SKILL.md quickstart. Updatesbroadcast-tv,mock-seller, andprogrammaticexamples to import and reference the exported constant rather than hand-rollingreporting_capabilitiesinline. Adds the constant to thebuild-decisioning-platformimports cheat sheet andgetProductsproduct 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 todocs/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 toNODE_ENV∈ {test, development} unlessADCP_NOOP_JWKS_ACK=1). Production promotion path covered (publish JWK to brand.json, swap in-memory key for KMS-backedSigningProvider). Plus the omit-key path for adopters who aren't ready to sign yet.No behavior change.
-
3a9b7fe: Fix
adaptSyncCreativesRequestForV2to pass the role-keyedassetsmanifest 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.5creative-asset.jsonschema declaresassetsusingpatternPropertieskeyed by role string — the same manifest shape v3 uses — so the flat output failed v2.5 schema validation on every field. The adapter now passesassetsthrough verbatim, and thesync_creativesconformance fixture inadapter-v2-5-conformance.test.jshas been updated from anexpected_failurespin 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
pollTaskCompletionnow accept a per-calltransport?override that beats the constructor-level cap.
SubmittedContinuation.trackexposes the per-call override;waitForCompletioninherits 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.declaredContentLengthrenamed tocontentLengthHeader(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.callTool→connectMCPWithFallbackImpl→wrapFetchWithSizeLimitfor the
non-OAuth MCP path.
@adcp/sdk@6.3.0
Minor Changes
-
89af100: Make
TenantConfig.signingKeyoptional + auto-wire it into webhook signing.The SDK was stricter than the AdCP 3.x spec:
signed-requestsis a preview specialism and CLAUDE.md § Protocol-Wide Requirements explicitly classifies RFC 9421 HTTP Signatures as "optional but recommended." Adopters were forced to fabricate aTenantSigningKey(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 onTenantConfig.signingKeyfor JWKS validation, once onserverOptions.webhooks.signerKeyfor outbound signatures).This change does two things:
1.
signingKeyis now optional. When omitted,runValidationskips the JWKS roundtrip entirely and the tenant transitions straight frompendingtohealthywithreason: '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
signingKeyIS set, the registry auto-wires it into outbound webhook signing. The privateJwk now flows intoserverOptions.webhooks.signerKeyautomatically. Set the key once onTenantConfig, get JWKS validation + signed webhooks. Strict onadcp_use: the JWK MUST carryadcp_use: "webhook-signing"per AdCP key-purpose discriminator (adcp#2423). Adopters who wire their own webhook signer onserverOptions.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 viajoseand returns aTenantSigningKeyalready tagged withadcp_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 outsideNODE_ENV∈ {'test','development'} unless the operator setsADCP_NOOP_JWKS_ACK=1. Mirrors theidempotency: 'disabled'allowlist gate —NODE_ENVdefaults 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
signingKeyneed to addadcp_use: "webhook-signing"to bothpublicJwkandprivateJwk. 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 onserverOptions.webhooksto bypass the auto-wire.Migration note added to
docs/migration-5.x-to-6.x.md§ Common gotchas. New describe blocks intest/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 ofResolveContextcarrying the resolvedAccount<TCtxMeta>.getAccountFinancialsnow receives(req, ctx: AccountToolContext<TCtxMeta>)so adopters fronting an upstream platform can read tokens / upstream IDs fromctx.account.ctx_metadatawithout re-resolving. Resolves the 7-adapter pain point wheregetAccountFinancialswas stubbed toUNSUPPORTED_FEATUREsolely because the v6 surface didn't thread the resolved account through. Breaking change for v6.x adopters who already implementedgetAccountFinancials— update the second arg type toAccountToolContext<TCtxMeta>and readctx.account.ctx_metadatadirectly. Resolve-step null surfacesACCOUNT_NOT_FOUND(terminal) before the platform method runs.AccountStore —
refreshTokenhook (#1145 Gap 2). OptionalrefreshToken(account, reason: 'auth_required'): Promise<{ token; expiresAt? }>. When defined and a platform method throwsAdcpError({ code: 'AUTH_REQUIRED' }), the framework refreshes via this hook, mutatesaccount.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 correctableAUTH_REQUIREDso the buyer re-links via their UI flow. Resolves UniversalAds' v5-OAuth-provider workaround. Wired intogetAccountFinancialsdispatch; 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 declaringpeer_substitutes_for: <step_id> | <step_id>[]on the substitute step. When a stateful step skips withmissing_tool/missing_test_controllerAND 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 baremissing_toolcascade 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_applicableany-peer rescue is unchanged for backward compat. Without apeer_substitutes_fordeclaration, missing reasons keep tripping the cascade immediately.The companion spec change at
adcontextprotocol/adcp(storyboard schema field +sales-social/index.yamledit) is required before any storyboard exercises the new path. -
e0b08d2: Adds
'silent'toTrackStatusso 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 reportpassed: truewhether they validated three transitions or none. That collapses two different states behind one icon: real protection vs. wired-but-not-exercised. Tracks likeproperty-lists,collection-lists, andcontent-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:
TrackStatuswidens to'pass' | 'fail' | 'skip' | 'partial' | 'silent'. A track is silent when every observation-bearing assertion record reportsobservation_count: 0and nothing failed. Skip/fail/partial precedence is preserved — silent only triggers on otherwise-clean runs.AssertionResult.observation_count?: numbercarries the run-level count from observation-based invariants.status.monotonicnow defines anonEndhook that emits a single record withobservation_count: history.size, giving the rollup a deterministic signal whether to demote.ComplianceSummary.tracks_silentand an updatedformatComplianceResultsrender silent rows distinctly (🔇, "no lifecycle observed") instead of the green check.
computeOverallStatustreats silent tracks asattempted(they ran) but never as unambiguouslypassing— a run with any silent track surfaces aspartial.computeOverallStatustolerates summaries serialized before this release (registry cache, fixtures) by defaultingtracks_silentto0when absent.Why widen the union instead of adding
observable: booleanonAssertionResult(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.