Releases: adcontextprotocol/adcp-client
v5.13.0
Minor Changes
-
be0d60b: Envelope hygiene: colocate the two error-envelope allowlists into a
single source of truth, and flipwrapEnvelopeto a fail-closed default
for unregistered error codes.Security-review follow-ups from #788 (M3 + M4):
- #800 (M4):
ERROR_ENVELOPE_FIELD_ALLOWLIST(sibling-keys allowlist
used bywrapEnvelope) and the formerCONFLICT_ALLOWED_ENVELOPE_KEYS
(inside-adcp_error allowlist used by the
idempotency.conflict_no_payload_leakinvariant) now live side-by-side
in the newsrc/lib/server/envelope-allowlist.tsmodule. The latter
is renamed toCONFLICT_ADCP_ERROR_ALLOWLISTto make the "keys inside
the adcp_error block" scope obvious. Both are exported from
@adcp/client/serverso callers with custom error envelopes can
inspect / extend the sets. - #799 (M3):
wrapEnvelopenow fails closed on unregistered error
codes. A code with no explicit entry inERROR_ENVELOPE_FIELD_ALLOWLIST
usesDEFAULT_ERROR_ENVELOPE_FIELDS—contextonly — instead of
inheriting success-envelope semantics. Sellers that wantreplayed
oroperation_idon a bespoke error code must register it explicitly.
The fail-closed posture matches the framework's own internal behavior:
create-adcp-server.tserror paths only ever echocontextvia
finalize();injectReplayedis never called on error responses.
Who is affected: consumers calling
wrapEnvelopewith an
adcp_error.codeother thanIDEMPOTENCY_CONFLICT(the only code
registered today) AND relying onreplayedoroperation_idto
round-trip. On upgrade, those fields silently drop — onlycontext
echoes.IDEMPOTENCY_CONFLICTis unchanged.Upgrade path: for bespoke error codes that genuinely need
replayedoroperation_idon the envelope, build the envelope
directly instead of callingwrapEnvelope, or open an issue so the
code can be added toERROR_ENVELOPE_FIELD_ALLOWLIST. The allowlist
is intentionally frozen at the module level — extending it requires a
spec-and-SDK conversation, not a local override.Breaking change (minor —
wrapEnvelopewas just shipped in 5.11.0):
narrow external surface, days-old on npm. - #800 (M4):
-
e5ef1be: Pin to AdCP 3.0.0 GA.
ADCP_VERSIONflips from the rollinglatestalias to the published
3.0.0release. Generated types, Zod schemas, compliance storyboards,
andschemas-data/are now locked to the 3.0.0 registry instead of
tracking whatever the registry serves next.COMPATIBLE_ADCP_VERSIONS
adds'3.0.0'alongside the existingv3alias and the beta.1 /
beta.3 wire-compat entries so mixed-version traffic keeps working.Supply-chain: the 3.0.0 tarball is cosign-verified against
adcontextprotocol/adcp's release workflow OIDC identity, which is a
stricter trust boundary than the checksum-onlylatestalias used
before.Side effects of the pin:
validate_property_deliveryresponse now uses its generated
ValidatePropertyDeliveryResponseSchema(upstream shipped the
registry entry in 3.0.0 GA). The schema requireslist_id,
summary,results, andvalidated_at;compliantis optional.
The previous hand-written stub accepted a bare{compliant}OR a
bare{errors}fallback; the{errors}branch is gone — error
responses now flow through the protocol's async error channel
rather than the response body. Callers readingcompliantstill
work; callers that consumed.errorsfrom the response must switch
to the standardTaskResult.adcpErrorpath.compliance/cache/3.0.0/is populated (cosign-verified) and
replacescompliance/cache/latest/as the storyboard source.
Patch Changes
-
22b44c4: Fix
governance.denial_blocks_mutationto allow expected-denial recovery
paths.The invariant anchored any governance denial (
GOVERNANCE_DENIED,
TERMS_REJECTED,POLICY_VIOLATION, etc.) and then flagged any later
successful mutation in the same run as a silent bypass. That fired on
first-party storyboards whose whole purpose is to test recovery —
media_buy_seller/governance_denied_recovery(buyer shrinks the buy
and retries) andmedia_buy_seller/measurement_terms_rejected(buyer
relaxes terms and retries) — because the retry step succeeded against
the same plan and tripped the anchor.A denial step that the storyboard marks
expect_error: trueis the
author explicitly acknowledging the denial. The subsequent mutation is
a recovery path, not a silent bypass, so the invariant no longer
anchors when the denial step is expected. The silent-bypass signal is
preserved forcheck_governance200s withstatus: deniedand for
adcp_errorresponses the author did not declare expected.When the invariant does fire on a wire-error denial, the failure
message now points the author at theexpect_error: trueescape so
the next author doesn't have to re-derive it from source. The hint is
suppressed oncheck_governance200 denials where the flag has no
effect.Closes #811.
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+ `creat...
v5.11.0
Minor Changes
-
740f609: Add typed factory helpers for creative asset construction that inject the
asset_typediscriminator:imageAsset,videoAsset,audioAsset,textAsset,urlAsset,htmlAsset,javascriptAsset,cssAsset,markdownAsset,webhookAsset, plus a groupedAssetnamespace (Asset.image({...})) over the same functions.Each helper takes the asset shape without
asset_typeand returns an object tagged with the canonical literal —imageAsset({ url, width, height })produces{ url, width, height, asset_type: 'image' }— eliminating the boilerplate at every construction site. The discriminator is written last in the returned object so a runtime bypass (cast that slipsasset_typeinto the input) cannot overwrite it.Return type is
Omit<T, 'asset_type'> & { asset_type: '<literal>' }(intersection) rather than the raw generated interface, so the builders compile regardless of whether the generated TypeScript types currently carry the discriminator — a defensive choice that makes the helpers stable across schema regenerations. -
828c112: Add
createDefaultTestControllerStoreto@adcp/client/testing— a default factory that wires everyforce_*,simulate_*,seed_*scenario against a genericDefaultSessionShape. Sellers provideloadSession/saveSessionand get a conformance-readyTestControllerStorewithout hand-rolling 300+ lines of boilerplate. Supports partial overrides for sellers who need to customize specific handlers. -
dd04ae9: Add
@adcp/client/express-mcpmiddleware that rewrites JSON-onlyAcceptheaders so they pass the MCP SDK'sStreamableHTTPServerTransportcheck whenenableJsonResponse: true. Local escape hatch pending upstream SDK fix (modelcontextprotocol/typescript-sdk#1944). -
4dc4743: Storyboard cross-step invariants are now default-on. Bundled assertions (
status.monotonic,idempotency.conflict_no_payload_leak,context.no_secret_echo,governance.denial_blocks_mutation) apply to every run unless a storyboard opts out — forks and new specialisms no longer ship with zero cross-step gating silently.Storyboard.invariantsnow accepts an object form{ disable?: string[]; enable?: string[] }.disableis the escape hatch that removes a specific default;enableadds a consumer-registered (non-default) assertion on top of the baseline. The legacyinvariants: [id, ...]array form still works and is treated as additive on top of the defaults.- Behavior change for direct-API callers:
resolveAssertions(['id'])now returns[...defaults, ...named]instead of exactly the named ids. Callers that relied on the array-only return shape (e.g., snapshottingresolveAssertions([...]).length) should switch toresolveAssertions({ enable: [...], disable: listDefaultAssertions() })to reproduce the old semantics. AssertionSpecgained an optionaldefault?: booleanflag. Consumers registering custom assertions viaregisterAssertion(...)can opt their own specs into the default-on path.resolveAssertions(...)fails fast on unknown ids inenable/ the legacy array, and ondisableids that aren't registered as defaults (typo guard — a silent no-op would mask coverage gaps). Errors name the registered set and emit aDid you mean "..."?suggestion when one of the unknown ids is within Levenshtein distance 2 of a known id.- Unknown top-level keys on the object form (e.g.
invariants: { disabled: [...] }— trailingdtypo) throw instead of silently normalising to an empty disable set. - New export
listDefaultAssertions()(re-exported from@adcp/client/testing) enumerates the default-on set for tooling / diagnostics.
status.monotonicfailure messages now include the legal next states from the anchor status and a link to the canonical enum schema, e.g.
media_buy mb-1: active → pending_creatives (step "create" → step "regress") is not in the lifecycle graph. Legal next states from "active": "canceled", "completed", "paused". See https://adcontextprotocol.org/schemas/latest/enums/media-buy-status.json for the canonical lifecycle.
Terminal states render as(none — terminal state)so the message is unambiguous. -
6a2c2c5: Add typed factory helpers for
preview_creativerender objects:urlRender,htmlRender,bothRender, plus a groupedRendernamespace. Each helper takes the render payload withoutoutput_formatand returns an object tagged with the canonical discriminator —urlRender({ render_id, preview_url, role })produces a valid url-variant render without repeatingoutput_format: 'url'at every call site.Mirrors the
imageAsset/videoAssetpattern shipped in #771.PreviewRenderis a oneOf onoutput_format(url/html/both) where the discriminator decides which sibling field becomes required. Matrix runs consistently surfaced renders missing eitheroutput_formator its required sibling — the helpers make the wrong shape syntactically harder to express because the input type requires the matchingpreview_url/preview_htmlper variant.Return type uses
Omit<Variant, 'output_format'> & { output_format: <literal> }so the builders stay robust across schema regenerations. Discriminator is spread last so a runtime cast cannot overwrite the canonical tag.Skill pitfall callouts in
build-creative-agentandbuild-generative-seller-agentnow recommend the render helpers alongside the asset helpers. -
9d583aa: Extend the bundled
status.monotonicdefault assertion to track the audience lifecycle alongside the seven resource types it already guards (adcontextprotocol/adcp#2836).sync_audiencesresponses carry per-audiencestatusvalues (processing | ready | too_small) drawn from the newly-named spec enum at/schemas/enums/audience-status.json, and the assertion now rejects off-graph transitions across storyboard steps for every observedaudience_id.Transition graph — fully bidirectional across the three states, matching the spec's permissive "MAY transition" hedging:
processing → ready | too_smallon matching completion.ready ↔ processingon re-sync (new members → re-match).too_small → processing | readyon re-sync (more members → re-match, directly back to ready when the re-matched count clears the minimum).ready ↔ too_smallas counts crossminimum_sizeacross re-syncs.
Observations are drawn from
sync_audiencesresponses only — discovery-only calls (request omits theaudiences[]array) still returnaudiences[], so the extractor covers both write and read paths under the single task name. No separatelist_audiencestask exists in the spec. Actionsdeletedandfailedomitstatusentirely on the response envelope; the extractor's id+status guard makes those rows silent (nothing to observe, nothing to check).Resource scoping is
(audience, audience_id), independent from the other tracked resources. Unknown enum values drift-reset the anchor rather than failing —response_schemaremains the gate for enum conformance.8 new unit tests cover the forward flow, the too_small → processing → ready re-sync path, bidirectional
ready ↔ too_small,ready → processingon re-sync, self-edge silent pass, deleted/failed silent pass, per-audience-id scoping, and enum-drift tolerance. The assertion description now enumeratesaudiencealongside the other resource types.Follow-up: wiring
audience-sync/index.yamlwithinvariants: [status.monotonic]in the adcp spec repo once this release lands. -
eca55c5: Storyboard runner auto-fires
comply_test_controllerseed scenarios from thefixtures:block (adcp-client#778).When a storyboard declares
prerequisites.controller_seeding: trueand carries a top-levelfixtures:block, the runner now issues acomply_test_controllercall per fixture entry before phase 1:fixtures.products[]→seed_productfixtures.pricing_options[]→seed_pricing_optionfixtures.creatives[]→seed_creativefixtures.plans[]→seed_planfixtures.media_buys[]→seed_media_buy
Each entry's id field(s) ride on
params; every other field is forwarded verbatim asparams.fixture. The seed pass surfaces as a synthetic__controller_seeding__phase inStoryboardResult.phases[]so compliance reports distinguish pre-flight setup from per-step buyer behavior.Grading semantics:
- Seed failure cascade-skips remaining phases with detailed
skip_reason: 'controller_seeding_failed'and canonicalskip.reason: 'prerequisite_failed'— respects the runner-output-contract's six canonical skip reasons (controller_seeding_failedis a newRunnerDetailedSkipReason, not a new canonical value). - Agent not advertising
comply_test_controller→ cascade-skips with canonicalskip.reason: 'missing_test_controller', implementing the spec'sfixture_seed_unsupportednot_applicable grade. No wire calls are issued. - Multi-pass mode seeds exactly once at the run level (inside
runMultiPass) instead of N times inside each pass — avoids inflatingfailed_count/skipped_countby N when a fixture breaks.
Closes the spec-side/seller-side gap. The
fixtures:block (adcontextprotocol/adcp#2585, rolled out in adcontextprotocol/adcp#2743) and theseed_*scenarios (adcontextprotocol/adcp#2584, implemented here asSEED_SCENARIOS+createSeedFixtureCache) shipped without runner glue. Storyboards likesales_non_guaranteed,creative_ad_server,governance_delivery_monitor,media_buy_governance_escalation, andgovernance_spend_authoritygo from red to green against sellers that implement the matchingseed*adapters.New
StoryboardRunOptions.skip_controller_seeding. Opt out of the pre-flight for agents that load fixt...
v5.10.0
Minor Changes
-
8f9260b: feat(server): request validation defaults to
'warn'outside productioncreateAdcpServer({ validation: { requests } })previously defaulted to
'off'everywhere. It now defaults to'warn'when
NODE_ENV !== 'production', mirroring the asymmetric default already in
place forresponses('strict'in dev/test,'off'in production).Production behaviour is unchanged: the default stays
'off'when
NODE_ENV === 'production', so prod request paths pay no AJV cost.What operators will see: in dev/test/CI, each incoming request that
doesn't match the bundled AdCP request schema logs a single
Schema validation warning (request)line through the configured
logger, with the tool name and the field pointer. Nothing is rejected —
the request still flows to the handler exactly as before. Node's test
runner does not setNODE_ENV, so suites running undernode --test
fall into the dev/test bucket and will start emitting these warnings.How to opt out: pass
validation: { requests: 'off' }on the server
config, or setNODE_ENV=productionfor the process.Why: keeps request and response defaults symmetric, and prepares seller
operators for upstream AdCP schema tightenings (e.g. adcp#2795, which
introduces a requiredasset_typediscriminator — buyer agents still
on RC3 fixtures will lack it). Surfacing those drifts as warnings
during development beats discovering them in a downstream consumer's
VALIDATION_ERRORafter deploy.Related: #694 (original intent for
requests: 'warn') and #727 A
(response-side default precedent). -
86a0fde: Register the fourth default cross-step assertion
status.monotonic(adcontextprotocol/adcp#2664). Resource statuses observed across storyboard steps MUST transition only along edges in the spec-published lifecycle graph for their resource type. Catches regressions likeactive → pending_creativeson a media_buy, orapproved → processingon a creative asset, that per-step validations cannot detect.Tracked lifecycles (one transition table per resource type, hardcoded against the enum schemas in
static/schemas/source/enums/*-status.jsonin the spec repo, with bidirectional edges listed explicitly):media_buy— forward flowpending_creatives → pending_start → active,active ↔ pausedreversible, terminalscompleted | rejected | canceled.creative(asset lifecycle) — forward flowprocessing → pending_review → approved | rejected,approved ↔ archivedreversible,rejected → processing | pending_reviewallowed on re-sync, no terminals.creative_approval— per-assignment on a package, forwardpending_review → approved | rejected,rejected → pending_reviewallowed on re-sync.account—active ↔ suspendedandactive ↔ payment_requiredreversible, terminalsrejected | closed.si_session— forwardactive → pending_handoff → complete | terminated, terminalscomplete | terminated.catalog_item— forwardpending → approved | rejected | warning,approved ↔ warningreversible,rejected → pendingallowed on re-sync.proposal— one-waydraft → committed.
Observations are drawn from task-aware extractors on
stepResult.response:create_media_buy/update_media_buy/get_media_buys,sync_creatives/list_creatives, nested.packages[].creative_approvals[],sync_accounts/list_accounts,si_initiate_session/si_send_message/si_terminate_session,sync_catalogs/list_catalogs(per-item),get_products(when the response carries aproposal). Unknown tasks produce no observations.State is scoped
(resource_type, resource_id)so independent resources don't interfere. Self-edges (same status re-read) are silent pass. Skipped / errored /expect_error: truesteps don't record observations. Unknown enum values (drift) reset the anchor without failing —response_schemacatches enum violations.Failure output names the resource, the illegal transition, and the two step ids:
media_buy mb-1: active → pending_creatives (step "create" → step "regress") is not in the lifecycle graph.Consumers who need a stricter variant canregisterAssertion(spec, { override: true }).18 new unit tests cover forward flows, terminal enforcement, bidirectional edges, skip semantics, (resource_type, resource_id) scoping, nested creative_approval arrays, adcp_error-gated observations, enum-drift tolerance.
-
573a176: Improve OAuth ergonomics for
adcp storyboard run.- Fix classification: capability-discovery failures whose error message says
"requires OAuth authorization"(the wordingNeedsAuthorizationErroremits) now classify asauth_requiredwith theSave credentials: adcp --save-auth <alias> <url> --oauthremediation hint, instead of falling through tooverall_status: 'unreachable'with no actionable advice. The keyword list indetectAuthRejectionnow matches"authorization"and"oauth"in addition to401/unauthorized/authentication/jws/jwt/signature verification. - Surface the hint earlier: the OAuth remediation observation now fires whenever the error text looks OAuth-shaped, not only when
discoverOAuthMetadatasuccessfully walks the well-known chain — an agent that 401s before its OAuth metadata is resolvable still gets a useful hint. - Inline OAuth flow:
adcp storyboard run <alias> --oauthnow opens the browser to complete PKCE when the saved alias has no valid tokens, then proceeds with the run. Matches the existingadcp <alias> get_adcp_capabilities --oauthbehavior so the two-step dance (--save-auth --oauththenstoryboard run) is no longer required. Raw URLs still need--save-authfirst; MCP only.
Docs:
docs/CLI.mdanddocs/guides/VALIDATE-YOUR-AGENT.mddocument both flows and add a troubleshooting row for theAgent requires OAuthfailure. - Fix classification: capability-discovery failures whose error message says
-
86ccc99: feat(server): response validation defaults to
'strict'outside productioncreateAdcpServer({ validation: { responses } })previously defaulted to
'warn'whenNODE_ENV !== 'production'. It now defaults to'strict'
in dev/test/CI so handler-returned schema drift fails with
VALIDATION_ERROR(with the offending field path indetails.issues)
instead of logging a warning the caller can silently ignore.Production behaviour is unchanged: the default stays
'off'when
NODE_ENV === 'production', so prod request paths pay no validation
cost. Passvalidation: { responses: 'warn' }to restore the previous
dev-mode behaviour;validation: { responses: 'off' }opts out
entirely.Why: the
compliance:skill-matrixharness has repeatedly surfaced
SERVICE_UNAVAILABLEfrom agents whose responses fail the wire schema.
The dispatcher's response validator catches this drift with a clear
field pointer, one layer that every tool inherits automatically. Making
that the default catches it during handler development rather than in a
downstream consumer.Migration: handler tests that use sparse fixtures (e.g.
{ products: [{ product_id: 'p1' }] }) will start returning
VALIDATION_ERROR. Either fill in the missing required fields to match
the AdCP schema, or setvalidation: { responses: 'off' }on the test
server to keep the fixture intentionally minimal. Note that Node's
test runner does not setNODE_ENV, so test suites running under
node --test(withNODE_ENV=undefined) fall into the dev/test
bucket and will start validating responses — this is intentional.Also: the
VALIDATION_ERRORenvelope'sdetails.issues[].schemaPath
is now gated behindexposeErrorDetails(same policy as the existing
SERVICE_UNAVAILABLE.details.reasonfield). Production responses no
longer leak#/oneOf/<n>/properties/...paths that fingerprint the
handler's internaloneOfbranch selection — buyers still get
pointer,message, andkeyword, which is sufficient to fix a
drifted payload.Closes #727 (A).
-
f64007c: fix(server): tighten handler return types so schema drift fails
tscTwo related tightenings close #727 (B).
1.
AdcpToolMapbrand rights results.acquire_rights,
get_rights, andget_brand_identityhadresult: Record<string, unknown>— a stale scaffold from before the response types were
code-generated. Replaced with the proper generated types:-
acquire_rights→AcquireRightsAcquired | AcquireRightsPendingApproval | AcquireRightsRejected -
get_rights→GetRightsSuccess -
get_brand_identity→GetBrandIdentitySuccess2.
DomainHandlerreturn type. The handler return union
previously included| Record<string, unknown>as a general escape
hatch, so any handler could return any shape. Sparse returns like
{ rights_id, status: 'acquired' }passedtscand only failed at
wire-level validation. Handler return type is now just
AdcpToolMap[K]['result'] | McpToolResponse, so drift fails at
compile time.adcpError(...)still works — it returns
McpToolResponse.Migration. If a handler returns a plain object literal without
spelling out the full success shape,tscwill now flag the drift
with an error like:Type '{ products: [{ product_id: 'p1' }] }' is not assignable to type 'McpToolResponse | GetProductsResponse'. Property 'reporting_capabilities' is missing in type '{ product_id: 'p1' }' but required in type 'Product'.Two ways to fix:
-
Fill in the missing required fields to match the AdCP schema (what
the wire-level validator would have demanded anyway). Use
`DEFAULT_REPORTING...
-
v5.9.1
Patch Changes
-
b1497f9: ci: consolidate pipeline and drop redundant jobs
CI-only change, no runtime/library behaviour affected. Published package contents are unchanged.
ci.yml: collapsetest/quality/securityinto a single job. Each was re-runningcheckout + setup-node + npm ci, wasting ~1–2 min of setup per PR. Also removes theclean && build:libre-build in the old quality job and the redundantbuildstep (alias ofbuild:lib).ci.yml: droppublish-dry-run.release.yml'sprepublishOnlyalready validates packaging on the actual release PR.ci.yml: drop deaddevelopbranch from the push trigger.schema-sync.yml: drop the PR-triggeredvalidate-schemasjob —ci.ymlalready syncs schemas and diffs generated files on every PR. Scheduled auto-update job preserved.commitlint.yml: usenpm ciinstead ofnpm install --save-dev; the@commitlint/*packages are already indevDependencies.
-
933eb2d: Two response-layer fixes for agents built from partial skill coverage:
buildCreativeResponse/buildCreativeMultiResponseno longer crash on missing fields. The default summary previously dereferenceddata.creative_manifest.format_id.idwithout guards — handlers that dropformat_id(required bycreative-manifest.json) crashed the dispatcher withCannot read properties of undefined (reading 'id'), swallowing the real schema violation behind an opaqueSERVICE_UNAVAILABLE. Now the summary optional-chains through the field chain and falls back to a generic string, so the response reaches wire-level validation and the buyer sees the actual missing-field error.replayed: falseis no longer injected on fresh executions.protocol-envelope.jsonpermits the field to be "omitted when the request was executed fresh"; emittingfalseviolates strict task response schemas that declareadditionalProperties: false(create-property-list-response, etc.). Fresh responses now drop any priorreplayedmarker; replays still carryreplayed: true. The existingtest/lib/idempotency-client.test.js"replayed omitted is surfaced as undefined" test aligns with this shift.Surfaced by matrix v10: six
creative_generativepairs crashed with the dereference, and everyproperty_listspair hit theadditionalPropertiesviolation. -
5eb2ae9: fix(testing):
context.no_secret_echowalks structuredTestOptions.auth, andregisterAssertionaccepts{ override: true }-
The default
context.no_secret_echoassertion in@adcp/client/testing
previously treatedoptions.authas a string and added the whole
discriminated-union object to its secret set.String.includes(obj)
against[object Object]matched nothing, so the assertion was
effectively a no-op for every consumer passing structured auth (bearer,
basic, oauth, oauth_client_credentials). It now extracts the leaf
secrets across every variant:- bearer:
token - basic:
username,password, and the base64user:passblob an
Authorization: Basicheader would carry - oauth:
tokens.access_token,tokens.refresh_token,
client.client_secret(confidential clients) - oauth_client_credentials:
credentials.client_idand
credentials.client_secret— resolving$ENV:VARreferences to their
runtime values so echoes of the real secret (not the reference string)
are caught — plustokens.access_token/tokens.refresh_token
A minimum-length guard (8 chars) skips substring matching on fixture
values that would otherwise collide with benign JSON. - bearer:
-
registerAssertion(spec, { override: true })now replaces an existing
registration instead of throwing. Lets consumers swap in a stricter
version of an SDK default (e.g. their owncontext.no_secret_echo)
without callingclearAssertionRegistry()and re-registering every other
default. Default behaviour ({ override: false }/ no options) is
unchanged and still throws on duplicate ids.
-
-
afc01f1: Widen two bundled default assertions per security-review feedback on adcontextprotocol/adcp#2769.
idempotency.conflict_no_payload_leak— flip the denylist-of-5-fields to an allowlist of 7 envelope keys (code,message,status,retry_after,correlation_id,request_id,operation_id). The previous implementation only flaggedpayload,stored_payload,request_body,original_request,original_response— a seller inliningbudget,start_time,product_id, oraccount_idat theadcp_errorroot slipped past, turning idempotency-key reuse into a read oracle for stolen-key attackers. Allowlisting closes the hole: anything a seller adds beyond the 7 envelope fields now fails the assertion.context.no_secret_echo— scan the full response body recursively (not just.context), add a bearer-token literal regex (/\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/i), add recursive suspect-property-name match (authorization,api_key,apikey,bearer,x-api-key), and pick upoptions.test_kit.auth.api_keyas a verbatim-secret source. The previous scope (response.contextonly, verbatimoptions.auth_token/.auth/.secrets[]only) missed the common cases where sellers echo credentials intoerror.message,audit.incoming_auth, nested debug fields, or as header-shaped properties. All caller-supplied secrets gate on a minimum length (8 chars) to avoid false positives on placeholder values.Both changes are patch-level — the assertion ids, public registration API, and passing-case behavior are unchanged; the narrowing on main was fresh in 5.9 and had no adopters broad enough for the strictening to break in practice.
governance.denial_blocks_mutationis unchanged.16 new unit tests cover both widenings: allowlist hits (valid envelope passes), denylist vestigial names still fail, non-allowlisted field leaks (including stable sorted error output), plus bearer literals, verbatim
options.auth_tokenecho,options.secrets[]echo,test_kit.auth.api_keyecho, suspect property names at any depth, array walking, short-value false-positive guard, and prose-"bearer" ignore.
v5.9.0
Minor Changes
-
6180150: Fix A2A multi-turn session continuity + add
pendingTaskIdretention for HITL flows. Mirrors adcp-client-python#251.The bug. The A2A adapter (
callA2ATool) never putcontextIdortaskIdon the Message envelope — every send opened a fresh server-side session regardless of caller state.AgentClientcompounded the error by storingresult.metadata.taskIdintocurrentContextIdon every success, so the field that was supposed to carry the conversation id was actually carrying a per-task correlation id. Multi-turn A2A conversations against sellers that key state offcontextId(ADK-based agents, session-scoped reasoning, any HITL flow) silently fell back to new-session-every-call.The fix.
callA2ATooltakes a newsessionarg and injectscontextId/taskIdonto the Message per the @a2a-js/sdk type.ProtocolClient.callToolthreads session ids through to the A2A branch (MCP unaffected — no session concept there).TaskExecutorstops aliasingoptions.contextIdto the client-minted correlationtaskId. The localtaskIdis now always a fresh UUID; the caller'scontextIdrides on the wire envelope only.TaskResultMetadatagainscontextId(server-returned A2A session id) andserverTaskId(server-tracked task id), populated from the response byProtocolResponseParser.getContextId/getTaskId.AgentClientretainscontextIdacross sends (auto-adopted from server responses so ADK-style id rewriting is transparent) and trackspendingTaskIdonly while the last response was non-terminal (input-required/working/submitted/auth-required/deferred). Terminal states clearpendingTaskIdso the next call starts fresh.
Public API (AgentClient).
client.getContextId(); // read retained contextId client.getPendingTaskId(); // read pending server taskId (HITL resume) client.resetContext(); // wipe session state client.resetContext(id); // rehydrate persisted contextId across process restart
setContextId(id)andclearContext()still exist for backwards compatibility (clearContextnow delegates toresetContext()).One AgentClient per conversation. Sharing an instance across concurrent conversations interleaves session ids (last-write-wins) — create a fresh
AgentClientor callresetContext()per logical conversation. Callers needing resume-across-process-restart should persistgetContextId()/getPendingTaskId()after non-terminal responses and seed them back viaresetContext(id)+ directsetContextIdon rehydration.Behavior change to note.
TaskOptions.contextIdno longer overrides the client-minted correlationtaskId(which was its unintended side effect). Callers who were readingresult.metadata.taskIdexpecting to see their caller-suppliedcontextIdshould now readresult.metadata.contextId. -
0e7c1c9:
createAdcpServer's dispatcher now auto-unwrapsthrow adcpError(...)into the normal response path. Handlers thatthrowan envelope (instead ofreturn-ing it) used to surface asSERVICE_UNAVAILABLE: Tool X handler threw: [object Object]— the thrown value is a plain object, not anError, soerr.messageis undefined andString(err)yields the[object Object]literal. The dispatcher now detects the envelope shape ({ isError: true, content: [...], structuredContent: { adcp_error: { code } } }) and returns it directly, preserving the typed code / field / suggestion exactly as if the handler had writtenreturn.Driver: matrix v8 showed this pattern persisting across fresh-Claude builds even when the skill examples use
return. Fixing it at the dispatcher closes the class of bugs once, instead of hoping every skill-corpus update lands. Alogger.warnstill fires on unwrap so agent authors see they should switch toreturn, but buyers stop paying for the mistake.Idempotency claims are released on unwrap (same as any other thrown path) so retries proceed normally. Non-envelope throws (
TypeError, custom errors, strings, objects without the full envelope shape) still surface asSERVICE_UNAVAILABLEwith the underlying cause indetails.reason— the existing handler-throw disclosure from PR #735 is unchanged. -
8c64d65: Bundle the
governance.denial_blocks_mutationdefault assertion and auto-register the existing defaults on any@adcp/client/testingimport (adcontextprotocol/adcp#2639, #2665 closed as superseded).New default assertion (
default-invariants.ts):governance.denial_blocks_mutation— once a plan receives a denial signal (GOVERNANCE_DENIED,CAMPAIGN_SUSPENDED,PERMISSION_DENIED,POLICY_VIOLATION,TERMS_REJECTED,COMPLIANCE_UNSATISFIED, orcheck_governancereturningstatus: "denied"), no subsequent step in the run may acquire a resource for that plan. Plan-scoped viaplan_id(pulled from response body or the runner's recorded request payload — never stale step context). Sticky within a run: a later successfulcheck_governancedoes not clear the denial. Write-task allowlist excludessync_*batch shapes for now. Silent pass when no denial signal appears.Auto-registration wiring:
storyboard/index.tsnow side-importsdefault-invariantsso any consumer of@adcp/client/testingpicks up all three built-ins (idempotency.conflict_no_payload_leak,context.no_secret_echo,governance.denial_blocks_mutation). Previously onlycomply()triggered registration; directrunStoryboardcallers against storyboards declaringinvariants: [...]would throwunregistered assertionon resolve. Consumers who want to replace the defaults canclearAssertionRegistry()and re-register.Supersedes #2665 (the sibling
@adcp/compliance-assertionspackage proposal): shipping these in-band is the lower-ceremony path and makes storyboards that reference the ids work out of the box against a fresh@adcp/clientinstall. -
7aca3fa: Add typed
CapabilityResolutionErrorforresolveStoryboardsForCapabilities(and by extensioncomply()). Addresses #734.The problem. The resolver threw plain
Errorinstances for two distinct, actionable agent-config faults — "specialism has no bundle" and "specialism's parent protocol isn't declared insupported_protocols". Callers (AAO's compliance heartbeat,evaluate_agent_quality, the publicapplicable-storyboardsREST endpoint) could only distinguish them by regexing the message, which broke if wording drifted and caused agent-config faults to page observability as system errors.The fix. Export
CapabilityResolutionError extends ADCPErrorwith acodediscriminator and structured fields so callers can branch without parsing messages:import { CapabilityResolutionError } from '@adcp/client/testing'; try { resolveStoryboardsForCapabilities(caps); } catch (err) { if (err instanceof CapabilityResolutionError) { switch (err.code) { case 'unknown_specialism': // err.specialism break; case 'specialism_parent_protocol_missing': // err.specialism, err.parentProtocol break; } } }
Existing message text is preserved so regex-based callers keep working during the migration. The
unknown_protocolcode is reserved for future use — today an unknownsupported_protocolsentry still logs aconsole.warnand is skipped (fail-open), not thrown. -
7f27e8f:
createAdcpServernow defaultsvalidation.responsesto'warn'whenprocess.env.NODE_ENV !== 'production'. Previously both sides defaulted to'off', leaving schema drift to surface downstream as crypticSERVICE_UNAVAILABLEoroneOfdiscriminator errors far from where the offending field lives.The new default catches handler-returned drift at wire-validation time with a clear field path, in dev/test/CI, where you want the signal. Production behavior is unchanged — set
NODE_ENV=productionand both sides stay'off'.Override explicitly via
createAdcpServer({ validation: { responses: 'off' | 'warn' | 'strict', requests: ... } })— an explicit config always wins over the environment-derived default.This is the first half of the architecture fix tracked in #727 — validation belongs at the wire layer, not in response builders. Tightening generated TS discriminated unions so
tsccatches sparse shapes is the remaining half.Cost: one AJV compile per tool on cold start + one validator invocation per response in dev. No effect on production.
-
0cc20df:
createAdcpServer'sexposeErrorDetailsnow defaults totrueoutsideNODE_ENV=production. Handler throws emit the underlying cause message and handler name inadcp_error.details+ the human-readable text, so agent authors seeSERVICE_UNAVAILABLE: Tool acquire_rights handler threw: Cannot find module '@adcp/client/foo'instead of the opaqueencountered an internal errorwe used to ship.- Production behavior is unchanged (errors stay redacted for live agents).
- Explicit
exposeErrorDetails: falsestill wins — production deployments that want the redaction without relying onNODE_ENVshould keep setting it. logger.error('Handler failed', ...)now includes the full stack (err.stack) so server logs point at the exact line that blew up, not just the message.
Matrix-harness debuggability was the driver: every
SERVICE_UNAVAILABLEin matrix v5–v7 was an opaque black box that required re-running with--keep-workspacesand inspecting Claude-generated code to figure out why a handler threw. With this default, the matrix log shows the fault lin...
v5.8.2
Patch Changes
-
2942e58: Fix
createAdcpServercontext echo for Sponsored Intelligence tools.si_get_offeringandsi_initiate_sessiondefinecontextas a domain-specific string on the request but require the protocol echo object on the response. The response auto-echo now only copiesrequest.contextwhen it is a plain object, so SI responses no longer fail with/context: must be object. -
56bbc59: Follow-up to the skill schema refresh (PR #716) targeting matrix failures that persisted:
DEFAULT_REPORTING_CAPABILITIESover hand-rolled literals — seller, generative-seller, and retail-media skill product examples previously hand-rolledreporting_capabilities: { ... }which drifts every time the spec adds a required field (most recentlydate_range_supportin AdCP latest). Skills now use the SDK-provided constant and flag the drift tax explicitly.create_media_buymust persistcurrency+total_budget— seller skill'screateMediaBuyexample flattens requesttotal_budget: { amount, currency }into top-levelcurrency+total_budgetfields on the persisted buy, so subsequentget_media_buysresponses pass the new required-field schema check. The old example stored onlypackages[].budgetand the required top-level fields weren't reconstructable.update_media_buy.affected_packagesmust bePackage[], notstring[]— seller skill'supdateMediaBuyexample now returns package objects ({ package_id, ... }) instead of bare IDs. Theupdate-media-buy-responseoneOf discriminator rejects string arrays with/affected_packages/0: must be object.
-
7e04fa0: Option B (structural) groundwork — stop treating response shapes as hand-written forever:
generate-agent-docs.tsnow extracts response schemas and emits a_Response (success branch):_block under every tool indocs/TYPE-SUMMARY.md. For tools whose response is aoneOfsuccess/error discriminator (e.g.,update_media_buy), the generator picks the success arm (noerrorsrequired field) so builders see the happy-path shape._Request:_and_Response_are now visually separated.TYPE-SUMMARY.mdis regenerated; every tool now carries both sides of the wire.- Seller + creative skills: added explicit top-level
currencyingetMediaBuyDeliveryandgetCreativeDeliveryexamples. The response schemas require it; the old examples omitted it and fresh-Claude agents built under those skills failed/currency: must have required propertyvalidation.
Builders can now cross-reference hand-written skill examples against an auto-updating TYPE-SUMMARY response block. When the spec adds a required field, the generated doc updates immediately while the skill example may lag — that's the drift-detection signal.
Next logical step (not in this PR): replace the hand-written
**tool** — Response Shapeblocks in skills with directSee [TYPE-SUMMARY.md § tool](…)pointers so the skill narrative focuses on logic and the shape stays generated.
v5.8.1
Patch Changes
-
f61f284: Re-export the storyboard assertion registry (
registerAssertion,
getAssertion,listAssertions,clearAssertionRegistry,
resolveAssertions, and typesAssertionSpec,AssertionContext,
AssertionResult) from@adcp/client/testingso authors of invariant
modules can import them from the documented package entry point. The
underlying module (./storyboard/assertions) already exported these;
only the parent./testingindex was missing the re-exports. Closes
the gap introduced by #692. -
bdebac9:
refs_resolvescope: canonicalize$agent_urlby stripping transport
suffixes instead of comparing raw target URL to bare agent origins.Before this fix, storyboards using
scope: { key: 'agent_url', equals: '$agent_url' }silently graded every source refout_of_scopeon MCP
and A2A runners, because$agent_urlexpanded to the runner's target
URL (with/mcp,/a2a, or/.well-known/agent.jsonsuffixes) while
refs carried the bare agent URL per AdCP convention. Net effect: the
check degraded from integrity enforcement to a no-op on every MCP agent.The scope comparator now mirrors
SingleAgentClient.computeBaseUrl:
strip/mcp,/a2a,/sse, and/.well-known/agent[-card].json
suffixes; lowercase scheme and host; drop default ports; strip
userinfo, query, and fragment. Path below the transport suffix is
preserved, so sibling agents at different subpaths on a shared host
(e.g.https://publisher.com/.well-known/adcp/salesvs
/.well-known/adcp/creative) remain distinguishable. Closes #710. -
bdebac9:
refs_resolve: harden grader-visible observation andactual.missing
payloads against hostile agent responses.Compliance reports may be published or forwarded to third parties, so
every ref field emitted by the runner is now:- Userinfo-scrubbed on URL-keyed fields via WHATWG URL parsing plus
a regex fallback that scrubsscheme://user:pass@shapes embedded
in non-URL fields. Credentials planted inagent_urlvalues can no
longer leak through compliance output. - Scheme-restricted on URL-keyed fields: non-
http(s)schemes
(e.g.javascript:,data:,file:) are replaced with a
<non-http scheme: …>placeholder so downstream UIs rendering
agent_urlas a link cannot inherit a stored-XSS vector. - Length-capped at 512 code points per string field, with a
code-point-boundary truncation that preserves surrogate pairs. - Count-capped at 50 observations per check, with an
observations_truncatedmarker when the cap fires. Meta
observations (scope_excluded_all_refs,target_paginated)
precede per-ref entries so the cap never drops primary signal.
Match and dedup behavior is unchanged: the internal projection used
for ref comparison is kept separate from the sanitized projection used
for user-facing output, so truncation never false-collapses dedup
keys.refsMatchandprojectRefalso now usehasOwnPropertyto
prevent storyboard authors from accidentally drawing match keys from
Object.prototype. Closes #714. - Userinfo-scrubbed on URL-keyed fields via WHATWG URL parsing plus
-
bdebac9:
refs_resolve: emit ascope_excluded_all_refsmeta-observation when
a scope filter partitions every source ref out. The integrity check
enforces nothing when no ref falls in-scope; graders previously got a
silent pass. The meta-observation surfaces the structural smell without
changing pass/fail semantics. Suppressed underon_out_of_scope: 'ignore'
(which explicitly opts out of scope warnings). Closes #711. -
bdebac9:
refs_resolve: detect paginated current-step targets and demote
unresolved refs to observations instead of failing the check.Previously, when the target response carried
pagination.has_more: true, any ref legitimately defined on a later page graded as
missing— a false-positive failure against a conformant paginating
seller. The runner now emits atarget_paginatedmeta-observation and
reports each would-be-missing ref as anunresolved_with_pagination
observation, letting the check pass until the spec-level resolution
lands (compliance mode requiring sellers to return everything
referenced by products in a single response). Closes #712. -
c4ff3e6: Skill example refresh to match recent upstream schema changes and fix a brand-rights coverage gap surfaced by the
compliance:skill-matrixdogfood harness:list_creative_formats.renders[]: upstream restructured renders to requireroleplus exactly one ofdimensions(object) orparameters_from_format_id: trueunderoneOf. Updated seller, creative, generative-seller, and retail-media skill examples; flaggedrenders: [{ width, height }]as the canonical wrong shape.get_media_buys.media_buys[]:currencyandtotal_budgetare now required per row. Seller skill example now shows both; added a persistence note (save these fields oncreate_media_buyso subsequent queries can echo them).contextresponse field: schema-typed asobject. Across all 8 skills, rewrote the "Context and Ext Passthrough" section to stop recommendingcontext: args.contextecho (which fabricates string values whenargs.contextis undefined or confused with domain fields likecampaign_context). Explicit guidance: leave the field out of your return —createAdcpServerauto-injects the request's context object; hand-setting a non-object string fails validation and the framework does not overwrite.- Brand-rights governance flow: the
brand_rights/governance_deniedscenario expects the brand agent to callcheck_governancebefore issuing a license. Addedaccounts: { syncAccounts, syncGovernance }handlers and acheckGovernance()call in theacquireRightsexample, returningGOVERNANCE_DENIEDwith findings propagated from the governance agent. - Seller idempotency section: referenced adcontextprotocol/adcp-client#678 as a known grader-side limitation on the missing-key probe (MCP Accept header negotiation), so builders don't chase a skill fix for what's actually a grader issue.
v5.8.0
Minor Changes
-
809d02e:
adcp storyboard rungains--invariants <module[,module...]>. The flag
dynamic-imports each specifier before the runner resolves
storyboard.invariants, giving operators a way to populate the assertion
registry (adcp#2639) without editing the CLI. Relative paths resolve against
the current directory; bare specifiers resolve as npm packages.Modules are expected to call
registerAssertion(...)at import time. The
flag runs before the--dry-rungate so bad specifiers surface immediately
during preview, not after agent resolution and auth.Applies to
adcp storyboard run,adcp comply(deprecated alias), and
adcp storyboard run --urlmulti-instance dispatch. -
46de887: Add
createComplyControllerto@adcp/client/testing— a domain-grouped
seller-side scaffold for thecomply_test_controllertool. Takes typed
seed/force/simulateadapters and returns{ toolDefinition, handle, handleRaw, register }so a seller can wire the tool with a single
controller.register(server)call.import { createComplyController } from '@adcp/client/testing'; const controller = createComplyController({ // Gate on something the SERVER controls — env var, resolved tenant flag, // TLS SNI match. Never trust caller-supplied fields like input.ext. sandboxGate: () => process.env.ADCP_SANDBOX === '1', seed: { product: ({ product_id, fixture }) => productRepo.upsert(product_id, fixture), creative: ({ creative_id, fixture }) => creativeRepo.upsert(creative_id, fixture), }, force: { creative_status: ({ creative_id, status }) => creativeRepo.transition(creative_id, status), }, }); controller.register(server);
The helper owns scenario dispatch, param validation, typed error
envelopes (UNKNOWN_SCENARIO,INVALID_PARAMS,FORBIDDEN), MCP
response shaping, and seed re-seed idempotency (same id + equivalent
fixture returnsprevious_state: "existing"; divergent fixture returns
INVALID_PARAMSwithout touching the adapter). Transition enforcement
stays adapter-side so the controller and the production path share a
single state machine.Hardened against common misuse: sandbox gate requires strict
=== true
(a gate that returns a truthy non-boolean denies, not allows); fixture
keys__proto__/constructor/prototypeare rejected with
INVALID_PARAMS; the default seed-fixture cache is capped at 1000
net-new keys to bound memory under adversarial seeding; and the
toolDefinition.inputSchemais shallow-copied so multiple controllers
on one process don't share a mutable shape.list_scenariosbypasses the sandbox gate so capability probes always
succeed — buyer tooling can distinguish "controller exists but locked"
from "controller missing", while state-mutating scenarios remain gated.
register()emits aconsole.warnwhen nosandboxGateis configured
and noADCP_SANDBOX=1/ADCP_COMPLY_CONTROLLER_UNGATED=1env flag is
set, so silent fail-open misuse becomes loud without breaking the
optional-gate API shape.Also extends
TestControllerStorewith the five seed methods
(seedProduct,seedPricingOption,seedCreative,seedPlan,
seedMediaBuy) and exportsSEED_SCENARIOS,SeedScenario,
SeedFixtureCache, andcreateSeedFixtureCache. Existing
registerTestControllercallers now pick up the seed surface and an
internal idempotency cache for free. Closes #701. -
d8fd93f: Add
runConformance(agentUrl, opts)— property-based fuzzing against an
agent's published JSON schemas, exposed as a new@adcp/client/conformance
subpath export sofast-checkand the schema bundle stay off the runtime
client path. Closes #691.Under the hood:
fast-checkarbitraries derived from the bundled draft-07
schemas atschemas/cache/latest/bundled/, paired with a two-path oracle
that classifies every response as accepted (validates the response
schema), rejected (well-formed AdCP error envelope with a spec-enum
reason code — the accepted rejection shape), or invalid (schema
mismatch, stack-trace leak, credential echo, lowercase reason code,
mutated context, or missing reason code). Responses that cleanly reject
unknown references count as passes, not failures.Stateless tier covers 11 discovery tools across every protocol:
get_products,list_creative_formats,list_creatives,
get_media_buys,get_signals,si_get_offering,
get_adcp_capabilities,tasks_list,list_property_lists,
list_content_standards,get_creative_features. Self-contained-state
and referential-ID tiers are tracked for follow-up releases.import { runConformance } from '@adcp/client/conformance'; const report = await runConformance('https://agent.example.com/mcp', { seed: 42, turnBudget: 50, authToken: process.env.AGENT_TOKEN, }); if (report.totalFailures > 0) process.exit(1);
See
docs/guides/CONFORMANCE.mdfor the full options reference. -
7c0b146: Conformance fuzzer Phase 2 (#698) — referential tools, fixture injection,
andadcp fuzzCLI.- Referential stateless tools: 6 new tools in the default run —
get_media_buy_delivery,get_property_list,get_content_standards,
get_creative_delivery,tasks_get,preview_creative. Random IDs
exercise the rejection surface (agents must return
REFERENCE_NOT_FOUND, not 500). - Fixtures: new
RunConformanceOptions.fixturesoption. When a
request property name matches a pool (creative_id/creative_ids,
media_buy_id/media_buy_ids,list_id,task_id,plan_id,
account_id,package_id/package_ids), the arbitrary draws from
fc.constantFrom(pool)instead of random strings — testing the
accepted path on referential tools. adcp fuzz <url>CLI: new subcommand with--seed,--tools,
--turn-budget,--protocol,--auth-token,--fixture name=a,b,
--format human|json,--max-failures,--max-payload-bytes, and
--list-tools. Exits non-zero on failure. Reproduction hint on every
failure:--seed <seed> --tools <tool>.
adcp fuzz https://agent.example.com/mcp --seed 42 adcp fuzz https://agent.example.com/mcp --fixture creative_ids=cre_a,cre_b --format json | jqNew public exports:
REFERENTIAL_STATELESS_TOOLS,DEFAULT_TOOLS,
ConformanceFixtures,SkipReason. - Referential stateless tools: 6 new tools in the default run —
-
73db0ac: Conformance fuzzer Stage 4 — creative seeding, configurable brand,
broader stack-trace detection, additionalProperties probing, and stricter
context-echo enforcement.Coverage (A)
sync_creativesauto-seeder: preflightslist_creative_formats,
picks the first format whose required assets are all of a simple type
(image, video, audio, text, url, html, javascript, css, markdown),
synthesizes placeholder values, and capturescreative_ids from the
response. Now runs as part ofseedFixtures/autoSeed.seedBrandoption +--seed-brand <domain>CLI flag: overrides
the mutating-seeder brand reference. Defaults to
{ domain: 'conformance.example' }, which sellers with brand
allowlists reject. Configurable per run.
Oracle (D)
- JVM + .NET stack-trace signatures:
at com.foo.Bar.method(Bar.java:42)
andat Foo.Bar() in X.cs:line 42shapes detected alongside the
existing V8/Python/Go/PHP patterns. - additionalProperties injection: when a schema permits extra keys
(additionalProperties: true), the generator sometimes injects one
(~15% frequency, single extra key from a fixed vocabulary). Exercises
the unknown-field tolerance surface — a common crash source where
agents deserialize into strict structs and reject unexpected keys. - Stricter context-echo: when a response schema declares a
top-levelcontextproperty, dropping it entirely is now an invariant
violation. Silent tolerance preserved for tools whose response schema
omits the field.
New public exports: extended
SeederNamewith'sync_creatives',
SeedOptions.brand,RunConformanceOptions.seedBrand. -
6b2a3b9: Conformance fuzzer Tier 3 — auto-seeding + update-tool fuzzing.
seedFixtures(agentUrl, opts)helper — creates a property list,
a content-standards config, and (after aget_productspreflight) a
media buy on the agent, captures the returned IDs, and returns a
ConformanceFixturesbag ready to pass torunConformance. Each
seeder is best-effort: failures degrade to a recorded warning and an
empty pool, never a thrown exception.runConformance({ autoSeed: true })— runs the seeder first,
merges results intooptions.fixtures(explicit fixtures win on
conflict), and includes Tier-3 update tools (update_media_buy,
update_property_list,update_content_standards) in the default
tool list. The report carriesautoSeeded: booleanand a
seedWarningsarray.adcp fuzz --auto-seedCLI flag.--list-toolsnow marks
Tier-3 tools with(update — needs --auto-seed or --fixture). The
human-readable report surfaces seeded IDs and any seed warnings.- New
standards_idsfixture pool —content_standardsuses
standards_id, notlist_id, so it gets its own key.
⚠️ Auto-seed mutates agent state. Point at a sandbox tenant — the
fuzzer creates artifacts that t...
v5.7.0
Minor Changes
-
7d33a92: AdCP 3.0 release blockers — SDK-level wiring for conformance-runner integration.
New subpath exports
@adcp/client/compliance-fixtures— canonicalCOMPLIANCE_FIXTURESdata for every hardcoded ID storyboards reference (test-product,sports_ctv_q2,video_30s,native_post,native_content,campaign_hero_video,gov_acme_q2_2027,mb_acme_q2_2026_auction,cpm_guaranteed, etc.) plus aseedComplianceFixtures(server)helper that writes fixtures into the state store under well-knowncompliance:*collections. Closes #663.@adcp/client/schemas— re-exports every generated Zod request schema plusTOOL_INPUT_SHAPES(ready-to-registerinputSchemamap covering non-framework tools likecreative_approvalandupdate_rights) and acustomToolFor(name, description, shape, handler)helper. Closes #667.
Server (
@adcp/client/server)createExpressAdapter({ mountPath, publicUrl, prm, server })returns the four pieces an Express-mounted agent needs:rawBodyVerify(captures raw bytes for RFC 9421),protectedResourceMiddleware(RFC 9728 PRM at the origin root),getUrl(mount-aware URL reconstruction for the signature verifier), andresetHook(delegates toserver.compliance.reset()). Closes #664.requireAuthenticatedOrSigned({ signature, fallback, requiredFor, resolveOperation })bundles presence-gated signature composition withrequired_forenforcement on the no-signature path.requireSignatureWhenPresentgrew an options parameter that carries the samerequiredFor+resolveOperationsemantics. Unsigned requests with no credentials on arequired_foroperation throwAuthErrorwhose cause isRequestSignatureError('request_signature_required'); valid bearer bypass stays valid. Closes #665.respondUnauthorized({ signatureError })emits aWWW-Authenticate: Signature error="<code>"challenge when the rejection comes from the RFC 9421 verifier.serve()auto-detects this viasignatureErrorCodeFromCause(err)— the signed_requests negative-vector grader reads the error code off the challenge, so previously callers had to override the 401 response by hand.AdcpServer.compliance.reset({ force? })drops session state and the idempotency cache between storyboards. Refuses to run in production-like deployments unlessforce: trueis passed.IdempotencyStore.clearAllis now an optional method on the store;memoryBackendimplements it, production backends leave it undefined. Closes #666.
Testing (
@adcp/client/testing)- Request-signing grader accepts an
agentCapabilityoption. When present, vectors whoseverifier_capabilitycan't coexist with the agent's declared profile (covers_content_digestdisagreement, vector-assertedrequired_fornot in agent's list) auto-skip withskip_reason: 'capability_profile_mismatch'.skipVectorsstays available for operator-driven overrides. Closes #668.
-
5b2ebb3: v3 audit follow-ups — tightened per expert review:
Build pipeline
build:libnow runssync-versionbeforetscsosrc/lib/version.tscan't drift frompackage.jsonacross changeset-driven bumps.sync-versionnow validates both version strings against/^[0-9A-Za-z.\-+]+$/to prevent template injection into the generated TS file.
sync_creatives validator
- New
SyncCreativesItemSchema,SyncCreativesSuccessStrictSchema, andSyncCreativesResponseStrictSchemaexports. The strict schema enforces: requiredcreative_id+action; spec's conditional thatstatusMUST be absent whenaction ∈ {failed, deleted};preview_urllimited tohttp(s):URLs; ISO-8601expires_at;assignment_errorskey regex. Wired intoTOOL_RESPONSE_SCHEMASso pipeline-level strict validation catches per-item drift forsync_creativesresponses automatically.
V3 guard
- New
VersionUnsupportedErrorwith typedreason('version' | 'idempotency' | 'synthetic'). Agent URL stays on the instance property but is omitted from the default message to prevent leakage into shared log sinks. client.requireV3()now corroborates the v3 claim: requiresmajorVersions.includes(3),adcp.idempotency.replayTtlSecondspresent, and rejects synthetic capabilities. Closes the "lying seller" bypass path.- New
allowV2config option onSingleAgentClientConfig— per-client bypass;ADCP_ALLOW_V2=1env fallback only applies whenallowV2isundefined. Enables safe use in multi-tenant deployments. requireV3ForMutations: trueopt-in gates mutating calls before dispatch.