Skip to content

test(compliance): cover the absent measurement_terms_acceptance gate for measurement_terms_accepted - #8539

Open
sujanchalla0510 wants to merge 119 commits into
adcontextprotocol:mainfrom
sujanchalla0510:fix-7344-measurement-terms-gate-tests
Open

sujanchalla0510 wants to merge 119 commits into
adcontextprotocol:mainfrom
sujanchalla0510:fix-7344-measurement-terms-gate-tests

Conversation

@sujanchalla0510

@sujanchalla0510 sujanchalla0510 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Refs #7344. Add coverage for the absent measurement_terms_acceptance declaration when evaluating the measurement_terms_accepted storyboard gate.

The test covers both an injected profile with media_buy: {} and a wire-shaped capabilities response payload without that feature. It checks phase/step capability_unsupported skips and guards against dispatching dependent get_products or create_media_buy tasks.

The response-payload case obtains the fixture response explicitly, then supplies it through _profile.raw_capabilities. It tests gate evaluation. Production capability discovery and fetch-and-gate wiring remain separate work under #7344.

Validation on current main with the patch, in Docker using the repository's pinned @adcp/sdk 17.0.0-beta.8:

  • Full sdk-runner-capability-gates.test.cjs: 63 passed, zero failures or skips.
  • Normal commit hooks: all 1,276 root unit tests, additional import/format/state-change checks, and type checks passed. Vitest used its standard four-worker cap for this VM's concurrent workload; assertions and timeouts are unchanged.
  • Normal push checks passed, including version synchronization and changeset scope. No changeset is needed for this test-only diff.

The branch includes a normal merge with current main; fresh CI and final-head review remain required before merging.

bokelley and others added 7 commits October 22, 2026 08:17
…provenance truth-of-claim (adcontextprotocol#8218) (adcontextprotocol#8405)

Closes adcontextprotocol#8199. Points provenance_truth_of_claim and provenance_audit_observation at the public conformance verifier and the served fixture JPEGs, and adds depends_on [discover_verifier] to accept_consistent_claim.

Backport to 3.1.x: protocol content only. The storyboards, fixtures, docs
section and changeset apply cleanly. The training-agent route, CSRF
exemption, JSON body cap and their tests are dropped. Production serves the
verifier from the main deployment, 3.1.x is never deployed, and 3.1.x
predates the json-body-limit util and the restructured CSRF exemption lists
the route depends on. The 3.1.x seller's accepted_verifiers check is
URL-agnostic, so the storyboards run against the 3.1.x training agent
unchanged.

(cherry picked from commit fbd3543)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…1.x (adcontextprotocol#8462)

GitHub runs a push-triggered workflow from the pushed branch's own copy, so
adcontextprotocol#8439's fix to forward-merge-3.1.yml only takes effect once 3.1.x carries it.
Take main's copy: CHANGELOG.md keeps main's file plus the missing 3.1.x
release blocks, published-versions.json is unioned, and the schema discovery
pointers are regenerated after every merge. The workflow checks out main and
runs scripts/forward-merge-release-records.cjs from there.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…2.x (adcontextprotocol#8461)

GitHub runs a push-triggered workflow from the pushed branch's own copy, so
adcontextprotocol#8439's fix to forward-merge-3.2.yml only takes effect once 3.2.x carries it.
Take main's copy: CHANGELOG.md keeps main's file plus the missing 3.2.x
release blocks, published-versions.json is unioned, and the schema discovery
pointers are regenerated after every merge. The workflow checks out main and
runs scripts/forward-merge-release-records.cjs from there.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… erratum) (adcontextprotocol#8354)

The AAO reference catalog tagged display_html and display_WxH_html as
canonical html5. That contradicts canonical-projection-ref rule 5: these
formats require one html asset (html_creative), while the html5 slot is a
zip bundle. Re-tag them as display_tag with an html_creative (html) slot
override, matching display_js, and publish display_320x50_html and
display_300x50_html with the same mapping. Catalog data only; no schema
changes.

Refs adcontextprotocol#8339

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… on 3.2.x (adcontextprotocol#8501) (adcontextprotocol#8532)

* fix(schemas): reopen the proposal-flow schemas closed against DR-0019 on 3.2.x

3.2 published the proposal-flow tasks closed from their request and
response roots down, and closed the containers they use. DR-0019 keeps
request roots, response roots and container objects open, and allows
closure only on join-key objects or row envelopes with an ext slot.
DR-0036 on main records the reopening as a correction that ships in 3.3
and in a 3.2.x patch (adcontextprotocol#8501).

Set additionalProperties to true at the same 24 locations as main:
- the request roots of list_products, request_proposals,
  refine_proposals, buy_products, accept_proposal, decline_proposals and
  control_media_buy;
- the response roots of list_products, request_proposals,
  refine_proposals and decline_proposals, and the refine_proposals
  result row;
- the three branches of media-buy-commitment-response and of
  control-media-buy-response;
- canonical-proposal, canonical-product, product-discovery-criteria,
  proposal-refinement, package-control and opportunity-context.

Keep the rejections that closure provided by naming the keys: a root
not on canonical-product forbids the legacy format_ids, and the root not
of request-proposals-response now also forbids feed_version and
pricing_version. Each rejects exactly what 3.2.7 rejected.

Adapt the tests that assumed closure: the training agent parity test
compares closure with source, the MCP projection test ignores the
format_ids prohibition, and the products-only brief test also rejects a
fabricated pricing_version.

Add a correction note to the 3.1 to 3.2 migration guide and a patch
changeset. No fields are added, and commercial-terms stays closed.

* fix(schemas): name the keys closure rejected on reopened 3.2.x schemas

Reopening the proposal-flow schemas dropped three rejections that tests
rely on:

- canonical-product rejected every field that only the legacy Product
  defines, because its description keeps the legacy Product graph absent.
  Forbid each of those 21 keys by name, including format_ids, is_custom
  and installments.
- The error and submitted branches of media-buy-commitment-response and
  control-media-buy-response rejected warnings, which only the completed
  branch carries. Forbid warnings there by name, as create-media-buy and
  update-media-buy responses already do.

Derive the projection test's canonical Product prohibition from the
source schema, and check the dual-shape fixture by which schema rejects
it rather than by closure's error text. Update the changeset and the
migration note to list the named rejections.
@mintlify

mintlify Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit 940ab13.

Approve preview

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 28, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — test-only PR adding coverage for the measurement_terms_accepted capability gate (#7344).

Checked:

  • Single file touched: tests/sdk-runner-capability-gates.test.cjs (+57/-0), test-only. No protocol surface (static/schemas/source/**, docs/reference/**, dist artifacts, task defs) touched, so changeset correctly omitted.
  • No high-risk paths, gated_paths: false. review_decision is REVIEW_REQUIRED but the gated-paths gate (row 2) does not apply since gated_paths is false.
  • No critical/high findings.

Medium findings

  • tests/sdk-runner-capability-gates.test.cjs:1429 — "Live capability fetch" block gates on a pre-injected _profile.raw_capabilities, so the runner never calls _client.getAdcpCapabilities; the deepEqual(dispatchedTasks, ['get_adcp_capabilities']) only reflects the test's own manual call, leaving the production fetch-and-gate path uncovered.

Decision: 1 medium finding, no escalation trigger (no data-loss/schema/infra category, no high-risk modified file, no deleted sensitive file, no team gate, no prior escalation). Falls through to row 9 — approve. The uncovered-path concern is worth the author's attention but does not block.

Medium findings

  • tests/sdk-runner-capability-gates.test.cjs:1429 — "Live capability fetch" block gates on injected profile, not a runner fetch

@sujanchalla0510 sujanchalla0510 changed the title test(compliance): cover the absent measurement_terms_acceptance gate, including the live capability fetch test(compliance): cover the absent measurement_terms_acceptance gate for measurement_terms_accepted Oct 28, 2026
@sujanchalla0510

Copy link
Copy Markdown
Contributor Author

Thanks — you are right about the mechanism, and I verified it experimentally before rewording (head c6cb3f0):

  • With the pinned @adcp/sdk 17.0.0-beta.2, runStoryboard evaluates storyboard-level gates on _profile.raw_capabilities only. Omitting _profile (or its raw_capabilities) does not make it resolve capabilities via _client.getAdcpCapabilities: evaluateRequiresCapabilityGate returns null when raw capabilities are undefined, the gate is left unevaluated, and the storyboard dispatches get_products (reproduced both with _profile: {tools} and with no _profile at all). So the construction you suggest cannot produce capability_unsupported in this harness — the fetch-and-gate wiring lives in the hosted runner / adcp-client, which is the sibling-repo half of the Storyboard runner may not auto-skip measurement_terms_accepted step when capability not declared #7344 brief.
  • What this PR now claims, precisely: (1) the absent-key gate case via profile injection (triage item 1 — previously only explicit false was covered), and (2) the gate evaluating a real get_adcp_capabilities response payload (wire shape, context echo, media_buy present, key absent) with dispatch guards on get_products/create_media_buy. Test name, comments, PR title and description reworded accordingly; the code comment in the test now states the profile-only gate behavior explicitly so nobody re-claims the fetch path here.

Local re-run on c6cb3f0: the renamed test passes 1/1.

bokelley and others added 16 commits October 28, 2026 05:53
…97-62hf-6gxq (adcontextprotocol#8540)

The MCP Node transport 2.0.0 (pinned by @adcp/sdk) does not bound request
body size or JSON-RPC batch length (high). 2.1.1 fixes it, satisfies the
pinned @modelcontextprotocol/server 2.3.1 peer, and has the same
@hono/node-server dependency the existing override already covers.
npm audit --omit=dev --audit-level=high: 0 vulnerabilities.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tion is a task error (adcontextprotocol#8475)

* fix(webhooks): let the buyer choose the signing mode, and make the HMAC rejection a task error

The buyer's webhook registration selects the signing mode again: the
seller signs with the profile signing_profile names, and an
authentication block asks for legacy signing. legacy_hmac_fallback is
a discovery flag, and no onboarding record selects the mode.

A seller that does not support legacy signing rejects the registration
with the existing UNSUPPORTED_FEATURE task error. Remove the unreleased
webhook_hmac_not_supported from the request-signing enum, which holds
the WWW-Authenticate challenge codes.

* docs(webhooks): tighten the signing-mode notes and changeset

Rename the migration-guide link to the narrowing that remains, list condition (b) under Deprecations, and restore the rule that a deprecation window does not reset. Drop removed-code history from the changeset. Rewrite the security page sentences in present tense and under 26 words.

* docs(webhooks): drop restated and rationale-only signing-mode text

Delete the "Condition (b) is deprecated" paragraph, which restated the
taxonomy row and the downgrade bullet, and fold its reason into that
bullet. Delete the rejected-alternative rationale from the security
page and the changeset, and the per-counterparty seller SHOULD in the
HMAC to 9421 migration note, which no longer fits a buyer-selected
mode. Split two long sentences.

* docs(webhooks): split long signing-mode sentences and align the rejection wording

Cut the reporting and artifact webhook paragraph in security.mdx and
the matching changeset paragraph to one sentence. Split the long
signing-mode sentences in security.mdx and the migration overview, and
name the condition (b) deprecation in the overview.

The push-notification-config and agent-notification-config
authentication descriptions now say the rejection points error.field
at the block, matching notification-config.
…and collections (adcontextprotocol#6135) (adcontextprotocol#7948)

* feat(schema): an empty product roster means undisclosed, for properties and collections

A non-empty publisher_properties or collections is the complete roster; an
empty list means the seller does not disclose it. No new field and no partial
state. publisher_properties and collections lose minItems: 1. Sellers serving a
release below 3.3 do not return an empty roster. Allow lists may be accepted
and run on matches; exclusion lists may be accepted if applied against the
seller's full internal roster, otherwise rejected with UNSUPPORTED_FEATURE.

Refs adcontextprotocol#6135

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(schema): an empty product collections array is an undisclosed roster, not invalid

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(schema): keep targeting-input list descriptions in step with targeting.json

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…ccount_id (adcontextprotocol#8474)

* spec(accounts): make brand identity the domain and account identity the account_id

A brand is its domain, plus brand_id where its brand.json lists several
brands. An account is the seller-issued account_id. Countries, currency,
timezone, sandbox, and operator_unit become account attributes that a
settings update changes on the same account.

A provisioning entry without account opens an account, and
idempotency_key covers retries. A natural-key reference resolves only
when exactly one account matches, otherwise ACCOUNT_AMBIGUOUS.

Deprecate account.identity_updates, operator_identity reconciliation,
ACCOUNT_MOVED, ACCOUNT_IDENTITY_CONFLICT, and account.stable_account_id.
No wire field is removed.

- Define how a natural-key filter matches: each supplied attribute equals
  the account's value, countries as a set and operator_unit by id.
- Count an account as included for delete_missing only when an entry
  names it or the seller returns its account_id.
- A seller that opens buyer-declared accounts through sync_accounts
  accepts settings-update entries keyed by account_id, and declines an
  attribute change it cannot make with UNSUPPORTED_FEATURE.
- A provisioning entry that the account scope maps to an existing
  account returns that account_id. Only the 3.2 exact-or-reject rules
  for billing and payment_terms still apply.
- currency, timezone, and sandbox are set at opening. A seller can
  review a countries change with status "pending_approval".
- Training agent: return UNSUPPORTED_FEATURE for a settings-update
  attribute change and INVALID_REQUEST for an operator.

Refs adcontextprotocol#8280, adcontextprotocol#8442. Supersedes adcontextprotocol#8457.

* spec(accounts): align with merged request_account

Bring the account_id model in line with adcontextprotocol#8293 as merged:

- account_id is unconditional and returned once the account is
  resolved. A seller never issues one it might later change, so a
  pending_approval row that may resolve to a different account carries
  request_id instead. Drop the remaining stable_account_id-conditional
  wording from the request_account changeset and the stable_account_id
  storyboard narrative.
- Provisioning-entry matching references the request_account
  deduplication guidance: brand and operator, attributes as filters,
  billing terms never filter.
- ACCOUNT_AMBIGUOUS lists matches in error.details.candidates, per
  error-details/account-ambiguous.json, instead of available_accounts.
- request_account currency and timezone are account attributes set
  when the account opens, not immutable.

---------

Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ntextprotocol#8546)

Fenced badge revocation and degradation failed with PostgreSQL syntax errors because the generation predicates were missing closing parentheses. Close both predicates while preserving the independent generation, grading revision, selected profile, and source-run guards. Add the nullable `history_start` column required by the installed reporting storage package without changing existing history boundaries.

Include adapter-owned failure kind/HTTP status and nested PostgreSQL SQLSTATE in operational alerts, retaining isolated-execution logging and provider-payload redaction. Refresh the client's generated registration fingerprint.

Run the badge mutation regressions in a private schema within the existing CI test database, and verify SDK-style reporting configuration inserts against the migrated table under rollback. The destructive GCS end-to-end suite retains its dedicated database requirement.

Validation:

- Correctness, security, and Node.js testing expert reviews; addressed the silently skipped badge regression suite.
- Full repository pre-commit checks (root/server unit suites, static guards, and TypeScript checks).
- Badge and reporting migration regressions plus the dedicated GCS end-to-end suite against Docker PostgreSQL 16.
- Generated Addie tool artifact validation and inventory portability check.
- Migration validation and production dependency audit.

The underlying Google provider failures and intermittent reporting notification recovery failures remain unconfirmed; the added diagnostics make subsequent incidents actionable. This PR fixes the reproduced SQL and migration defects. No protocol changeset is needed.
…textprotocol#8550)

* fix(compliance): run shared seller checks on auction inventory

* test(compliance): cover maintenance bundles and run auction regressions in CI

* fix(compliance): pin the published runner and preserve stale-cache scope

* test: isolate unit checks from repository indexing

* docs(sdk): align the prerelease guide with the published runner

* test(sdk): advance unchanged response evidence to beta.3
…tprotocol#8554)

Backport adcontextprotocol#8550 with native 3.1 account and FormatId contracts. Use the modern runner only in the private development fixture; leave production dependencies unchanged.

(cherry picked from commit cdf4c7f)
…tprotocol#8555)

Backport adcontextprotocol#8550 with native 3.2 account and format contracts. Use the modern runner only in the private development fixture; leave production dependencies unchanged.

(cherry picked from commit cdf4c7f)
…0019 (adcontextprotocol#8531)

* fix(schemas): reopen the 3.2 proposal-flow schemas closed against DR-0019

3.2 published the proposal-flow tasks closed from their request and
response roots down. DR-0019 keeps response roots, capability blocks and
container objects open, and allows closure only on class A or class B
rows. No record explains the closure. Reopen these locations as
correction (a), which DR-0036 records:

- the request roots of list_products, request_proposals,
  refine_proposals, buy_products, accept_proposal, decline_proposals and
  control_media_buy;
- the response roots of list_products, request_proposals,
  refine_proposals and decline_proposals, and the refine_proposals
  result row;
- the three branches of media-buy-commitment-response (buy_products and
  accept_proposal) and of control-media-buy-response;
- canonical-proposal, canonical-product, product-discovery-criteria,
  proposal-refinement, package-control and opportunity-context.

Each change sets additionalProperties to true at the listed pointer and
nothing else. commercial-terms and the purchases inside it stay closed.

List the 24 locations in scripts/lint-additive-minor-corrections.json,
each with property "*", release 3.3.0, and the DR-0019 sentence that
required it. The baseline shrinks from 70 to 41 entries: the 29 removed
entries are 3.3 fields on reopened schemas. definition_pins on
commercial-terms and installments and rights on product-purchase still
need their entries. drifted_pins, installment_changes and
unsatisfied_installment_changes also wait for 4.0, but sit on reopened
schemas. A later PR removes all of them.

The adcp_version echo now validates on the control_media_buy,
decline_proposals, buy_products and accept_proposal responses (adcontextprotocol#8518).

The training agent derives its discovery tool schemas from source, so
its parity test now compares their closure with source instead of
expecting false.

Add a correction note to the 3.2 to 3.3 migration guide and the
versioning page, and a patch changeset.

* fix(schemas): keep rejecting the legacy keys the closed proposal flow rejected

Closure did two jobs on two reopened schemas. It rejected the legacy
format_ids on a canonical Product, and a fabricated root feed_version on
a request_proposals response. Tests assert both rejections. DR-0019
requires a loosening to name the validation that replaces what closure
provided.

Forbid both keys by name: a root not on canonical-product, and one more
entry in the existing root not of request-proposals-response. Each
rejects exactly what 3.2 rejected, so no payload changes validity
against 3.2.

The MCP projection test checks that the list_products output never
mentions format_ids. It now ignores the prohibition itself, which
forbids the key rather than exposing it.

* fix(schemas): keep rejecting a fabricated pricing_version on request_proposals

The version-adaptation guide says a compatibility projection never
synthesizes feed_version or pricing_version. The closed 3.2
request_proposals response enforced both at its root. The reopening kept
feed_version forbidden by name but not pricing_version, so a projection
that fabricates a root pricing fence now validates.

Add pricing_version to the root not of request-proposals-response, and
assert in the products-only brief test that a fabricated pricing fence
fails, next to the feed fence check.

* docs(media-buy): say the 3.2 discovery requests reject unknown fields

The include_collections descriptions and docs said the list_products,
request_proposals and refine_proposals requests reject unknown fields.
After the reopening only their 3.2 schemas do. Say so; the advice to
send the flag only to sellers that declare media_buy.collection_cards
stands.

The pricing-models page listed the proposal among the closed schemas
that carry accepted terms. The proposal is reopened; the commercial
terms that carry them stay closed.

* test(schemas): require the corrections list to match DR-0036's tables

The check already fails on an entry that DR-0036's correction tables
don't list. Now that correction (a) is listed, check the other direction
too: every location in the tables is in the corrections list. Correction
(b) has no rows yet; adcontextprotocol#8523 and adcontextprotocol#8524 add its rows and entries together.

* fix(schemas): name the keys closure rejected on the reopened schemas

Reopening the proposal-flow schemas dropped rejections that tests rely
on:

- canonical-product rejected every field that only the legacy Product
  defines, because its description keeps the legacy Product graph absent.
  Forbid each of those 19 keys by name, including format_ids, is_custom
  and product_card.
- The error and submitted branches of media-buy-commitment-response and
  control-media-buy-response rejected warnings, which only the completed
  branch carries. Forbid warnings there by name, as create-media-buy and
  update-media-buy responses already do.

Derive the projection test's canonical Product prohibition from the
source schema. The include_collections test now expects every discovery
request to accept unknown fields, and the three proposal-flow flags to
warn that their 3.2 schemas reject it. Update the changeset and the
migration note to list the named rejections.

---------

Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
…auction-only-compliance

# Conflicts:
#	package-lock.json
#	package.json
#	static/compliance/source/protocols/media-buy/scenarios/measurement_terms_rejected.yaml
#	static/compliance/source/specialisms/sales-broadcast-tv/index.yaml
#	static/compliance/source/universal/stale-response-advisory.yaml
#	static/schemas/source/media-buy/decline-proposals-request.json
#	static/schemas/source/media-buy/product-discovery-criteria.json
#	tests/auction-only-seller-compliance.test.cjs
…uction-only-compliance

# Conflicts:
#	.changeset/reopen-proposal-flow-schemas.md
…auction-only-compliance

# Conflicts:
#	CHANGELOG.md
#	dist/schemas/index.json
#	dist/schemas/latest.json
#	package-lock.json
#	package.json
#	server/src/creative-agent/reference-formats.json
#	server/src/creative-agent/task-handlers.ts
#	server/tests/unit/creative-agent.test.ts
#	static/compliance/source/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml
#	static/compliance/source/protocols/media-buy/scenarios/measurement_terms_rejected.yaml
#	static/compliance/source/universal/stale-response-advisory.yaml
#	static/schemas/source/index.json
#	tests/auction-only-seller-compliance.test.cjs
#	tests/sdk-runner/README.md
#	tests/sdk-runner/package.json
#	tests/sdk-runner/testing.cjs
KonstantinMirin and others added 27 commits October 30, 2026 08:20
…dcontextprotocol#8518) (adcontextprotocol#8547)

* fix(schemas): reopen the 3.2 proposal-flow schemas closed against DR-0019

3.2 published the proposal-flow tasks closed from their request and
response roots down. DR-0019 keeps response roots, capability blocks and
container objects open, and allows closure only on class A or class B
rows. No record explains the closure. Reopen these locations as
correction (a), which DR-0036 records:

- the request roots of list_products, request_proposals,
  refine_proposals, buy_products, accept_proposal, decline_proposals and
  control_media_buy;
- the response roots of list_products, request_proposals,
  refine_proposals and decline_proposals, and the refine_proposals
  result row;
- the three branches of media-buy-commitment-response (buy_products and
  accept_proposal) and of control-media-buy-response;
- canonical-proposal, canonical-product, product-discovery-criteria,
  proposal-refinement, package-control and opportunity-context.

Each change sets additionalProperties to true at the listed pointer and
nothing else. commercial-terms and the purchases inside it stay closed.

List the 24 locations in scripts/lint-additive-minor-corrections.json,
each with property "*", release 3.3.0, and the DR-0019 sentence that
required it. The baseline shrinks from 70 to 41 entries: the 29 removed
entries are 3.3 fields on reopened schemas. definition_pins on
commercial-terms and installments and rights on product-purchase still
need their entries. drifted_pins, installment_changes and
unsatisfied_installment_changes also wait for 4.0, but sit on reopened
schemas. A later PR removes all of them.

The adcp_version echo now validates on the control_media_buy,
decline_proposals, buy_products and accept_proposal responses (adcontextprotocol#8518).

The training agent derives its discovery tool schemas from source, so
its parity test now compares their closure with source instead of
expecting false.

Add a correction note to the 3.2 to 3.3 migration guide and the
versioning page, and a patch changeset.

* fix(schemas): keep rejecting the legacy keys the closed proposal flow rejected

Closure did two jobs on two reopened schemas. It rejected the legacy
format_ids on a canonical Product, and a fabricated root feed_version on
a request_proposals response. Tests assert both rejections. DR-0019
requires a loosening to name the validation that replaces what closure
provided.

Forbid both keys by name: a root not on canonical-product, and one more
entry in the existing root not of request-proposals-response. Each
rejects exactly what 3.2 rejected, so no payload changes validity
against 3.2.

The MCP projection test checks that the list_products output never
mentions format_ids. It now ignores the prohibition itself, which
forbids the key rather than exposing it.

* fix(schemas): keep rejecting a fabricated pricing_version on request_proposals

The version-adaptation guide says a compatibility projection never
synthesizes feed_version or pricing_version. The closed 3.2
request_proposals response enforced both at its root. The reopening kept
feed_version forbidden by name but not pricing_version, so a projection
that fabricates a root pricing fence now validates.

Add pricing_version to the root not of request-proposals-response, and
assert in the products-only brief test that a fabricated pricing fence
fails, next to the feed fence check.

* docs(media-buy): say the 3.2 discovery requests reject unknown fields

The include_collections descriptions and docs said the list_products,
request_proposals and refine_proposals requests reject unknown fields.
After the reopening only their 3.2 schemas do. Say so; the advice to
send the flag only to sellers that declare media_buy.collection_cards
stands.

The pricing-models page listed the proposal among the closed schemas
that carry accepted terms. The proposal is reopened; the commercial
terms that carry them stay closed.

* test(schemas): require the corrections list to match DR-0036's tables

The check already fails on an entry that DR-0036's correction tables
don't list. Now that correction (a) is listed, check the other direction
too: every location in the tables is in the corrections list. Correction
(b) has no rows yet; adcontextprotocol#8523 and adcontextprotocol#8524 add its rows and entries together.

* fix(schemas): name the keys closure rejected on the reopened schemas

Reopening the proposal-flow schemas dropped rejections that tests rely
on:

- canonical-product rejected every field that only the legacy Product
  defines, because its description keeps the legacy Product graph absent.
  Forbid each of those 19 keys by name, including format_ids, is_custom
  and product_card.
- The error and submitted branches of media-buy-commitment-response and
  control-media-buy-response rejected warnings, which only the completed
  branch carries. Forbid warnings there by name, as create-media-buy and
  update-media-buy responses already do.

Derive the projection test's canonical Product prohibition from the
source schema. The include_collections test now expects every discovery
request to accept unknown fields, and the three proposal-flow flags to
warn that their 3.2 schemas reject it. Update the changeset and the
migration note to list the named rejections.

* build(schemas): compose the protocol envelope into closed task responses

Draft-07 additionalProperties does not see properties declared through
allOf, so a closed response root or oneOf/anyOf branch rejects every
envelope field even when the response composes core/protocol-envelope.json.

Add scripts/compose-response-envelope.cjs. For each schema that lists the
protocol envelope in its root allOf, it declares every envelope property
on each closed root and inline allOf/anyOf/oneOf member as a $ref to the
property's declaration. The property list comes from the envelope and the
schemas it composes. Only missing keys are added, so a local declaration
that narrows an envelope field is kept. The documentary payload property
is left out.

The flat tree, the bundled tree and the MCP projection read every source
document through the transform. The additive-minor check reads its source
candidate the same way, and counts names declared through nested allOf,
so it compares what a release publishes.

* fix(schemas): compose the protocol envelope on every task response

The envelope notes say task response schemas define only body fields and
the envelope supplies the rest, but 47 of the 124 tool response and async
status schemas did not compose core/protocol-envelope.json, and adcp_version
and adcp_major_version reached responses only through a direct
core/version-envelope.json composition or a hand-copied $ref.

- core/protocol-envelope.json composes core/version-envelope.json, so the
  served version fields are envelope fields on every response. Both
  descriptions now say so; the version envelope is part of the payload only
  on a request.
- Every tool response and async status schema, and the shared compact
  components, compose the protocol envelope through the root allOf.
  Response schemas no longer compose the version envelope directly.
- The closed compact schemas drop their hand-copied replayed and
  adcp_version $refs; the schema build declares envelope fields on closed
  roots and branches. Narrowing declarations stay.
- decline_proposals let its completed branch carry status completed, as
  refine_proposals and request_proposals do, instead of forbidding status.
- Governance responses, async status payloads and webhook result examples
  carry the envelope status the envelope already required.

Validation tests and doc-example checks read the published composition;
the collection-copies and products-only compliance vectors, test
fixtures and examples carry status; list_products adcp_version leaves the
3.3 x-added-in sweep, because the envelope supplies it;
and the MCP projection cap moves to 23 MiB.

* test(schemas): check envelope composition on every tool response

Enumerate every tool's response and async status schemas from
discoverTools and assert that each composes core/protocol-envelope.json
through its root allOf, and that the built schema accepts every envelope
property on every closed root and oneOf/anyOf branch.

Each closed node is compiled on its own, so every branch is evaluated. A
vector adds one envelope property, with a value its envelope declaration
and any local narrowing accept, to a control instance without it. The
built node must raise no new error; the source node must raise exactly one,
its additionalProperties rejecting that property, unless the source
declares the property itself. The test also checks that source schemas
carry no hand-copied envelope $refs and that the build keeps narrowing
declarations unchanged.

Run it from npm test as test:response-envelope-composition.

* test(schemas): use an opaque governance_context in the envelope vectors

The envelope value table used a JWT-shaped literal for governance_context,
which secret scanning reports as a hardcoded token. The schema only requires
printable ASCII, so an opaque compact-JWS-shaped string grades the same
property without resembling a credential.

* fix(training-agent): keep task outcomes out of the envelope status from 3.1

Every task response now composes the protocol envelope, so a top-level
status must be a task status. Three handlers still put a domain outcome
there: check_governance (the verdict), report_plan_outcome (the outcome
state) and creative_approval (the approval decision). 3.1 renamed those to
verdict, outcome_state and approval_status.

From 3.1 on, the handlers send status: completed with the outcome only in
its renamed field. A served 3.0 release keeps the outcome in status, as
the 3.0 schemas define it. The served release comes from the SDK request
context on the governance tenant, from the dispatcher on the legacy route,
and from the negotiated release on the creative_approval custom tool, which
now declares adcp_version and adcp_major_version so the pin reaches the SDK.
Stored report_plan_outcome responses stay release-neutral and replays are
projected the same way.

The renames landed in adcontextprotocol#4897. Fixes adcontextprotocol#8553.

* build(schemas): follow nested allOf when a property moves into a composition

A response that composes core/protocol-envelope.json declares adcp_version
through the envelope's own allOf of core/version-envelope.json. The
additive-minor check followed only one level of allOf when it decided that
a property had moved rather than been removed, so an open response root
that now takes adcp_version from the envelope read as removing it.
compositionDeclares now resolves allOf entries the way declaredNames does.

* fix(schemas): let the envelope govern replayed and message on compact responses

The compact proposal-flow responses pinned replayed to const true, and the
compact branches and core/compact-task-* payloads capped message at 2,000
characters. Both narrowed envelope fields without a reason. The envelope's
replayed is a boolean that defaults to false, so a seller that always sends
replayed: false conforms, and the envelope's message has no bound in 3.x
(x-max-length-4.0 is 1024 from 4.0). The schemas rejected conforming
responses.

The local declarations are removed, so the envelope's declarations govern.
The composition test adds edge-value vectors (replayed: false and a
2,500-character message) to every closed root and branch and to the compact
task payloads, and checks that no response node pins replayed.

* docs(governance): record envelope composition as DR-0036 correction (b)

Correction (b) recorded the adcp_version echo on closed task responses,
pointing at adcontextprotocol#8523 and adcontextprotocol#8524. adcontextprotocol#8547 supersedes both by composing the whole
protocol envelope into every task response. (b) now names that change and
quotes the text that already required it: the REQUIRED status and the
body-fields-only note in core/protocol-envelope.json as published in 3.2.7,
and the adcp_version echo in docs/reference/versioning.mdx.

Every closed location that gains an envelope field is a proposal-flow
response root or branch that correction (a) reopens, so (b) adds no check
entries.

* chore(changeset): note the compact replayed and message normalization

* fix(schemas): let the envelope govern message on every response node

Sixteen open async submitted roots and branches still capped message at
2,000 characters: build_creative, create_media_buy, update_media_buy,
get_creative_features, get_products, get_signals, preview_creative,
sync_audiences, sync_catalogs and sync_creatives. The envelope defines
message once, with no bound in 3.x, so the cap rejected conforming
responses. The local maxLength is removed; descriptions stay.

The composition test now walks every response node, open or closed: each
must accept replayed: false and a 2,500-character message, and none may
narrow message length or pin replayed. A const pin on message stays
allowed, as on the unavailable get_reporting_status lookup. The changeset
gives the final count: replayed at 10 locations, message at 23.

* build(schemas): validate responses against the built schema tree only

Response validators read raw source schemas, sometimes with the envelope
composition applied at runtime. A closed source response node puts status in
required through the envelope allOf and rejects it through
additionalProperties, so raw source accepts no conforming response, and the
training agent loaded scripts/compose-response-envelope.cjs at runtime,
which the production image does not ship.

Every response validator now reads dist/schemas/latest, the tree the build
publishes and the runtime image ships:
- scripts/built-schemas.cjs builds the tree when its inputs changed (a
  digest stamp the build writes) and reads it with /schemas/ ids. Builds
  take a lock, so concurrent test processes and npm run build:schemas
  build one at a time.
- The training agent's validator (source-schema.ts) compiles the built
  tree; the second, composing validator is gone, and so is the injected
  status: a response without status now fails. The server unit setup
  builds the tree first.
- The storyboard sample_response lint and the tests that validated
  response instances against source now read the built tree.

* fix(schemas): declare envelope fields only on the envelope

Response schemas still restated envelope fields inline: task_id at 23
nodes, message at 21, replayed at 11, governance_context on
check_governance, status and task_id on get_task_status, and adcp_version,
adcp_major_version, context_id, timestamp and adcp_error on the
get_reporting_status failure branches. Some copies disagreed with the
envelope. They are removed, so the envelope's declarations govern. A local
declaration stays only where it narrows: a status const or enum, a const
pin (the fixed message and adcp_error of an unavailable reporting lookup),
or context as a $ref to core/context.json.

Two constraints carried only by copies move to the envelope: minLength 1
on task_id, and the untrusted-input handling of message. The envelope's
replayed text now says that an absent replayed means false and that a
response may carry replayed: false. The root status enums of
decline_proposals, request_proposals and refine_proposals are removed;
their branch consts discriminate.

The composition test flags any other local redeclaration, not only $ref
copies. Its single-fault vectors now start from a control that passes the
built node: the node's required fields, with envelope values its
declarations accept. An addition vector adds exactly one error on source
and none on built; a removal vector of a required envelope field adds
exactly the missing-required error on built and removes exactly the
field's additionalProperties error on source. The test refuses to run
below Node 24 and checks that every vector ran.

The changeset gives the corrected count: replayed was pinned at 12
locations, so the normalization covers 35.

* refactor(training-agent): one table for outcomes renamed out of status

Three handlers each decided whether to put their outcome in status for a
served 3.0, and acquire_rights did not, so a served 3.0 got status:
completed where the 3.0 schema requires acquired, pending_approval or
rejected.

served-outcome-status.ts holds the one table of the 3.1 renames found by
comparing the 3.0.28 and 3.1.29 schemas: acquire_rights rights_status,
check_governance verdict, creative_approval approval_status and
report_plan_outcome outcome_state. (The create_media_buy and
update_media_buy media_buy_status split is already handled by the SDK for
a served 3.0.) Handlers emit the 3.1 shape; the transport adapters (the
governance and brand platforms, custom tools and the legacy dispatcher)
project it onto a served 3.0 before the SDK shapes the response. Caches
keep the release-neutral result.

Custom tools now always declare adcp_version and adcp_major_version and
forward the served release to their handlers, instead of the per-tool
opt-in. servesOutcomeInEnvelopeStatus uses the shared
parseAdcpReleaseVersion, moved to types.ts. The smoke test adds
report_plan_outcome at a 3.0 pin with its replay, and acquire_rights at
the current release and at 3.0.

* docs: carry status in list examples and complete the envelope field list

The list_products and list_proposals response examples now carry status,
which every task response requires. The MCP guide's envelope field list
adds message, push_notification_config, adcp_version and
adcp_major_version. The versioning page no longer says adcp_version rides
on the payload: it is an envelope field on a response.

* fix(training-agent): project renamed outcomes on the MCP dispatcher too

The legacy route has a second dispatcher, the in-process MCP server, that
the outcome table did not reach, so an unpinned check_governance (served
3.0) answered status: completed. It now projects fresh and replayed
results the same way. A served 3.0 response keeps the 3.1 field next to
status: the 3.0 schemas are open and in-process readers use it.

* fix(schemas): open the remaining response roots and drop the envelope build step

Response schemas compose core/protocol-envelope.json through their root
allOf. A closed response root or status branch rejects the fields that
allOf composes, so the build used to merge the envelope's properties into
each closed node, which made source and published schemas differ. DR-0019
keeps response roots open, so the closed ones open instead:
- list_collections, list_proposals, get_media_buy_issue and
  report_media_buy_issue, new in 3.3;
- the get_reporting_status failure branches, experimental in 3.2.7.
The 3.2 proposal-flow response roots and branches are reopened by DR-0036
correction (a) (adcontextprotocol#8531). Where closure carried a branch exclusion, the
exclusion is now explicit: a found or recorded issue carries no errors and a
failed one no issue, a failed reporting read carries no ledger result, and
list_collections carries no products. Closed value objects and rows inside
response bodies stay closed.

scripts/compose-response-envelope.cjs and its callers in the build, the MCP
projection and the additive-minor lint are removed; the lint keeps following
nested allOf. Source and published schemas agree with no generator, and
validators keep reading the one published tree.

The composition test now requires that no response root or status branch is
closed, and runs acceptance vectors on every response node: a control that
passes the node conjoined with the envelope, and the control plus each
envelope field, including replayed: false and a 2,500-character message. It
keeps the inline-redeclaration rule.

The task_id minLength that the compact copies carried is dropped rather than
moved: on the envelope it would tighten every 3.2 response, which the
additive-minor guard rejects against 3.2.7. DR-0036 correction (b) now
records the contract reason only, with no check entries.

* test(schemas): restate why the MCP projection cap is 23 MiB

Without the build-time merge into closed nodes, the projection's growth
comes from every response bundling the envelope: 23,426,967 bytes, still
over 22 MiB.

* build(schemas): record bound-tightening corrections in the additive-minor check

DR-0036 corrections could only let a closed schema accept more. A schema
can also contradict its text the other way, by accepting a value the
published text already excludes, such as an empty identifier. A correction
entry may now name a bound and its value instead of a property
({file, pointer, bound, value}). The check accepts exactly that bound at
that location from the entry's release on, requires the candidate to carry
it there (through a $ref to a shared definition), and requires the entry in
the decision record's tables like any other correction. Any other
tightening, or another value, still fails.

Tests cover a recorded tightening, an unrecorded one, a different value or
bound next to a recorded one, an unapplied entry, and an entry the record
does not list.

* fix(compliance): resolve context_outputs paths through a root's allOf and arms together

The context_outputs path lint read only the first of oneOf, anyOf and
allOf on a node, and when expected_arm selected a branch it dropped the
root's allOf. A response root composes the protocol envelope through allOf
and lists its arms in oneOf, so a capture of task_id from a submitted
response stopped resolving once the arms no longer restated task_id. The
lint now walks all three keywords and checks a selected arm together with
the root's allOf.

* fix(schemas): give task_id one non-empty definition, required on submitted through the envelope

A task_id is the handle a caller polls and a webhook references, yet most
task_id declarations accepted an empty string, and adcontextprotocol#8547 had dropped the
minLength: 1 that 3.2.7 published on core/compact-task-submitted.json.

- core/task-id.json is the one definition: type string, minLength 1,
  x-entity task, x-max-length-4.0 128. Every task_id declaration refers to
  it: the protocol envelope, tasks/get and get_task_status requests and
  responses, the tasks/list and list_tasks responses and task_ids filters,
  the MCP webhook payload, and the compliance controller's request and
  response, which keep their own maxLength 128.
- The envelope requires task_id when status is submitted (if/then in its
  allOf). The 19 submitted roots and branches no longer list task_id
  themselves; completed branches keep not: {required: [task_id]}.
- DR-0036 records the bound as correction (c), "task_id identifies a
  task", at the nine locations the additive-minor check reports, with
  matching entries in scripts/lint-additive-minor-corrections.json. The
  baseline is not used.

The composition test checks that task_id: "" adds exactly its minLength
error on every response node and on every request that carries a task_id,
and that a submitted response without task_id fails on the envelope rule
alone. Structural tests and the MCP schema analysis counts follow the
shared definition.

* docs(schemas): describe open task response roots in the envelope notes

The envelope notes still described the deleted build step that merged
envelope fields into closed schemas, and allowed any local narrowing of
an envelope field. Response roots and status branches are now open, and
a task schema restates no envelope field other than a status
discriminator, an identical context reference or a fixed-value pin.

* fix(schemas): require task_id on submitted responses through core/task-submitted.json

The envelope required task_id through an if/then keyed on status.
Code generators ignore conditionals, so generated types left task_id
optional even after a caller narrowed on status: submitted.

core/task-submitted.json is now the one place the rule lives: status
const submitted and a required task_id (core/task-id.json). The envelope
loses its if/then; its task_id description and notes point at the new
schema. Every submitted body and branch composes it through allOf:
core/compact-task-submitted.json, the eight *-async-response-submitted.json
bodies that do not go through it, and the ten inline submitted oneOf
branches. They no longer restate status: submitted or a required list,
except control_media_buy and the buy_products/accept_proposal commitment,
whose OpenAPI discriminator on status needs the local const. Completed
branches keep their not: {required: [task_id]} exclusions.

The oneOf audit fuses a variant's allOf members before classifying it, as
code generators do, so the submitted branches stay discriminated; its
baseline is refreshed with no new entries (get_content_standards improves
from dangerous to narrowable).

The composition test requires every submitted body and branch to compose
core/task-submitted.json, forbids an if/then that keys task_id on status,
and checks that a submitted response without task_id fails on
core/task-submitted.json's required alone. DR-0036 correction (c) now
names core/task-id.json (non-empty) and core/task-submitted.json
(required on submitted).

* build(schemas): compare composed required sets in the additive-minor check

required-added compared only a node's own required list, so a name
required through a new allOf entry was invisible. Composing the protocol
envelope made status required on 35 responses and async status payloads
while the check reported 0 new findings.

The check now compares the composed required set, a node's own names plus
those of its allOf entries through $refs into composed documents, against
the base's composed set. A correction may name a required name
({file, pointer, required}); the check accepts exactly that name at that
location and requires the candidate's composed set to contain it. A
regression test covers a name required through a new allOf entry, a
recorded correction, an unrecorded name next to it, and an unapplied entry.

DR-0036 correction (b) records the 35 status additions, quoting 3.2.7's
"The status field is REQUIRED on every task response envelope", with
matching entries in scripts/lint-additive-minor-corrections.json. The
baseline is not used.

* fix(schemas): pin status on working and input-required payloads

Add core/task-working.json and core/task-input-required.json, mirroring
core/task-submitted.json. Every working and input-required async body,
and the compact working and input-required responses, compose the
schema for their status through allOf, so a payload accepts only its own
status. The composition test requires every status body to compose its
core/task-<status>.json. The MCP webhook example that carried a
create_media_buy result in a working payload now carries a complete
result.

* fix(schemas): bound task-level message at 2,000 in core/task-message.json

Give a task-level message one definition, core/task-message.json
(type string, maxLength 2000, x-max-length-4.0 1024, untrusted input to
the receiver). The protocol envelope, the MCP webhook payload and the
response wrapper refer to it, so the bound does not depend on the
delivery path. A seller with longer text truncates it rather than fail
the task. No task schema restates the bound.

DR-0036 records the change as correction (d), at the 2,000 that 3.2.7
published on the compact and submitted responses, with one check entry
per location the additive-minor check flags. The composition test
rejects a 2,001-character message on every response node and on both
other carriers with exactly the maxLength error, and accepts 2,000.

* fix(schemas): drop the 4.0 annotation from the enforced message bound

core/task-message.json enforces maxLength 2000, so an x-max-length-4.0
of 1024 annotated a limit below the keyword that enforces it. The
annotation is for fields without an enforced bound; remove it and the
4.0 wording from the definition, DR-0036 (d) and the changeset. Any
4.0 tightening of message is a separate decision.

The capabilities worst case grows from 91,262,597 to 91,266,501 bytes
(+3,904). The capabilities response composes the envelope, whose
message used to count at the 4.0 annotation of 1,024 characters and now
counts at the enforced 2,000: 976 more characters of free text at four
bytes each. security.mdx and the pinned figure in
tests/capabilities-structural-bounds.test.cjs move with it.

* chore(deps): bump @adcp/sdk to 17.0.0-beta.6

17.0.0-beta.6 carries adcp-client#3493: the SDK no longer strips status
from the compact responses on a fixed list. It keeps status where the
served release's response schema composes the protocol envelope and
omits it where that release's schema has none.

The SDK now packages 3.3.0-beta.1 and no longer 3.3.0-beta.0, which the
creative 3.3 beta sandbox serves. The training agent retains it from the
committed dist/schemas bundle, as it does 3.2 beta.6 and the last 3.2
RC, and the sandbox route registers the retained bundles before it
handles a request. The tests that build the sandbox or read the 3.3
beta.0 enums directly register them first.

tenant-smoke's list_products case is served at 3.2, whose published
list_products schema is closed and has no status, so the SDK omits it.
The case grades the response against the newest published 3.2 schema,
the release it was served at, rather than the 3.3 source.

The SDK page states the installed prerelease and its 3.3.0-beta.1 pin.

* fix(training-agent): register the built schemas under the candidate release

@adcp/sdk decides whether list_products and decline_proposals keep the
envelope status from the served release's published response schema
(adcp-client#3493). Its pinned 3.3.0-beta.1 bundle predates adcontextprotocol#8547, so a
candidate run served 3.3-beta.1 without status and failed the schemas
it grades against.

At startup, a trusted candidate process (NODE_ENV=test and candidate
mode, see candidate-version.ts) registers the built dist/schemas/latest
tree under the candidate's release key, when that tree was built for
it. Production and ordinary processes keep the SDK's published pin.
The SDK's ESM status rule reads schemas through its CommonJS schema
loader, a separate module instance, so the tree is registered with both
instances; the storyboard runner's scoped root does not reach that
instance either. Remove the registration once the SDK pins a protocol
release that contains adcontextprotocol#8547.

* test(sdk-conformance): record the reviewed findings against @adcp/sdk 17.0.0-beta.6

The probe run with 17.0.0-beta.6 matches the reviewed set except for the
recorded TypeScript SDK version: 125 cases, one with findings. The one
TypeScript finding is unchanged. At the pinned 3.2.1 GA artifact,
list_products carries adcp_version, which that release's closed
response schema forbids. Neither adcp-client#3493 nor this PR clears it:
the SDK still sends no status at a served 3.2, and the artifact is
immutable. The Python and Go entries are unchanged. The README names the
current SDK and its 3.3.0-beta.1 pin.

---------

Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
…dcontextprotocol#8606)

* docs(governance): ratify DR-0027 to DR-0030 (WG Session C consent)

Objections closed 2026-10-30 with none raised.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(governance): drop stale pending decided_by lines

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ard-merge/3.1.x-3.1.31

chore(release): forward-merge AdCP 3.1.31 and documentation
Co-authored-by: aao-release-bot[bot] <280565558+aao-release-bot[bot]@users.noreply.github.com>
Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
… grant (adcontextprotocol#8607)

sync_reporting_receipts and sync_reporting_status no longer accept receipts
and statuses after the caller loses authority. A seller that wants
reconciliation to finish after a relationship ends keeps a reduced grant
(reconciliation tasks only) instead of revoking; coverage continues while it
lists get_reporting_status. Owner decision.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ental) (adcontextprotocol#8488)

* feat(creative): live creative updates with staged activation (experimental)

A buyer changes a creative that is serving by syncing it again under the
same creative_id. Today the spec refuses updates to creatives on active
packages with INVALID_STATE and tells buyers to pause the package, which
does not work on ad servers where a trafficked creative is fixed, and the
documented zero-gap path (new creative_id, swap after approval) breaks on
sellers that review only in package context.

- creative.live_updates (experimental): per change kind (click_through,
  trackers, asset_content, format) the seller declares uninterrupted,
  staged (prior approved version serves until the change is approved and
  promoted; on_rejection keeps_prior | stops), or paused_for_review. The
  capability is a floor; list_creatives returns exact per-creative
  live_updates; dry_run returns the exact outcome. Name and tags are
  always accepted. Live = approved creative assigned to a non-canceled,
  non-ended package on a non-terminal buy, paused or not.
- staged_update readback on sync_creatives and list_creatives; review is
  one decision across assignments; other transitions discard it. Folds in
  the core of staged activation (adcontextprotocol#6780).
- CREATIVE_LIVE_UPDATE_UNSUPPORTED (+ error details) for declaring
  sellers; non-declaring sellers keep INVALID_STATE.
- creative.status_changed carries staged_update.outcome and permits
  approved -> approved for staged outcomes.
- force_creative_status resolves a pending staged update.
- Training agent /sales implements the surface; storyboards
  creative_live_updates and creative_live_update_refusal exercise it.

Refs adcontextprotocol#8404, adcontextprotocol#6780

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(creative): released schema alias in live update example; record media-buy context growth

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(creative): report staged outcomes on a new event, keep the original rule for non-declaring sellers, drop per-creative readback

Addresses review on adcontextprotocol#8488.

- creative.status_changed is unchanged. Staged outcomes ride on the new
  experimental creative.staged_update_changed notification type, fired only
  to subscribers that register it, so an older buyer never receives an
  approved -> approved fire its closed validator rejects.
- Non-declaring sellers keep the original "cannot be updated" rule and
  INVALID_STATE; only sellers that declare creative.live_updates get the new
  behavior.
- list_creatives no longer returns a per-creative live_updates policy or lists
  it in fields; the capability is the general floor and sync_creatives dry_run
  answers for one change.
- Training agent gates live updates to the 3.3 wire, emits the new event, and
  the storyboard drops the readback phase.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(creative): bound live_updates.in_place and repin the capabilities worst case

The capabilities structural-bounds test (adcontextprotocol#8198) requires every array in the
response to carry maxItems. in_place lists at most one entry per change kind,
so it holds at most 4. The pinned worst-case figure and open-position count
move with the new capability block.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(creative): state the edit-or-swap line, and that dynamic creatives are out of scope

Addresses pkras's review on adcontextprotocol#8488: say where an in-place edit ends and a swap
begins, say explicitly that dynamic (DCO) creatives are out of scope for the
experiment, and keep audience, rotation, and weight out of the change kinds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dcontextprotocol#8613)

subscription_coverage[].status gains inactive with a required deactivated_at;
an inactive subscription is listed only while it has retained obligations for
the account. Before, a deactivated subscription still in scope could only read
as active.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd reference model (adcontextprotocol#8616)

* test(compliance): canonical reporting-subscription coverage vectors and reference model

Adds the SDK-independent conformance foundation for AdCP 3.3
reporting-subscription coverage (experimental media_buy.reporting_subscriptions):

- static/compliance/source/test-vectors/reporting-subscriptions/: 113 cases
  with case.schema.json, pinned to IANA tzdata 2026f. 81 are ported from the
  adcp-client#3505 parity table and re-derived from the spec text. 32 are new:
  midnight DST in Santiago, Havana, and Cairo; Lord Howe's 30-minute DST;
  Kathmandu sub-daily; month-end clamps; P7D, P1W, and P1Y; compound changes
  at one instant; four-grant chains; max_accounts per offering; fractional
  seconds and offsets; empty flights; the confirmation period.
- scripts/reference/reporting-coverage/: a brute-force reference model that
  enumerates every period and intersects one predicate per clause. Its time
  zone evaluator reads the pinned zic source itself and is checked against
  zic output; it uses no ICU, Intl, or zoneinfo.
- tests: the model reproduces every vector; each case's wire objects validate
  against the AdCP schemas. Seeded property tests cover exception monotonicity,
  nothing owed twice, cutover stretch bounds, prefix stability, no retroactive
  extension, and calendar invariants.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(compliance): read tzdata.json without a check-then-read race

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(compliance): drop an unused loop binding in the coverage model

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(compliance): freeze reporting-subscription error and reason codes

Adds error-codes.json (SDK error codes with recovery, dispatched, and fix)
and reason-codes.json (why a period is or isn't owed, with the fixed rule
order) beside the coverage vectors. The case schema carries both
vocabularies, and expected_error now gives a frozen COVERAGE_INPUT_* code
and detail. Tests check the vocabularies against the case schema, the wire
exclusion and gap reasons, and every error the reference model raises.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the Version Packages PR (adcontextprotocol#8623)

* fix(release): generation-only pushes defer storyboard verification to the Version Packages PR

On main, an ordinary push only regenerates the Draft Version Packages PR,
which publishes nothing. verify-release still ran the ~2h storyboard
matrix first, and generation (which rightly requires exact current main)
was then staled by the next unrelated merge, so beta.3 could not generate
while main was busy. Skip the matrix for a main push that commits no
release artifacts (generation_only). The Version Packages PR gets its own
storyboard run in PR CI, and publication still requires that exact-head
run (verified-merge) or full verification at the release merge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(release): enforce the generation-only invariant across jobs (security review)

Classify under set -e so a read failure emits no output; export
generation_only and make the release job refuse to publish a push that
commits release artifacts while classified generation_only. Correct the
docs: the Version Packages PR's storyboards run once it is ready, main
coverage is path-filtered, and generation signs candidates before any
storyboard run. Tests for other-version artifacts and an unreadable
package.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dcontextprotocol#8495)

media_buy_seller now sends a distinct context.buyer_ref on each package of
its create_media_buy and requires check_buy_status to return each package's
own product_id and context, in any order. get_media_buys already makes this
a MUST; only a capability-gated scenario checked it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dcontextprotocol#8634)

npm audit --omit=dev --audit-level=high now fails Build Check on every PR:
follow-redirects 1.16.0-1.16.1 drops cross-realm sensitiveHeaders and can
forward credentials across redirects (high). 1.16.2 fixes it. Lockfile-only.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the coverage vectors (adcontextprotocol#8622)

Rulings on ambiguities the canonical coverage vectors surfaced (adcontextprotocol#8616): the
local day start is always a boundary (PT24H equals P1D and days nest in
sub-daily schedules where midnight does not exist); Lord Howe's 90-minute
hour stands; E never precedes the outgoing side's own start; reactivation
resumes after the last period owed before deactivation; UTC aliases are
exactly UTC and Etc/UTC; billing_cycle needs a date component; P7D weeks
start on Thursday; status_retention_days counts 24-hour days. The reference
model and vectors pin each.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-valid bodies (adcontextprotocol#8572)

Five routed vectors still carried the {"plan_id":"plan_001"} body adcontextprotocol#7567
reported, so grading them depended on whether a verifier validates the request
before the signature.

- profile-3.2/positive/001, profile-3.2/negative/001 and
  profile-3.2/negative/002 are hand-authored and skipped by the profile-3.2
  generator, so they never received adcontextprotocol#7580's bodies. Each now carries the
  create_media_buy body its generated siblings use, under its own
  idempotency_key, with Content-Digest and signature re-derived from the test
  key. negative/001 keeps a cryptographically valid signature spelled in
  Base64URL, and negative/002 a signature valid for the canonical host, so
  each still has exactly one fault.
- positive/002 and negative/018 are restored to the bodies and signatures
  adcontextprotocol#7580 gave them; adcontextprotocol#7581 merged after adcontextprotocol#7580 from an older base and put the
  stub versions back.
- The check that every routed vector carries an operation-valid body, which
  adcontextprotocol#7581 also removed, is restored and now covers profile-3.2/ as well.

Fixes adcontextprotocol#8571.

Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
…ery stop (adcontextprotocol#8617)

* feat(reporting): billing stubs at cutovers and the cancellation delivery stop

Billing feeds no longer leave an unowed stretch at a cutover: when the
incoming billing schedule has no boundary at E, [E, first incoming boundary)
is owed as one stub obligation (stub: true). This covers account timezone
changes on billing offerings. The coverage stop time uses the new
cancellation.effective_at, the delivery stop, instead of the instant the
cancellation was recorded. A subscription seller does not deliver outside a
buy's flight; make-goods extend end_time first or go on a subsequent buy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(reporting): move the delivery stop out of the closed cancellation object

cancellation has additionalProperties: false, so adding effective_at inside it
is non-additive (3.2 validators would reject 3.3 responses). The delivery stop
becomes the media-buy-level sibling cancellation_effective_at instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(reporting): exact empty-stretch condition; billing stubs in the coverage vectors

The cutover stretch is empty exactly when E is also an incoming boundary,
which moving to a finer schedule guarantees and moving to a coarser one does
not. The reference model emits billing stub rows (stub: true), and the
billing-cutover-owes-a-stub vector pins a UTC-day to New York-day billing
cutover.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(reporting): account-level billing configurations get stubs and delivery within the flight

Owner decision: the billing stub at cutovers and the delivery-within-flight
rule extend to sync_accounts reporting_delivery_configs billing generations
(x-adcp-validation.billing_continuity), so subscription and account-level
billing behave the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(reporting): reference model reads cancellation_effective_at

The coverage vectors and reference model now take the delivery stop from
cancellation_effective_at when present, and a new case pins delivery through
a cancellation notice period.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aging, in-place membership (adcontextprotocol#8619)

* feat(reporting): deterministic admission and resolution, overrides, paging, in-place membership

Makes normative what the TS and Python SDKs implement for experimental
reporting subscriptions: sticky max_accounts admission ordered by
authorized_at then account_id byte order (accounts without buys count),
per-package resolution fixed per (buy, version) until withdrawal, and
billing subscriptions on match exact with dimensions. For scale it adds
destination_overrides, bounded inline readback with counts and a paged
get_principal reporting_subscription_page read, and in-place membership
edits recorded as excluded_by_buyer intervals. Several pacing or analytics
subscriptions may now apply to one account and purpose; billing stays one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(reporting): partial grants keep coverage; delivered rows declare account_id

Coverage depends only on get_reporting_status in allowed_tasks. A grant
without sync_reporting_receipts or sync_reporting_status keeps the account
covered, and the seller refuses the missing task with SCOPE_INSUFFICIENT
rather than as an unknown identifier. A seller advertising subscriptions
publishes every managed-delivery offering with a row schema that requires
account_id and keys on it, as both SDKs already assume.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(reporting): pin sticky resolution in the coverage vectors; a buy owes its packages' union

The reference model now resolves each (buy, version) pair once, at the later
of accepted_at and confirmed_at, and re-resolves only when the resolved
offering is withdrawn. resolution-change-is-a-cutover becomes
a-newly-available-offering-never-re-resolves, and
withdrawal-of-the-resolved-offering-is-a-cutover is new. The selection rule
says a buy owes every offering any of its packages resolves, so it can owe
two even under exact or finest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lity on the wire (adcontextprotocol#8618)

* feat(reporting): put flight history, re-grants, and offering availability on the wire

Coverage of experimental reporting subscriptions is meant to be computable by
the buyer without asking. This adds the inputs that were missing:
authorization.regranted_at; flight_history listing every retroactive flight
edit with the flight in force before it; a per-package
reporting_delivery_offering_ids snapshot on get_media_buys; and
available_from / withdrawn_at on offerings. It also clarifies
delivery_config_generations and consumer-status chains when one version
resolves several offerings (offering_id), next_expected_at for owed sets, and
when an owed set is final (period end before ledger_as_of). The capabilities
worst case is repinned for the two offering timestamps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(reporting): coverage vectors use flight_history and published offering availability

With flight_history and available_from/withdrawn_at on the wire, the vectors
need no buyer-retained input: prior_flights becomes flight_history, the
reference model reads it, non_wire_inputs is removed, and the vector test
validates offering availability and flight history against the schemas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(reporting): bound flight_history by retention and refuse edits past 64 entries

Ladon finding: maxItems 64 contradicted 'list every retroactive edit'. Entries
older than status_retention_days may be dropped, since they govern only
periods that ended before them; past 64 retained entries a seller refuses a
retroactive edit with INVALID_STATE, and applies an edit it cannot refuse
from the instant it records it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…timezone changes (adcontextprotocol#8620)

* feat(accounts): money terms on resolved account requests; consistent timezone changes

Account requests (experimental): matched and created resolutions must carry
the target account with its money terms; an updated request that changed
billing, billing_entity, or payment_terms carries effective_at; a change
request that resolves as a separate account lists not_carried_over. currency
and timezone change only where account.requests.changeable_attributes lists
them, so request_account and sync_principal docs now agree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(accounts): the created walkthrough record carries account with its money terms

Ladon finding: the request_account walkthrough resolved as created without
the now-required account.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dles (adcontextprotocol#8636)

The Validate schema PR bundle workflow only runs for PRs to main, whose
bundles are AdCP 3.3 candidates. The Python SDK's default branch is the 9.x
line for AdCP 3.2; 3.3 bundles generate only on its next branch (SDK 10).
Check out next explicitly.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r grants, timezone, and clock (adcontextprotocol#8621)

Adds the experimental comply_test_controller scenario reporting_subscription_probe
(media_buy.reporting_subscriptions) with revoke_grant, regrant, reduce_grant,
set_account_timezone, and advance_time, plus the allowed_tasks and timezone
params. Storyboards can then test reporting-subscription coverage across
grants, wind-downs, and timezone cutovers against a remote seller on one
virtual clock.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs(reference): curated 3.3.0-beta.3 notes

What's new in 3.3 gains a "New in beta.3" section: account change outcomes
and opt-in, money terms on resolved requests, revocation meaning no access
with the reduced-grant wind-down and partial grants, and the reporting
subscription convergence (owed confirmation period, inactive coverage,
billing stubs, wire data, determinism, scale, period arithmetic, vectors,
and the controller probe), plus live creative updates. The adopter-attention
table and the beta program's merged-so-far columns are updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: empty changeset for the beta.3 notes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: aao-release-bot[bot] <280565558+aao-release-bot[bot]@users.noreply.github.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Escalate to human review

Escalate — gated paths touched without required approval.

This PR modifies files under .github/workflows/**, which are under a hard, deterministic approval gate (gated_paths: true), and review_decision is REVIEW_REQUIRED (not APPROVED). Per decision rule row 2, the outcome must be escalate regardless of how clean the diff is.

Escalation reasons

  • Modifies .github/workflows/release-docs.yml, .github/workflows/release.yml, .github/workflows/training-agent-storyboards.yml, and .github/workflows/validate-schema-bundle.yml — gated workflow paths requiring human/CODEOWNERS review. Gate is not satisfied because review_decision is REVIEW_REQUIRED.

Context
No new reviewer findings this pass. The authored delta since the prior approve (head 3878fc3) is empty — the 99 new commits are forward-merges of already-landed main; the PR's own contribution (tests/sdk-runner-capability-gates.test.cjs) is unchanged. The prior Medium (sdk-runner-capability-gates.test.cjs:1429 — gate test drives an injected profile, not a live runner fetch) stands, non-blocking. Despite the clean/unchanged authored diff, the gated-paths gate is unsatisfied and takes precedence over the prior approve.

Medium findings

  • tests/sdk-runner-capability-gates.test.cjs:1429 — "Live capability fetch" block gates on injected profile, not a runner fetch (non-blocking)

Why human review

  • Modifies .github/workflows/release-docs.yml, release.yml, training-agent-storyboards.yml, and validate-schema-bundle.yml — gated workflow paths; review_decision is REVIEW_REQUIRED, so human/CODEOWNERS approval is required (decision rule row 2).
  • This PR touches a path under a hard, non-overridable approval gate (.github/workflows/release-docs.yml (modified) matches .github/workflows/**; .github/workflows/release.yml (modified) matches .github/workflows/**; .github/workflows/training-agent-storyboards.yml (modified) matches .github/workflows/**; .github/workflows/validate-schema-bundle.yml (modified) matches .github/workflows/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@aao-secretariat aao-secretariat Bot added the ladon/needs-human-review Ladon has escalated this PR for human review. label Oct 31, 2026
@aao-secretariat
aao-secretariat Bot requested a review from bokelley October 31, 2026 16:16

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ladon/needs-human-review Ladon has escalated this PR for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants