Skip to content

feat: AdCP 3.0.12 + denial gate + silent assertion status + size-limit wrap#1802

Merged
bokelley merged 7 commits into
mainfrom
bokelley/triage-open-issues-v3
May 17, 2026
Merged

feat: AdCP 3.0.12 + denial gate + silent assertion status + size-limit wrap#1802
bokelley merged 7 commits into
mainfrom
bokelley/triage-open-issues-v3

Conversation

@bokelley

@bokelley bokelley commented May 17, 2026

Copy link
Copy Markdown
Contributor

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_VERSION 3.0.11 → 3.0.12; sync-schemas pulls the new universal storyboard + test-kit fixture.
  • Framework gate 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.
  • rawMcpProbe parses Streamable-HTTP MCP SSE responses (probes.ts). Sends Accept: application/json, text/event-stream + parses the SSE data: 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 into options.test_kit so from_test_kit: true / $test_kit.* references resolve from the CLI.
  • Worked example wires the live-mode probe principal (hello_seller_adapter_guaranteed.ts). Registers demo-acme-outdoor-live-v1 as a second bearer; resolver stamps mode: 'live' to trigger the gate.
  • CI gate added via new extraStoryboards parameter on runHelloAdapterGates.

#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, and bin/adcp.js (mock-server help).

#1797 — AssertionResult.status: 'silent' | 'pass' | 'fail'

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 (the spec-side grader at adcp#2834, Addie, CLI summary) read this per-assertion enum directly rather than inferring from observation_count === 0. Track-level rollup unchanged. Tests added for both invariants.

#1799getAgentInfo() honors transport.maxResponseBytes

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. getAgentInfo now wraps both call sites in withResponseSizeLimit, and the A2A auth-stamping fetchImpl is composed through wrapFetchWithSizeLimit. Regression test added at test/unit/get-agent-info-size-limit.test.js.

Test plan

  • npm run format:check clean
  • npm run build clean
  • node --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 green
  • node --test test/lib/storyboard-default-invariants.test.js — 146/146 pass
  • node --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 pass
  • CI green

Closes #1522, #1524, #1797, #1799.

🤖 Generated with Claude Code

bokelley and others added 2 commits May 17, 2026 04:28
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>
@bokelley bokelley changed the title docs: link to upstream mock-server-authority anchor (adcp#4029, closes #1524) feat: AdCP 3.0.12 bump + denial-gate wiring (closes #1522), mock-server-authority docs (closes #1524) May 17, 2026
bokelley and others added 2 commits May 17, 2026 05:33
…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>
@bokelley bokelley changed the title feat: AdCP 3.0.12 bump + denial-gate wiring (closes #1522), mock-server-authority docs (closes #1524) Bucket-A triage: closes #1522 (AdCP 3.0.12 + denial gate), #1524 (mock-server-authority docs), #1797 (silent assertion status), #1799 (size-limit wrap) May 17, 2026
@bokelley bokelley changed the title Bucket-A triage: closes #1522 (AdCP 3.0.12 + denial gate), #1524 (mock-server-authority docs), #1797 (silent assertion status), #1799 (size-limit wrap) feat: AdCP 3.0.12 + denial gate + silent assertion status + size-limit wrap May 17, 2026
bokelley and others added 2 commits May 17, 2026 06:00
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>
@bokelley

Copy link
Copy Markdown
Contributor Author

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)

  • Comply gate context/ext echo cap. Open-object fields on the request schema could be stuffed with arbitrary payloads. Self-reflection only (no cross-tenant amplifier), but bounded at 8 KiB serialized to prevent the gate from being a self-amplifier (`from-platform.ts`).
  • Bearer JSDoc warning on the worked-example `demo-acme-outdoor-live-v1` token, callout that it's published in the open-source SDK and the public compliance test-kit and MUST NOT be copied into a production seller's keys map (`hello_seller_adapter_guaranteed.ts`).

Out-of-scope gaps filed as follow-ups

Reviewer-flagged but accepted as-is

  • `AssertionResult.status` is ahead of the spec (adcp#2834 is open). Field-name collision risk is low; flagging in case spec lands with a different shape.
  • `ctx.authInfo.token` is `@deprecated` in `ResolvedAuthInfo`; used in the worked example for principal detection. Comment explicitly directs production sellers to `credential.key_id` or a tenant-store lookup.

…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>
@bokelley bokelley merged commit 63c74fc into main May 17, 2026
10 checks passed
@bokelley bokelley deleted the bokelley/triage-open-issues-v3 branch May 17, 2026 10:35
bokelley added a commit that referenced this pull request May 17, 2026
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>
bokelley added a commit that referenced this pull request May 17, 2026
…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>
@github-actions github-actions Bot mentioned this pull request May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

track: wire denial-path conformance once adcp#4028 storyboard lands

1 participant