v5.12.0
Minor Changes
-
054d37a: Expose
wrapEnvelopefrom@adcp/client/server— a public helper for attaching AdCP envelope fields (replayed,context,operation_id) to handler responses, with error-code-specific field allowlists (e.g., IDEMPOTENCY_CONFLICT dropsreplayed). Promoted for sellers that wire their own MCP / A2A handlers without the framework.Parity with the framework's internal
injectContextIntoResponse:opts.contextis NOT attached when the inner payload already carries acontextthe handler placed itself (handler wins). The per-error-code allowlist now listscontextexplicitly rather than short-circuiting — a module-load invariant asserts every allowlist entry includescontextso future error codes can't silently drop correlation echo. Return type widened to surface the envelope fields (replayed?,context?,operation_id?) for caller autocomplete. -
8d86be7: Add
runAgainstLocalAgentto@adcp/client/testing— a one-call compliance harness that composescreateAdcpServer+serve+seedComplianceFixtures+ the webhook receiver + the storyboard runner. Sellers iterating on their handlers no longer need to hand-roll the 300-line bootstrap (ephemeral port, fixtures, webhook receiver, loop, teardown) fromadcp'sserver/tests/manual/run-storyboards.ts.Programmatic surface.
@adcp/client/testingnow exportsrunAgainstLocalAgent({ createAgent, storyboards, fixtures?, webhookReceiver?, authorizationServer?, runStoryboardOptions?, onListening?, onStoryboardComplete?, bail? }). The caller'screateAgentmust close over a stablestateStoreso seeds persist across the factory callsserve()makes per request.storyboardsaccepts'all'(every storyboard in the cache),AgentCapabilities(the same resolution the live assessment runner does),string[](storyboard or bundle ids), orStoryboard[].CLI surface.
adcp storyboard run --local-agent <module> [id|bundle]is a thin wrapper over the programmatic helper. The module must exportcreateAgentas default or named.--format junitemits a JUnit XML report on stdout for single-storyboard and--local-agentruns — each storyboard becomes a<testsuite>, each step a<testcase>.Test authorization server.
@adcp/client/compliance-fixturesnow exportscreateTestAuthorizationServer({ subjects?, issuer?, algorithm? })— an in-process OAuth 2.0 AS that serves RFC 8414 metadata, JWKS, and a client-credentials token endpoint. Pairs withrunAgainstLocalAgent({ authorizationServer: true })to gradesecurity_baseline,signed-requests, and other auth-requiring storyboards locally without reaching an external IdP. RS256 by default (ES256 available); HS* is refused to matchverifyBearer's asymmetric-only allowlist.New guide.
docs/guides/VALIDATE-LOCALLY.mdwalks the ten-line pattern, the stable-stateStore rule, the CLI equivalent, and the auth-server integration.Closes adcp-client#786.
-
39e661f: Add seed fixture merge helpers and a
get_productstest-controller bridge so Group A compliance storyboards can seed fixtures end-to-end without seller boilerplate.Seed merge helpers (
@adcp/client/testing):- Generic
mergeSeed<T>(base, seed)— permissive merge:undefined/nullin seed preserves base; every other leaf (including0,false,"",[]) overrides. Arrays replace by default;Map/Setthrow. - Typed per-kind wrappers (
mergeSeedProduct,mergeSeedPricingOption,mergeSeedCreative,mergeSeedPlan,mergeSeedMediaBuy) layer by-id overlay on well-known id-keyed arrays so seeding a single entry doesn't drop the rest:pricing_options[]bypricing_option_id,publisher_properties[]by(publisher_domain, selection_type),packages[]bypackage_id, creativeassets[]byasset_id, planfindings[]bypolicy_id, planchecks[]bycheck_id. - Shared
overlayById(base, seed, identity)helper so sellers can apply the same overlay rule to domain-specific fields.
get_productsbridge (@adcp/client):createAdcpServer({ testController: { getSeededProducts } })— seeded products append to handler output on sandbox requests (account.sandbox === true,context.sandbox === true, and — whenresolveAccountreturns an account —ctx.account.sandbox === true). Production traffic or a resolved non-sandbox account skips the bridge entirely.product_idcollisions resolve with the seeded entry winning. Returns that are non-arrays or entries missingproduct_idare logged and dropped rather than thrown. Handler-declaredsandbox: falsestays authoritative (the bridge does not overwrite it).bridgeFromTestControllerStore(store, productDefaults)— one-liner that wraps anyMap<string, unknown>seed store into aTestControllerBridge; each stored fixture is merged ontoproductDefaultsviamergeSeedProduct.- Opt-in via presence of
getSeededProducts; the previousaugmentGetProductsflag is dropped (one-rule opt-in).
- Generic
Patch Changes
-
f86afe4: Storyboard runner: honor
step.sample_requestin
list_creative_formatsrequest builder.Prior behavior hardcoded
list_creative_formats() { return {}; }, so
any storyboard step declaringformat_ids: ["..."](or any other
query param) in its sample_request hit the wire as an empty request.
The agent returned unfiltered results and downstream round-trip /
substitution-observer assertions failed silently (the agent looked
non-conformant, but the filter had never been sent).Mirrors the pattern used by peer builders (
build_creative,
sync_creatives, etc.). No other API change.Closes #780.
-
b8b7fb2: Storyboard runner: fix spec-violating shapes and
sample_request
precedence across the SI + governance request builders. All affected
builders now honorstep.sample_requestfirst (matching peer builders),
and their synthetic fallbacks conform to the generated Zod schemas so
framework-dispatch agents running strict validation at the MCP boundary
no longer reject them with-32602 invalid_type.si_get_offering: drop the stringcontextand the out-of-schema
identity; emit the prose string as optionalintent(per
si-get-offering-request.json,contextis a ref to an object).si_initiate_session: move prose fromcontext(which must be an
object) to requiredintent; default the identity fallback to the
realistic anonymous handoff shape (consent_granted: false+
anonymous_session_id) instead ofconsent_granted: truewith an
empty consented user — spec-legal either way, but the anonymous shape
is what a host that hasn't obtained PII consent actually sends.si_send_message/si_terminate_session: honorsample_requestso
storyboards can driveaction_response,handoff_transaction,
termination_context, and non-defaultreasonpaths without the
fallback stomping the scenario.sync_governance: lengthen defaultauthentication.credentialsto
meetminLength: 32, and honorsample_requestso fixtures like
signal-marketplace/scenarios/governance_denied.yamlthat author
url: $context.governance_agent_urlflow through.
Closes #802.
-
8d58987: Fix unbounded re-execution when a buyer SDK retries a mutating request against a handler whose response fails strict-mode validation (issue #758).
Under the strict response-validation default, a drifted handler produced a
VALIDATION_ERRORand released its idempotency claim on the way out, so the next retry re-entered the handler with the same drift — looping as fast as the buyer's retry budget allowed. The dispatcher now caches theVALIDATION_ERRORenvelope under the same(principal, key, payloadHash)tuple for 10 seconds; retries on the same key short-circuit to the cached error instead of re-running side effects, and the cache clears itself before a handler fix would be gated on TTL expiry.A retry with a different canonical payload still produces
IDEMPOTENCY_CONFLICT(the cache scopes on payload hash, same as the success cache), and a buyer that generates a fresh idempotency key per retry is not short-circuited — both behaviors are intentional. Same-key retry storms are the dominant failure mode; fresh-key loops already have the buyer's backoff as the correct control point.New
IdempotencyStore.saveTransientError(...)method is optional on the interface — custom store implementations that want retry-storm protection can implement it; omitting it preserves the prior release-on-error behavior. Stores built viacreateIdempotencyStorepick it up automatically.Operational note. A drifted handler reachable by a hostile buyer is a cache-fill vector (every fresh key writes a 10s entry). Alert on sustained
VALIDATION_ERRORrates per principal — steady-state should be zero. -
c6bced1: Testing: schema-driven round-trip invariant for every storyboard request builder, plus fallback fixes so each builder's fallback round-trips through the generated Zod schema.
Adds
test/lib/request-builder-schema-roundtrip.test.jsthat iterates every task inTOOL_REQUEST_SCHEMAS(pluscreative_approvalandupdate_rights) and asserts the fallback request — empty context, emptysample_request, syntheticidempotency_keywhere required — parses cleanly against the matching schema fromsrc/lib/types/schemas.generated.ts. New builders are picked up automatically.Running the invariant surfaced eight pre-existing fallbacks that had drifted out of spec. Fixed:
update_media_buypackages fallback now setspackage_id.update_rights/creative_approvalfallbacks userights_id(the spec field) instead ofrights_grant_id;creative_approvalnow emitscreative_url+creative_id.sync_creativesfallback assets carry the requiredasset_typediscriminator (image/video/text).buildAssetsForFormatuses spec-correct video fields (duration_ms,container_format,width,height).calibrate_content/validate_content_deliveryartifacts useassets: [](the schema is an array of typed assets, not an object map).activate_signaldefaultsdestinationsto a placeholder agent entry so the fallback path satisfies the schema's required array.create_content_standards/update_content_standardsfallbacks align with the currentscope+policiesshape (old schema usedname+rules).si_get_offering/si_initiate_sessionpassoptions.si_contextthrough the schema'sintent(string) field instead of the wire-levelcontextslot that the spec types asContextObject;si_initiate_sessionnow emits the requiredintent.
Closes #803.
-
5e52efa: Fix storyboard
REQUEST_BUILDERSforlog_eventandcreate_media_buyso they emit spec-conformant payloads and honor hand-authoredstep.sample_request— framework-dispatch agents running zod at the MCP boundary previously rejected these with-32602 invalid_type(#793).log_eventnow honorsstep.sample_requestwhen present (same convention assync_catalogs,update_media_buy,report_usage). The synthetic fallback emitsevent_time(wastimestamp) and placesvalue+currencyundercustom_data(was nestedvalue: { amount, currency }). Unblockssales_catalog_drivenandsales_socialstoryboards whose authored events carriedevent_time,content_ids, and spec-shaped siblings that the builder was discarding.create_media_buynow emits every authored package instead of droppingpackages[1+]. The first package still receives context-derivedproduct_id/pricing_option_idoverrides (so single-package storyboards against arbitrary sellers keep working); additional packages pass through with context injection only, preserving per-packageproduct_id,bid_price,pricing_option_id, andcreative_assignments. Unblocks multi-package storyboards (e.g.sales_non_guaranteed) wherecontext_outputscapturedpackages[1].package_idassecond_package_id— the next step was being skipped with "unresolved context variables from prior steps".
Surfaced while diagnosing adcontextprotocol/adcp#2872.