chore: release package#728
Merged
Merged
Conversation
dad373e to
8b6c622
Compare
8b6c622 to
b58a128
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@adcp/client@5.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).
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 feat(server): default exposeErrorDetails to true outside NODE_ENV=production #735 is unchanged.8c64d65: Bundle the
governance.denial_blocks_mutationdefault assertion and auto-register the existing defaults on any@adcp/client/testingimport (Compliance: specialism-level invariants layer on storyboard runner 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: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.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 line on the first run.e979d07: Add OAuth 2.0 client credentials (RFC 6749 §4.4) support to the library and CLI for machine-to-machine compliance testing. Addresses adcontextprotocol/adcp#2677.
The problem. Sales agents that authenticate via OAuth client credentials couldn't be tested with
@adcp/clientwithout a user manually exchanging credentials for a token and pasting the bearer in. Tokens expire; CI pipelines need a way to point the library at a token endpoint and let it handle refresh.Library-level auto-refresh.
ProtocolClient.callToolnow re-exchanges the secret for a fresh access token before every call whenAgentConfig.oauth_client_credentialsis set (cached while valid — single POST on miss, no-op on warm cache). Concurrent callers for the same agent coalesce onto one refresh POST. On a mid-call 401 the client force-refreshes once and retries — covers the case where the AS rotates something out of band. Refreshed tokens persist via any attachedOAuthConfigStorage.New
authtype onTestOptions.createTestClient/ADCPMultiAgentClientaccept{ type: 'oauth_client_credentials', credentials, tokens? }. Storyboard runs,adcp fuzz,adcp grade, and any programmatic consumer get auto-refresh for free.CLI flags on
--save-auth:Full subcommand help:
adcp --save-auth --help.Secret storage. Literal secrets land in
~/.adcp/config.json(mode0600, directory0700). For CI,--client-id-env/--client-secret-envstore a$ENV:VAR_NAMEreference resolved at token-exchange time — nothing sensitive on disk:Empty env-var values are rejected loudly (catches the common
.envtypoCLIENT_SECRET=).Audience binding (RFC 8707).
AgentOAuthClientCredentialsacceptsresource?: string | string[](emitted as repeatedresourceform fields, RFC 8707) andaudience?: string(the Auth0/Okta/Azure AD vendor parameter). Required for agents behind audience-validating proxies.Security hardening.
token_endpointmust behttps://—http://is rejected with a typedmalformederror before any request hits the wire.http://localhostandhttp://127.0.0.1are allowed for local dev.https://user:pass@auth.example.com/token) are rejected — credentials belong inclient_id/client_secret, not the URL, and leaking them via error messages and log aggregators is easy.allowPrivateIp: true. The CLI opts in (operator-driven); the library trusts whatever the agent URL already trusts. Hosted consumers accepting untrusted configs get the guard for free.+,!'()*percent-encoded) — notencodeURIComponent. Fixes interop with secrets containing those characters.error_descriptionfrom the authorization server is control-character-stripped and truncated before being surfaced — defends against ANSI / CRLF injection from a hostile AS.is401Errornow recognizes MCP SDK error shape (err.code === 401). The MCPStreamableHTTPClientTransportthrows errors with HTTP status on.code; the retry path for CC and auth-code flows was silently skipping them. Caught by the new integration test.CLI flags (all on
--save-auth):--client-id <value>/--client-id-env <VAR>— literal or env reference--client-secret <value>/--client-secret-env <VAR>— literal or env reference--scope <scope>— optional OAuth scope--oauth-token-url <url>— optional; discovered from the agent URL via RFC 9728 + RFC 8414 when omitted. Supply explicitly only when the agent does not advertise OAuth metadata.--oauth-auth-method basic|body— credential placement (default:basicper RFC 6749 §2.3.1)Programmatic API under
@adcp/client/auth:exchangeClientCredentials(credentials, options?)— one-shot token exchangeensureClientCredentialsTokens(agent, options?)— refresh-if-stale helper that updatesagent.oauth_tokensin place (coalesces concurrent calls) and optionally persists viaOAuthConfigStorageClientCredentialsExchangeError— typed error withkind: 'oauth' | 'malformed' | 'network',oauthError,oauthErrorDescription,httpStatusMissingEnvSecretError— typed error withreason: 'unset' | 'empty'resolveSecret,isEnvSecretReference,toEnvSecretReference— secret-resolution utilitiesAgentOAuthClientCredentials— type for the newAgentConfig.oauth_client_credentialsfieldThe authorization-code flow (
--oauth) and existingauth_tokenpaths are unchanged.createFileOAuthStoragepersistsoauth_client_credentialsalongsideoauth_tokensso CLI and programmatic consumers share the same on-disk shape.65740a1: Thin response builders for four tools whose handlers previously had no typed wrapper, plus per-variant constructors for
acquire_rights:acquireRightsResponse(data)— envelope wrapper on theAcquireRightsResponseunion.acquireRightsAcquired({...}),acquireRightsPendingApproval({...}),acquireRightsRejected({...})— per-variant constructors. A coding agent typingacquireRightsAcqu…gets the right variant's required-field shape directly without reading a 4-variant union.syncAccountsResponse(data)— envelope wrapper onSyncAccountsResponse.syncGovernanceResponse(data)— envelope wrapper onSyncGovernanceResponse.reportUsageResponse(data)with.acceptAll(request, { errors })shortcut — the.acceptAllform computesaccepted = usage.length - errors.lengthso the common "ack all / ack all minus validated failures" cases are one call.All four are auto-applied via
createAdcpServer'sTOOL_META— handlers return domain objects and the framework wraps. Also exported from@adcp/clientand@adcp/client/serverfor manual use.Scope note (per test-agent-team review): these builders are only MCP envelope wrappers — they do not enforce schema constraints like
credentials.minLength: 32,authentication.schemes.length === 1, orcreative_manifest.format_idobject shape. Those belong in wire-level Zod validation (already available ascreateAdcpServer({ validation: { responses: 'strict' } }), tracked for default-on). Validation in builders would be the wrong layer — it only fires for tools whose handlers reach the wrapper, misses manual-tool paths, and encourages per-tool workarounds instead of fixing the generator + validator.e68b2fb: Add uniform-error-response fuzz invariant (Fuzz invariant: byte-equivalent error responses for unauthorized vs unknown references #731).
adcp fuzznow runs a paired-probe check on referential lookup tools asserting byte-equivalent error responses for "exists but inaccessible" vs "does not exist" — the AdCP spec MUST from error-handling.mdx (landed in adcp#2689, hardened in adcp#2691).Two modes:
isError/ A2Atask.status.statedivergence. Always runs.--auth-token-cross-tenantflag +ADCP_AUTH_TOKEN_CROSS_TENANTenv var): seeder runs as tenant A, invariant probes as tenant B against the seeded id + a fresh UUID. Catches the full cross-tenant existence-leak surface.Comparator enforces identical
error.code/message/field/details, HTTP status, MCPisError, A2Atask.status.state, and response headers with a closed allowlist (Date,Server,Server-Timing,Age,Via,X-Request-Id,X-Correlation-Id,X-Trace-Id,Traceparent,Tracestate,CF-Ray,X-Amz-Cf-Id,X-Amz-Request-Id,X-Amzn-Trace-Id).Content-Length,Vary,Content-Type,ETag,Cache-Control, and rate-limit headers MUST match.Tool coverage:
get_property_list,get_content_standards,get_media_buy_delivery,get_creative_delivery,tasks_get. Extending is additive viaTOOL_ID_CONFIGinsrc/lib/conformance/invariants/uniformError.ts.Public API:
RunConformanceOptions.authTokenCrossTenant?: stringConformanceReport.uniformError: UniformErrorReport[]--auth-token-cross-tenant <token>Security: response headers are redacted at capture time when they name a credential (
Authorization,X-Adcp-Auth,Cookie, etc.), and bearer tokens echoed in response bodies are masked — no credential ever lands in a stored report.Docs:
docs/guides/VALIDATE-YOUR-AGENT.mdhas a new "Uniform-error-response invariant (paired probe)" subsection including the preparation checklist for two-tenant testing.skills/build-seller-agent/SKILL.md§ Protocol-Wide Requirements adds "Resolve-then-authorize" as a universal MUST;skills/build-governance-agent/SKILL.mdcross-references it.fb38c53: Breaking for raw-string callers: adapter error code string values changed from lowercase-custom (
'list_not_found') to uppercase-snake ('REFERENCE_NOT_FOUND','UNSUPPORTED_FEATURE', etc.) to comply with the AdCP spec's uppercase-snake convention. Closes test agent: get_property_list returns lowercase reason code "not_found" instead of uppercase-snake #700.Affected constants (the KEYS are unchanged, only the emitted string VALUES changed):
PropertyListErrorCodes(property-list-adapter.ts)ContentStandardsErrorCodes(content-standards-adapter.ts)SIErrorCodes(si-session-manager.ts)ProposalErrorCodes(proposal-manager.ts)Unaffected: code that uses the exported enum constants.
PropertyListErrorCodes.LIST_NOT_FOUNDstill resolves — the key is stable, only the emitted value changed.Breaks: code that pattern-matches raw strings. Multiple
*_NOT_FOUNDkeys now collapse to'REFERENCE_NOT_FOUND'so string-based switches can no longer distinguish the source domain.Migration: replace raw-string comparisons with the exported helpers + constants.
Semver justification: bumped
minorrather thanmajorbecause these adapter scaffolds are pre-stable surface intended for implementers extending the stock classes — not yet depended on by downstream shipped products. A repo-wide search found zero raw-string consumers. Value changes in future releases may warrantmajoronce implementers are shipping.Also emitted by this change:
SIErrorCodes.SESSION_TERMINATEDnow emits the message"Session is not active"(previously"Session has already been terminated") to match the existingSESSION_EXPIREDbranch — prevents subclass implementers from accidentally leaking terminal-vs-expired state distinction in multi-tenant deployments.Patch Changes
fb38c53: Drop the
provide_performance_feedbackrequest builder from the storyboard runner so the spec-conformantsample_requestfrom the storyboard drives the payload. The builder emitted non-specfeedback/satisfaction/notesfields that caused conformant sellers to reject the request withINVALID_REQUEST. Closes request-builder.js provide_performance_feedback override sends non-spec fields #689.ba8c907: Fix
sync_catalogsandreport_usagestoryboard request-builders to honorstep.sample_requestwhen present, and use spec-valid defaults when building a fallback.sync_catalogs — before this fix, the builder ignored the storyboard's
sample_requestentirely and returned a hardcoded catalog withfeed_format: 'json'(not in theFeedFormatSchemaunion:google_merchant_center | facebook_catalog | shopify | linkedin_jobs | custom) and notypefield (required byCatalogSchema). Every conformance agent running the generated Zod schema rejected the request with-32602on both paths. The fallback now usestype: 'product'+feed_format: 'custom', and the builder readssample_requestfirst.report_usage — same pattern: builder ignored
sample_requestand returned per-entry shape{ creative_id, impressions, spend: { amount, currency } }which doesn't matchusage-entry.json(expects top-levelvendor_cost: number+currency: string+accounton each entry). Agents rejected with-32602listing all three missing fields. Fixed by readingsample_requestfirst and aligning the fallback to the spec shape.Surfaced by the matrix harness — every
sales_catalog_drivenandcreative_ad_serverrun showed the same builder-generated -32602 before this patch.faef971: Clarify idempotency-on-error semantics in the seller and creative skill docs, driven by the audit in #744.
What the audit found. The dispatcher releases the idempotency claim on every error path — envelope returns, envelope throws, and uncaught exceptions. That's already documented for the "transient failures don't lock into the cache" case, but the handler-author implication wasn't spelled out: a handler that mutates state before erroring will double-write on retry. The surface for this bug widened with #743 (auto-unwrap of thrown envelopes), which blesses
throw adcpError(...)as a supported path.Why not cache terminals instead. The AdCP
recovery: terminalcatalog is mostly state-dependent (ACCOUNT_SUSPENDED,BUDGET_EXHAUSTED,ACCOUNT_PAYMENT_REQUIRED,ACCOUNT_SETUP_REQUIREDall flip after out-of-band remediation). Caching them would lock buyers into stale errors for the full replay TTL. OnlyUNSUPPORTED_FEATUREandACCOUNT_NOT_FOUNDare truly immutable, and re-executing them is cheap.Changes.
skills/build-seller-agent/SKILL.mdidempotency section now documents the mutate-last contract, with a workedbudgetApprovedexample showing the broken-vs-correct ordering and a note on making partial-write paths converge via natural-key upsert.skills/build-creative-agent/SKILL.mdswaps the now-stale "throw surfaces asSERVICE_UNAVAILABLE" rationale (invalidated by feat(server): auto-unwrap thrown adcpError envelopes in dispatcher #743) for the still-true claim-release rationale.No runtime behavior changes; docs only. No changes to
compliance/cache/— storyboards there are machine-synced from the upstream spec repo, so a conformance assertion that locks in error-claim-release semantics is a follow-up foradcontextprotocol/adcp.929b6b3: Add
unresolved_hidden_by_paginationmeta-observation torefs_resolvewhentarget_paginatedAND at least oneunresolved_with_paginationco-occur on the same result. Closes refs_resolve: grader-level flag when target_paginated co-occurs with unresolved_with_pagination (pagination-bypass) #718.Catches the integrity gap introduced by fix(storyboard): refs_resolve follow-ups (#710, #711, #712, #714) #717: a seller that unconditionally returns
pagination.has_more: truecan hide refs it can't service — the demotion logic passes the check, and graders keying onrefs_resolve.passedalone miss the structural smell. The new meta-observation names the co-occurrence neutrally (structural descriptor, not an accusation — graders decide intent) so compliance dashboards get an independent grader signal without changing pass/fail semantics. Shape mirrorsscope_excluded_all_refs(the refs_resolve: warn when scope filter excludes 100% of source refs #711 silent-no-op detector):{ kind, unresolved_count }— the per-ref detail already lives in theunresolved_with_paginationobservations.unresolved_countis deduped, so it matches the per-ref observation count.Becomes redundant when
adcp#2601's "compliance mode returns everything referenced in a single response" rule lands at the spec level.e68b2fb: Internal: MCP and A2A protocol adapters can now capture raw HTTP responses (status, headers, body, latency) when
withRawResponseCapture(fn)is active. Exported fromsrc/lib/protocols/rawResponseCapture.ts. Conformance-only infrastructure — the wrapper is a pass-through when no capture slot is set, so regular clients pay only one AsyncLocalStorage lookup per request. Foundation for the uniform-error-response fuzz invariant (issue Fuzz invariant: byte-equivalent error responses for unauthorized vs unknown references #731).fb38c53: Extract the protocol transport-suffix regex (
/mcp,/a2a,/sse) to a single source inutils/a2a-discoveryand share it betweenSingleAgentClient.computeBaseUrland the storyboardcanonicalizeAgentUrlForScope. Adding a new transport now only requires updating one regex. Closes Factor out shared transport-suffix registry between SingleAgentClient.computeBaseUrl and storyboard validations canonicalizer #719.0169874: Skill fixes uncovered by matrix v8's handler-throw disclosure (PR feat(server): default exposeErrorDetails to true outside NODE_ENV=production #735):
acquire_rights+sync_accounts+sync_governance): swap|→:in the composite account-key template literal.ctx.store.put's key pattern is[A-Za-z0-9_.\-:]—|is rejected and the handler throws on the first sync. Also guardacquireRightsagainst missingaccount.brand.domain/account.operatorbefore composing the key.list_creatives+build_creative): destructurectx.store.list— it returns{ items, nextCursor? }, not a bare array. Previously the examples called.filter/.findon the envelope object and blew up withTypeError, surfaced asSERVICE_UNAVAILABLE. Also flipthrow adcpError(...)toreturn adcpError(...)inbuild_creative; throwing bypasses the envelope path and reports asSERVICE_UNAVAILABLEinstead ofCREATIVE_NOT_FOUND.property-lists): add alist_property_listsexample showingconst { items } = await ctx.store.list('property_list'). Matrix v8 builds repeatedly.map-ed the raw result; the skill now shows the correct shape in-line.No SDK code changes — these are skill-corpus fixes visible to agent builders.
53c531e: Storyboard runner now forwards
push_notification_configfromsample_requestto the outbound request when a programmatic request builder is used (create_media_buy,update_media_buy, etc.). Previously, onlycontext,ext, andidempotency_keywere merged from the hand-authored sample_request on top of the builder output —push_notification_configsilently fell off the wagon, so every webhook-emission conformance phase (universal/webhook-emission,specialisms/sales-broadcast-tvwindow-update webhook, etc.) failed vacuously with the agent under test never receiving the webhook URL.{{runner.webhook_url:<step_id>}}substitution is applied to the carried-over config so the runner's ephemeral receiver URL still resolves correctly. Fixes Runner strips push_notification_config from sample_request despite ADCP_ENVELOPE_FIELDS listing it #747.18fa51b: Extend the uniform-error-response comparator (fuzz: add A2A integration coverage for uniform-error-response invariant #738) to walk A2A Task and Message shapes when looking for the AdCP error envelope.
extractEnvelopenow findsadcp_errornested inresult.artifacts[].parts[].data(Task reply) orresult.parts[].data(Message reply);peelWrappersreduces A2A Task/Message bodies to their data-part payloads so per-requesttask.id/contextId/artifactId/messageIddon't false-positive structural compares on identical success bodies.Adds
test/lib/uniform-error-invariant-a2a.test.js— the A2A-shaped sibling of the existing MCP integration test, running the same five-case matrix (baseline compliant/leak, cross-tenant compliant/leak, baseline fallback) against an in-process A2A seller reached through@a2a-js/sdk/client. Closes the gap where only hand-crafted JSON strings exercised the A2A path.