feat: AdCP 3.0.12 + denial gate + silent assertion status + size-limit wrap#1802
Conversation
Replaces SDK-author "the mock is the impartial referee" prose with a forward link to the spec's normative triage section. Touches: - docs/proposals/lifecycle-state-and-sandbox-authority.md — § "Cross-implementation story" - CLAUDE.md — "When a compliance storyboard fails, triage before patching" - bin/adcp.js — adcp mock-server --help "NOTES" section The spec landed the anchor in adcontextprotocol/adcp#4030 (closing adcp#4029). Triage order spec → mock → SDK is now normatively pinned; the SDK now points at the spec rather than encoding the same opinion. Closes #1524. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…denial gate Closes #1522 — adopts the new universal storyboard from adcp#4028 (live-mode comply_test_controller denial) as a graded path on hello_seller_adapter_guaranteed. The bump unblocks the storyboard end-to-end against framework-gated sellers; the SDK changes below close the wire-level gaps surfaced during integration. ADCP_VERSION 3.0.11 -> 3.0.12; sync-schemas pulls the new comply-controller-mode-gate.yaml universal storyboard and the acme-outdoor-live.yaml test-kit fixture. Framework gate now echoes context/ext on FORBIDDEN refusal (from-platform.ts). The denial storyboard asserts context.correlation_id round-trips on refusal; previously the gate dropped them, so the gate now mirrors them onto the ControllerError envelope. rawMcpProbe parses Streamable-HTTP MCP SSE responses (probes.ts). Strict MCP servers (the official SDK and createAdcpServer) require clients to advertise Accept: application/json, text/event-stream and respond to tools/call with a single SSE event whose data: line is the JSON-RPC envelope. The probe now sends both Accept values and parses the data: line when content-type is text/event-stream. Unit test added. adcp storyboard run --test-kit PATH (bin/adcp.js). Loads a test-kit YAML and threads it into runStoryboard / runFullAssessment. Worked example wires the live-mode probe principal (hello_seller_adapter_guaranteed.ts). Registers demo-acme-outdoor-live-v1 as a second bearer; the resolver stamps mode: 'live' when that token authenticates, triggering FORBIDDEN. CI gate added (hello-seller-adapter-guaranteed.test.js + runHelloAdapterGates.js extraStoryboards parameter). The denial gate runs against the same agent process and fails closed on regression in the framework gate, context echo, or SSE probe parsing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…il' on observation-bearing invariants Closes #1797. status.monotonic and impairment.coherence now stamp status: 'silent' on the onEnd summary record when zero lifecycle resources were observed, and status: 'pass' once at least one was. Downstream renderers (adcp#2834, Addie, CLI summary) can read this per-assertion enum directly rather than inferring from observation_count === 0. Track-level TrackStatus: 'silent' rollup (computeTrackStatus) is unchanged; this fills in the per-assertion analog so wired-but-not- observed assertions don't render identically to real passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onseSizeLimit Closes #1799. transport.maxResponseBytes was dormant on the discovery path: mcpClient.listTools() and A2AClient.fromCardUrl bypassed the ALS slot, so a hostile vendor could return arbitrarily large discovery payloads against a size-capped client. - listTools() (in-process + out-of-process MCP paths) wrapped in withResponseSizeLimit so the existing connectMCP size-limiter sees the active slot. - A2AClient.fromCardUrl + deferred agentCardPromise read wrapped in withResponseSizeLimit. Auth-stamping fetchImpl composed through wrapFetchWithSizeLimit so the slot fires on the actual card fetch. Regression test at test/unit/get-agent-info-size-limit.test.js aborts an oversized A2A card discovery with ResponseTooLargeError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three parallel expert reviews (code-reviewer + security-reviewer +
ad-tech-protocol-expert) converged on one real correctness issue, plus
defense-in-depth nits. CI also flagged out-of-sync generated TS for the
3.0.12 schema bump.
rawMcpProbe now id-matches across all SSE data: events instead of taking
the first. MCP Streamable HTTP allows server-initiated frames (e.g.
notifications/progress) before the final tools/call response — picking
the first data: line would parse the progress envelope instead of the
result. The probe walks every data: line, parses each, and selects the
envelope whose id matches the request, falling back to the last
parseable envelope when no id matches. New unit test in
test/lib/storyboard-security.test.js asserts the progress-frame-before-
result ordering.
Comply gate now caps context/ext echo at 8 KiB serialized. The fields
are open-object on the request schema, so a hostile caller could stuff
arbitrarily large payloads onto the FORBIDDEN refusal. Self-reflection
only (no cross-tenant amplifier), but bounding the response body
prevents the gate from being a self-amplifier. Anything over the cap
drops the field rather than echoing garbage — mirrors a missing field.
Bearer JSDoc warning added to hello_seller_adapter_guaranteed.ts. The
demo-acme-outdoor-live-v1 bearer is published in the open-source SDK
and the public compliance test-kit; callout warns adopters not to copy
it into a production seller's keys map.
Regenerated TS types for AdCP 3.0.12. CI Test & Build flagged
src/lib/types/{core,manifest,schemas,tools}.generated.ts as out of sync
with the 3.0.12 schemas; ran sync-schemas:all + generate-types:all +
generate-wellknown-schemas to land the diff. Adds the optional
account.supported_billing field shape required by the 3.0.12 spec
change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI ci:docs-check flagged docs/llms.txt + docs/TYPE-SUMMARY.md as out-of-sync after the 3.0.12 sync-schemas. The new universal storyboard (comply_controller_mode_gate) and the renamed brand-rights error (GOVERNANCE_DENIED → AcquireRightsRejected) need to land in the agent- facing docs. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expert review (2026-05-17)Three reviews in parallel (code-reviewer + security-reviewer + ad-tech-protocol-expert) on the initial four-commit bundle. Convergent finding addressed in commit `fa25a6f6`; additional defense-in-depth follow-ups from the security review applied in the same commit. Convergent finding (fixed)rawMcpProbe SSE parser took only the first `data:` line. MCP Streamable HTTP allows server-initiated frames (e.g. `notifications/progress`) before the final `tools/call` response on the same SSE stream. The probe would parse the progress envelope and grade the storyboard against the wrong payload. Fix in `src/lib/testing/storyboard/probes.ts`: walk all `data:` lines, JSON-parse each, select the envelope whose `id` matches the request, fall back to the last parseable envelope otherwise. New unit test in `test/lib/storyboard-security.test.js` asserts the progress-frame-before-result ordering. Defense-in-depth nits (applied)
Out-of-scope gaps filed as follow-ups
Reviewer-flagged but accepted as-is
|
…minimal capability fixture
AdCP 3.0.12 added an allOf/if/then to get-adcp-capabilities-response:
sellers declaring media_buy in supported_protocols MUST include account
with supported_billing. The fixture in agent-resolver-publisher-side.test.js
declared media_buy without the matching account block, so all three
identity.brand_json_url cases failed schema validation under the new
cache.
Add a minimal `account: { supported_billing: ['operator'] }` to the
fixture; the suite still tests what it claimed to (identity field
forward-compat), the new closed-property requirement is satisfied
without semantic noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two parallel reviews (code-reviewer + ad-tech-protocol-expert) ran against the bundle. One real blocker + one info-level clarification addressed. #1804 test was mis-targeted (code-reviewer blocker) - The test called `client.getAgentInfo()` which routes through SingleAgentClient's inline A2A discovery branch at line 2755-2798 (already wrapped by PR #1802 for #1799), NOT through the newly- wrapped fetchA2ACanonicalUrl path. - fetchA2ACanonicalUrl is reached via ensureCanonicalUrlResolved, which the public resolveCanonicalUrl() method drives. - Test now calls client.resolveCanonicalUrl() in both cases, which is the only public surface that exercises the new wrap. Phase-cascade ordering noted in runner.ts comment (protocol-expert Q1) - The spec gate says "comply_test_controller advertised"; my detector gates on `step.task === 'comply_test_controller'`. The phase cascade at the seedingMissingController check (line ~1893) enforces "advertised" upstream — by the time per-step grading reaches the detector, the controller is already confirmed present. Added a comment block clarifying this so future maintainers don't read the detector in isolation and miss the upstream guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nner detection (#1805) (#1812) * fix(client): fetchA2ACanonicalUrl honors transport.maxResponseBytes Closes #1804. Second A2A discovery DoS surface, flagged by the security review on PR #1802. fetchA2ACanonicalUrl runs implicitly before every executeTask / listTools / getAgentInfo call against an A2A agent whose canonical URL hasn't been resolved yet. Until this fix it called native fetch without composing through wrapFetchWithSizeLimit, so a hostile vendor could ship a 5 GB agent-card body on first discovery to blow memory before any application-layer parsing ran. Same fix shape as #1799 (getAgentInfo): wrap the A2AClient.fromCardUrl call and the deferred agentCardPromise read in withResponseSizeLimit, and route the auth-stamping fetchImpl through wrapFetchWithSizeLimit so the active ALS slot fires on the wire call. Regression test at test/unit/fetch-a2a-canonical-url-size-limit.test.js aborts an oversized canonical-URL discovery with ResponseTooLargeError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(testing): runner detects force_scenario_unsupported per AdCP 3.0.12 Closes #1805. AdCP 3.0.12 runner-output-contract introduces a new not_applicable detail string runners MUST emit: when a comply_test_controller step targets a force_* scenario that the agent advertises the controller for but does not implement (response `{success: false, error: 'UNKNOWN_SCENARIO'}`), the runner grades the step `not_applicable` with detail `force_scenario_unsupported` BEFORE applying the step's authored validations. Without this, the failing `success: true` check would mask the coverage gap as a real agent fault. - New RunnerDetailedSkipReason variant `force_scenario_unsupported` in src/lib/testing/storyboard/types.ts, mapped onto canonical `not_applicable` via DETAILED_SKIP_TO_CANONICAL. - Detection in src/lib/testing/storyboard/runner.ts before the early- exit unsupported-tool skip path, keyed on the tuple (step.task === 'comply_test_controller', resolved scenario starts with 'force_', response.success === false, response.error === 'UNKNOWN_SCENARIO'). - Companion to fixture_seed_unsupported (seeding.ts) — same shape but for force_* in step phases, not seed_* in the fixtures phase. Test at test/lib/storyboard-force-scenario-unsupported.test.js covers the happy path, the negative case (non-force_* UNKNOWN_SCENARIO does NOT trigger), the success case (force_* that succeeds grades normally), and the DETAILED_SKIP_TO_CANONICAL mapping. Spec source: compliance/cache/3.0.12/universal/runner-output-contract.yaml > skip_result.reasons.force_scenario_unsupported. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address expert review on #1812 Two parallel reviews (code-reviewer + ad-tech-protocol-expert) ran against the bundle. One real blocker + one info-level clarification addressed. #1804 test was mis-targeted (code-reviewer blocker) - The test called `client.getAgentInfo()` which routes through SingleAgentClient's inline A2A discovery branch at line 2755-2798 (already wrapped by PR #1802 for #1799), NOT through the newly- wrapped fetchA2ACanonicalUrl path. - fetchA2ACanonicalUrl is reached via ensureCanonicalUrlResolved, which the public resolveCanonicalUrl() method drives. - Test now calls client.resolveCanonicalUrl() in both cases, which is the only public surface that exercises the new wrap. Phase-cascade ordering noted in runner.ts comment (protocol-expert Q1) - The spec gate says "comply_test_controller advertised"; my detector gates on `step.task === 'comply_test_controller'`. The phase cascade at the seedingMissingController check (line ~1893) enforces "advertised" upstream — by the time per-step grading reaches the detector, the controller is already confirmed present. Added a comment block clarifying this so future maintainers don't read the detector in isolation and miss the upstream guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle of four Bucket-A triage closures from session 2026-05-17. Each commit is a self-contained close.
#1522 — AdCP 3.0.12 + comply_controller_mode_gate denial gate
Adopts the new universal storyboard from adcp#4028 as a graded path on
hello_seller_adapter_guaranteed. The bump unblocks the storyboard end-to-end against framework-gated sellers; the SDK changes close wire-level gaps surfaced during integration.ADCP_VERSION3.0.11 → 3.0.12; sync-schemas pulls the new universal storyboard + test-kit fixture.from-platform.ts). The denial storyboard assertscontext.correlation_idround-trips on refusal — previously the gate dropped them.rawMcpProbeparses Streamable-HTTP MCP SSE responses (probes.ts). SendsAccept: application/json, text/event-stream+ parses the SSEdata:line. Storyboards routed via the raw probe now grade against Streamable-HTTP MCP servers; previously the probe 406'd silently. Unit test added.adcp storyboard run --test-kit PATH(bin/adcp.js) — loads the test-kit YAML intooptions.test_kitsofrom_test_kit: true/$test_kit.*references resolve from the CLI.hello_seller_adapter_guaranteed.ts). Registersdemo-acme-outdoor-live-v1as a second bearer; resolver stampsmode: 'live'to trigger the gate.extraStoryboardsparameter onrunHelloAdapterGates.#1524 — mock-server-authority anchor
Replaces SDK-author "mock is the impartial referee" prose with a forward link to the spec's normative triage section (Mock-server authority and failure triage, landed in adcp#4030). Touches
docs/proposals/lifecycle-state-and-sandbox-authority.md,CLAUDE.md, andbin/adcp.js(mock-server help).#1797 — AssertionResult.status: 'silent' | 'pass' | 'fail'
status.monotonicandimpairment.coherencenow stampstatus: 'silent'on theonEndsummary record when zero lifecycle resources were observed, andstatus: 'pass'once at least one was. Downstream renderers (the spec-side grader at adcp#2834, Addie, CLI summary) read this per-assertion enum directly rather than inferring fromobservation_count === 0. Track-level rollup unchanged. Tests added for both invariants.#1799 —
getAgentInfo()honorstransport.maxResponseBytestransport.maxResponseByteswas dormant on the discovery path:mcpClient.listTools()andA2AClient.fromCardUrlbypassed the ALS slot, so a hostile vendor could return arbitrarily large discovery payloads against a size-capped client.getAgentInfonow wraps both call sites inwithResponseSizeLimit, and the A2A auth-stampingfetchImplis composed throughwrapFetchWithSizeLimit. Regression test added attest/unit/get-agent-info-size-limit.test.js.Test plan
npm run format:checkcleannpm run buildcleannode --test test/examples/hello-seller-adapter-guaranteed.test.js— 4/4 pass (denial gate green)node --test test/lib/storyboard-security.test.js— 23/23 suites pass, new SSE rawMcpProbe test greennode --test test/lib/storyboard-default-invariants.test.js— 146/146 passnode --test test/unit/a2a-card-size-limit.test.js test/unit/mcp-tool-size-limit.test.js test/unit/oauth-size-limit.test.js test/unit/response-size-limit.test.js test/unit/get-agent-info-size-limit.test.js— 32/32 passCloses #1522, #1524, #1797, #1799.
🤖 Generated with Claude Code