Repository navigation
fix(schemas): compose the protocol envelope on every task response (#8518) - #8547
Conversation
…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 (#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.
… 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.
…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.
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.
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.
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.
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.
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.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 39200612 | Triggered | JSON Web Token | 45a42c1 | tests/response-envelope-composition.test.cjs | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
There was a problem hiding this comment.
Ladon verdict: Escalate to human review
Escalate — gated schema paths require human/CODEOWNERS approval.
This PR modifies 100+ files under static/schemas/source/**, which are under a hard, deterministic approval gate (gated_paths: true). The required review status is REVIEW_REQUIRED, not APPROVED. Per decision rule row 2, the outcome must be escalate (there are no blocking findings that would make it request-changes).
Reviewer findings: clean — no Critical/High/Medium findings. The reviewer validated wire-shape fidelity (envelope composition only targets additionalProperties:false nodes via allOf/anyOf/oneOf, preserves local narrowing, excludes documentary payload), oneOf disjointness, schema↔docs coherence (check_governance, audit-trail, list_collections, report_media_buy_issue examples updated), changeset correctness (patch is defensible — body invariant holds; status required codifies a pre-existing normative MUST per DR-0008), and release-artifact immutability (no dist/** touched). Test coverage is strong.
Despite the clean review, two independent gates force escalation and cannot be waived by a clean diff:
- Gated paths (
static/schemas/source/**) withreview_decision != APPROVED. - The change tightens
statusto required across published response schemas. While the reviewer frames this as codifying a pre-existing MUST, an optional→required flip on published fields is a breaking-class concern per the constitution; breaking-class changes are never auto-approved without human ratification.
A human/CODEOWNERS reviewer must approve before merge. Once review_decision becomes APPROVED, this PR can fall through to normal evaluation (which, given no blocking findings, would approve).
Why human review
- Gated paths: PR modifies 100+ files under static/schemas/source/** (hard approval gate) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS approval required.
- Potential breaking-class change: status field tightened to required across published response schemas (optional→required flip). Per the constitution's hard gate, breaking-class changes are never auto-approved and require human ratification; escalate unless review_decision is APPROVED.
- This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/account/get-account-financials-response.json (modified) matches
static/schemas/source/**; static/schemas/source/account/get-account-requests-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/list-account-changes-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/list-accounts-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/report-usage-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/request-account-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/sync-accounts-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/sync-governance-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/acquire-rights-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/creative-approval-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/get-brand-identity-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/get-rights-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/search-brands-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/update-rights-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/verify-brand-claim-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/verify-brand-claims-response.json (modified)
…(truncated)…
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.
…om 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 #4897. Fixes #8553.
…osition 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.
There was a problem hiding this comment.
Ladon verdict: Escalate to human review
Escalate — hard approval gate unsatisfied + breaking-class protocol change.
This PR modifies 100+ files under static/schemas/source/**, which are under a hard, non-overridable approval gate (gated_paths: true), and the current GitHub review_decision is REVIEW_REQUIRED, not APPROVED. Per decision-table row 2, this forces escalation until a human/CODEOWNERS approval is recorded.
Additionally, the change carries a breaking-class protocol semantics shift: the outcome/status field placement moves across the top-level status vs. verdict/outcome_state/approval_status wire shapes depending on SDK-negotiated release (3.0 vs 3.1). Per the constitution's hard gate, breaking-class changes are never auto-approved — ratification is a human act — and must escalate unless review_decision is APPROVED.
The reviewer's delta pass over 12 server/test files surfaced no new Critical/High/Medium findings: the servesOutcomeInEnvelopeStatus() regex gate is correct across 3.0/3.0.x/3.1/3.10/undefined, all three buildCheckResponse call sites are updated together, and tenant-smoke.test.ts validates both wire shapes against the real source schemas. The code is clean. But cleanliness does not lift either the deterministic gated-paths gate or the prose breaking-class gate — both require explicit human/CODEOWNERS approval.
Prior decision was also escalate for the same two reasons; the gate conditions persist (gated paths still unsatisfied, breaking-class change remains). Human/CODEOWNERS review required.
Why human review
- Gated paths: PR modifies 100+ files under static/schemas/source/** (hard, non-overridable approval gate) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS approval required before merge.
- Breaking-class protocol change: outcome field placement is relocated between top-level
status(3.0) andverdict/outcome_state/approval_statuswithstatus: completed(3.1+), an optional→required/semantics shift on published response schemas. Per the constitution's hard gate, breaking-class changes are never auto-approved and require human ratification; escalate unless review_decision is APPROVED. - This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/account/get-account-financials-response.json (modified) matches
static/schemas/source/**; static/schemas/source/account/get-account-requests-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/list-account-changes-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/list-accounts-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/report-usage-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/request-account-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/sync-accounts-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/account/sync-governance-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/acquire-rights-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/creative-approval-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/get-brand-identity-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/get-rights-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/search-brands-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/update-rights-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/verify-brand-claim-response.json (modified) matchesstatic/schemas/source/**; static/schemas/source/brand/verify-brand-claims-response.json (modified) matches `static/sch
…(truncated)…
… 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.
Correction (b) recorded the adcp_version echo on closed task responses, pointing at #8523 and #8524. #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.
Conflicts resolved by recomputing the figures that both sides moved: the capabilities worst case is main's 91,804,371 plus this branch's 3,904 for the enforced message bound (91,808,275), and the MCP schema analysis counts are main's plus core/task-id.json (+1 instance, +1 unique). tests/definition-pin-vectors.test.cjs follows main's deletion, tests/hold-policy-vectors.test.cjs takes main's terms-digest helper and keeps the built-schema reads, and the 3.3 sweep drops both withdrawn rows. The new list_proposals rows test carries status, which the envelope requires.
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.
…elease @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 #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 #8547.
|
Storyboards, current surface in candidate mode: every tenant meets its floors, and every required-clean and required-exact storyboard passes. That includes Two things for the SDK:
Still red, both SDK-side, with no schema change here:
|
… 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.
|
For rebasing and re-running storyboards:
Rebase onto |
Main is on @adcp/sdk 17.0.0-beta.6 (#8597) and moves the training agent's 3.3 candidate and creative sandbox pins to 3.3-beta.1. Where #8597 overlaps this branch's bump, main's version is kept: the branch's retention of 3.3.0-beta.0 from dist/schemas, the creative sandbox route's registration call and the test hooks that registered it are dropped, since nothing serves 3.3-beta.0 any more. The SDK page takes main's text. The candidate registration of the built schemas under 3.3.0-beta.1 stays. Its second registration with the SDK's CommonJS module instance is still needed on beta.6 and now cites adcp-client#3519.
|
Merged The tenant-smoke Storyboards, current surface in candidate mode: every tenant meets its floors, and all 41 required-clean and required-exact checks pass. That includes Still red, all SDK-side:
|
….2.x backport of #8547) (#8567) * 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 43 of the 118 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, and request_proposals and refine_proposals drop their hand-copied adcp_version $refs. - Response roots and status branches stay open (DR-0019), so the envelope reaches every response through that allOf with no build step. The two get_reporting_status failure branches, the last closed response nodes (experimental on 3.2.x), are opened. A failed lookup forbids each body field of the success view by name, the rejection its closure provided, so it still reveals no resource data. - decline_proposals lets 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, and the list_products examples, the MCP guide's envelope field list and the versioning page say the same. Test fixtures, compliance vectors and doc examples carry status. The reporting regression test validates the dispatched wire response, with the status the dispatcher stamps, instead of injecting one. tests/response-envelope-composition.test.cjs enumerates every tool's response and async status schemas through discoverTools and checks that each composes the protocol envelope through its root allOf, that none composes the version envelope directly, and that no response root or status branch is closed. Every root and branch is compiled on its own and accepts each envelope property, with single-fault vectors; a vector is shown to fail a closed node on exactly one error. It runs from npm test as test:response-envelope-composition. * 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. Four handlers put a domain outcome there: acquire_rights (the rights status), check_governance (the verdict), creative_approval (the approval decision) and report_plan_outcome (the outcome state). 3.1 renamed those to rights_status, verdict, approval_status and outcome_state (#4897). served-outcome-status.ts holds the one table of those renames, found by comparing the 3.0 and 3.1 schemas. Handlers emit the 3.1 shape: status completed, with the outcome only in its renamed field. The transport adapters project it onto a served 3.0 before the SDK shapes the response, carrying the outcome in status as the 3.0 schemas define it and keeping the 3.1 field beside it: the governance and brand platforms, the custom tools, the legacy dispatcher, and the in-process MCP server on the legacy route, for fresh and replayed results alike. Caches keep the release-neutral result. acquire_rights at a 3.0 pin used to answer status completed where the 3.0 schema requires acquired, pending_approval or rejected. The served release comes from the SDK request context on the tenants and from the dispatcher on the legacy route. Custom tools always declare adcp_version and adcp_major_version and forward the served release to their handlers. servesOutcomeInEnvelopeStatus uses the shared parseAdcpReleaseVersion, now in types.ts. The governance tests assert the verdict and the task status separately, and the smoke test covers report_plan_outcome at a 3.0 pin with its replay, and acquire_rights at the current release and at 3.0. Fixes #8553. * fix(schemas): declare envelope fields only on the envelope, and task_id once Response schemas restated envelope fields inline, and some copies narrowed them without a reason: the compact proposal-flow responses pinned replayed to const true, and the compact branches, the core/compact-task-* payloads and the submitted branches and async submitted payloads of the other tasks capped message at 2,000 characters while the envelope, the MCP webhook payload and the response wrapper declared no bound. The envelope's replayed is a boolean that defaults to false, so the replayed pins rejected conforming responses, and the message bound depended on the delivery path. - Remove the inline copies of envelope fields from 35 response schemas: task_id, message, replayed, governance_context, adcp_error, the version and correlation fields, and context declarations with a local description. A status const or enum that discriminates a branch, a context $ref identical to the envelope's, and a const that pins a fixed value (the message and adcp_error of an unavailable get_reporting_status lookup, which keep it from revealing whether a resource exists) stay. - A task-level message has one definition, core/task-message.json: type string, maxLength 2000, untrusted input to the receiver, and a seller with longer text truncates it rather than fail the task. The protocol envelope, the MCP webhook payload and the response wrapper refer to it, so the 2,000 that 3.2.7 published on the compact and submitted responses is the one bound on every carrier. The envelope's replayed says that an absent replayed means false and that a response may carry replayed: false. - task_id has one definition, core/task-id.json: type string, minLength 1, x-entity task, x-max-length-4.0 128, registered in the schema index. 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. A task_id is the handle a caller polls and a webhook references, and 3.2.7 already published minLength 1 on core/compact-task-submitted.json. - core/task-submitted.json is the one place a submitted response requires task_id: status const submitted and a required task_id (core/task-id.json). Code generators ignore an if/then, so the envelope states no conditional rule of its own. 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. - core/task-working.json and core/task-input-required.json pin the working and input-required status the same way, and every working and input-required body composes its own, so an async status payload accepts only its status. The MCP webhook example that carried a create_media_buy result now carries a complete one. - The oneOf audit fuses a variant's allOf members before classifying it, as code generators do, so the submitted branches stay discriminated. - The storyboard context-output lint follows every composition keyword of a node, and the branch an expected_arm selects carries its document's root allOf, so a capture of task_id from a submitted branch resolves through the envelope. Both storyboard path lints find an expected_arm const through a branch's allOf, as the submitted branches now carry it. The composition test checks that no response node copies an envelope field, pins replayed or bounds message, that task_id has one definition, that every submitted, working and input-required body composes its core/task-<status>.json, that no response schema keys task_id on status through if/then, and that the envelope, webhook payload and wrapper message refer to core/task-message.json. Its per-node vectors add replayed: false and a 2,000-character message, and each is shown to fail a replayed pin or a lower message bound on exactly one error. Every response document and both other message carriers accept a 2,000-character message and reject 2,001 characters on maxLength only. Every response document rejects an empty task_id on its length only; every document with a submitted body or branch rejects a submitted response without task_id on task_id only; and every request that carries a task_id rejects an empty one on its length only. Structural tests and the MCP schema analysis counts follow the shared definition. The media-buy model-context budget moves from 443 to 444 KiB: the inputs that reach core/task-id.json come to 453,646 bytes, 14 over the former budget. The parity compile limit moves from 1,265,000 to 1,266,000 bytes: the comply_test_controller request reaches the shared task definitions through its forced task results and comes to 1,265,334.
… closed The 3.2.10 backport found two more privacy clamps: - get_reporting_status's Unavailable lookup failure is closed in every 3.2 release; #8547 opened it during the 3.3 betas. Its caller isolation text requires unknown and unauthorized identifiers to use one identical lookup_unavailable shape, so it closes again with the envelope fields redeclared as 3.2.8 does. The Operational failure branch stays open; its failure_kind const keeps the two apart. - BILLING_NOT_PERMITTED_FOR_AGENT details stay closed, as PERMISSION_DENIED's per-agent details do: their description calls extra keys commercial-state oracles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every response root and status branch accepts every envelope field (#8547), so the Unavailable lookup branch can't close without redeclaring the envelope, which the composition rule forbids. main's shape already carries the caller-isolation clamp: the error objects stay closed, the message is a const, and the ledger and account fields are rejected by name. The branch goes back to that shape; only its nested error objects stay closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… closed The 3.2.10 backport found two more privacy clamps: - get_reporting_status's Unavailable lookup failure is closed in every 3.2 release; #8547 opened it during the 3.3 betas. Its caller isolation text requires unknown and unauthorized identifiers to use one identical lookup_unavailable shape, so it closes again with the envelope fields redeclared as 3.2.8 does. The Operational failure branch stays open; its failure_kind const keeps the two apart. - BILLING_NOT_PERMITTED_FOR_AGENT details stay closed, as PERMISSION_DENIED's per-agent details do: their description calls extra keys commercial-state oracles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every response root and status branch accepts every envelope field (#8547), so the Unavailable lookup branch can't close without redeclaring the envelope, which the composition rule forbids. main's shape already carries the caller-isolation clamp: the error objects stay closed, the message is a const, and the ledger and account fields are rejected by name. The branch goes back to that shape; only its nested error objects stay closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WG decided on 2026-11-09 that 3.x opens the schemas that were meant to be open, so a 3.2 validator does not reject a newer 3.x payload just because a schema was closed. This is the 3.2.10 backport of the 3.3 opening. additionalProperties goes from false to true at 671 of the 775 closed locations in the 3.2 source tree. 104 locations stay closed, by the keep-closed list approved for 3.3; every file/pointer the two lines share has the same class: oneOf branches told apart only by key presence, the signed commercial terms and the must-understand proposal budget constraint, metric qualifiers and forecast dimensions (join keys), the JWS response envelope, privacy boundaries (TMP match requests, the not-found error, permission-denied and billing-not-permitted details, get_principal and sync_principal results, the get_reporting_status unavailable lookup), the credential-free reporting destinations and their setup action, placement-presentation (no key material or markup), and patternProperties maps keyed by IDs. The get_reporting_status unavailable-lookup branch stays closed on 3.2.x with the envelope redeclared; 3.3 opens it under #8547's envelope rule and enforces the same clamp with named rejections. The MCP model-context projection drops boolean additionalProperties, true as well as false (ported from f5c56db on main), so the opening does not inflate the prompt view. Tests that pinned closure now assert the opened behavior or compare against the source schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(schemas): fold in #8772's capability-block and container openings Brings over the schema, test and doc changes of #8772 by Konstantin Mirin: the daypart window, the overlay requirement and support containers, spot reporting and canonical reporting capabilities open; boolean demographics becomes the deprecated 3.2 shorthand for { age: true }, so only gender support satisfies a gender requirement. The DR-0036 register entries and the named exception from #8772 are left out: the next commits open these schemas under the WG decision of 2026-11-09, which needs neither. Co-authored-by: Konstantin Mirin <konstantin.mirin@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): open the 3.3 schemas that were meant to be open The WG decided on 2026-11-09 that 3.x opens the schemas that were meant to be open: closing a schema only breaks hard-coded validators, so opening one is not a protocol break. additionalProperties goes from false to true at 784 of the 859 closed locations in the source tree. Nothing else in those schemas changes. 75 locations stay closed for a structural reason: oneOf branches told apart only by key presence (a2ui bound-value, brand.json), the signed commercial terms that terms_digest binds, metric qualifiers and forecast dimensions (join keys), the canonical installment identity, the JWS response envelope, the TMP match requests and the not-found error (privacy boundaries), the reporting-destination setup action, and patternProperties maps keyed by IDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): keep the privacy clamps closed; drop closure claims from opened schemas The per-agent PERMISSION_DENIED details and the failed and unconfigured get_principal results stay closed: their descriptions make the closed shape the clamp against an existence or commercial-state oracle. Descriptions of schemas that are now open no longer say they are closed or reject unknown members. The metric qualifiers stay closed, so their descriptions now say new keys ship in a major release, not in a minor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lint): flag additions only to schemas that stay closed Under the WG decision of 2026-11-09 and the rewritten DR-0036, schemas are open by default and a schema stays closed only for a structural reason. The additive-minor lint now matches: - property-added-to-closed fires only where the schema is closed in both the base and the candidate. Opening a schema, and adding to one the candidate opens, passes. - The closed-anywhere-in-the-major rule goes: closedInEarlierReleases, the "(closed in X)" findings and the released exemption. - Corrections only tighten a bound or require a name the published text already set. The 24 whole-schema reopen entries go; an entry that lets in a property is an error. - Openings print one notice line with the count. - Removals, renames, retypes, enum removals, bound tightening, new required names and closing an open schema still fail. The baseline drops the 34 entries this resolves and keeps 4, each with its resolution: metric_id and the two sla-window fields sit on schemas that stay closed (signed commercial terms), and evaluator is restored. Also ports the DR-0036 cross-check removal from #8858 (from Konstantin Mirin's #8777): the corrections list names its decision record, which no longer lists entries. Co-authored-by: Konstantin Mirin <konstantin.mirin@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(lint): cover the open-by-default additive-minor rule Drops the tests of the earlier-release closure, the released exemption and the reopen and property corrections, moves the correction checks onto bound fixtures, and adds cases for additions to schemas closed on both sides, opened, or open, for closing an open schema, and for a correction that lets in a property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): keep the reporting-status and billing-permission clamps closed The 3.2.10 backport found two more privacy clamps: - get_reporting_status's Unavailable lookup failure is closed in every 3.2 release; #8547 opened it during the 3.3 betas. Its caller isolation text requires unknown and unauthorized identifiers to use one identical lookup_unavailable shape, so it closes again with the envelope fields redeclared as 3.2.8 does. The Operational failure branch stays open; its failure_kind const keeps the two apart. - BILLING_NOT_PERMITTED_FOR_AGENT details stay closed, as PERMISSION_DENIED's per-agent details do: their description calls extra keys commercial-state oracles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: say schemas are open by default; restate the closed ones' reasons The 3.2 to 3.3 migration guide's "Closed schemas stay closed within a major" section becomes "Schemas are open by default": what opened, the structural reasons the rest stay closed, and what it means for validators, generated types and 3.2 peers. The 3.3 beta page, What's new, the release notes and the 4.0 compatibility guide follow. Pending changesets that cited "a minor never changes a closed schema" or a lettered DR-0036 correction now cite the structural reason (commercial_terms is signed) or DR-0024. Adds the changeset for the opening. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): keep trust.json and placement-presentation closed Both are security-sensitive documents whose closure keeps out an open-ended class of content that no list of names could cover: - trust.json sits on the signature-verification fetch path, and signing keys are published only in the key directory, so its records, agents, grants and acknowledgements carry no key material (jwks_uri, key pins, signing profiles) and no undeclared authority. - placement-presentation is a declarative, non-executable frame that consumers render, so it carries no markup or script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: count the 100 locations that stay closed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): keep principal results, reporting destinations and hard budget bounds closed - get_principal and sync_principal results keep their closed branches: no branch may become an oracle for another principal's existence or state, and a dry run issues no durable identifier. - Reporting destinations are credential-free by contract, and closure rejects credentials under any name. - proposal-budget-constraint is the strict peer of budget-range: a seller that ignored an unknown member of a hard constraint would violate a constraint it never read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: count the 116 locations that stay closed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): keep the reporting-status lookup_unavailable branch open Every response root and status branch accepts every envelope field (#8547), so the Unavailable lookup branch can't close without redeclaring the envelope, which the composition rule forbids. main's shape already carries the caller-isolation clamp: the error objects stay closed, the message is a const, and the ledger and account fields are rejected by name. The branch goes back to that shape; only its nested error objects stay closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(security): count the capability blocks 3.3 opens as open positions The worst-case capabilities document is unchanged at 99,100,981 bytes; its open positions go from 108 to 190. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): name the fields closure used to reject in opened schemas Where a closed schema also rejected a specific field the protocol defines elsewhere, or one its text forbids there, the opened schema now rejects it by name in a not/anyOf/required clause, as #8531 did for canonical-product. Examples: trackers, landing_page_url and weight stay off unassign, replace and merge; hold entries carry no expires_at; a reporting revision carries no destination_ref; delivery-ready webhooks carry no access_token; SDK metadata carries no self-asserted approval; an attestation presentation carries no evaluator decision; requirement branches reject seller maxima. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: expect opened schemas to accept unknown keys Tests that asserted generic closure (an undeclared key is rejected, or additionalProperties is false) now assert the opened behaviour. Tests that reject a named field are unchanged and pass through the schema's named rejection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): keep key-presence anyOf unions apart by name Opening the branches of an anyOf that was told apart only by which keys are present lets one payload carry both forms. Each branch now rejects the other's keys by name: - canonical-account-ref: the account_id form rejects brand and operator, and the natural-key form rejects account_id; - geo-region requirement and support: all_values and values exclude each other per country; - the deprecated fused-system postal area rejects country; - a sync_reporting_receipts result carries a receipt or an adjustment receipt, not both. Also names step on frequency-cap impression constraints and mode on list application receipts, matching the 3.2.10 backport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: finish the open-schema test fallout - mcp-schema-projection: PARITY_COMPILE_LIMIT 1,384,000 -> 1,385,000, measured (the named rejections add 395 bytes to the comply-test-controller closure); the list_products legacy-graph check strips the new format_ids and v1_format_ref prohibitions as it strips canonical-product's. - Unknown-key cases that are now accepted: entries_total on list receipts, an unknown geo_countries support key, response_due_at and completion_due_at on issue receipts, the bucket_completness typo, and an unexpected publisher targeting key-value config key. - New canonical-account-ref test: account_id and the natural key exclude each other, and an unknown key is accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(training-agent): keep tools/list within budget and ignore unknown declarations - tools/list discovery schemas omit `additionalProperties: true` inside bundled definitions. It is JSON Schema's default and carries no information, and dropping it keeps the four-tool surface under its 162 KiB budget once the named rejections land. The root keeps the keyword, which the parity test compares with the source. - The parity test compares openness instead of the literal keyword. - sync_principal keeps only the declaration axes the seller reads, so an unknown declarations member is ignored rather than stored and read back. An unknown section is still refused (UNSUPPORTED_FEATURE). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): address review on named rejections and key-presence guards From Konstantin Mirin's review of #8866: - Key-presence guards on two more unions: account-ref's account_id and natural-key forms exclude each other (as canonical-account-ref does), and image bleed's uniform and per-side forms exclude each other. canonical-account-ref's account_id form also rejects operator_unit, currency, timezone and sandbox, and a failed receipt result carries no receipt. A rescan of every oneOf and anyOf finds no other key-presence union. - Named rejections now cover only names the protocol defines elsewhere or forbids there. Dropped: step, covered (two nodes), gpc_honored, political, threshold_rate, per_ad_spend, verified (two nodes) and mode on list receipts. access_token on the delivery-ready webhook stays as a credential clamp. - Every node or union with a named rejection says in its description what it rejects and why, since generated types can't express not. - The get_principal current branch and the sync_principal applied and validated branches open: they return the caller's own configuration and are dispatched by kind. The validated branch rejects destination_ref and configuration_version, because a dry run issues no durable identifier. recognized, unconfigured and failed stay closed. - The reporting-status lookup_unavailable branch states its clamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: call the commercial terms a hold until 4.0; carry #8772's doc hunks From Konstantin Mirin's review of #8866: - The commercial terms and proposal-budget-constraint are held closed until 4.0, not closed for a structural reason. terms_digest covers every member at every depth, open ones included, so the docs no longer claim that closure stops a 3.2 buyer accepting a term it ignored. - The boolean demographics deprecation reaches the 3.2 to 3.3 summary and Deprecations list, and the 4.0 guide's removal count and seller-emitted list, as #8772 had them. - target_audiences sits beside audience_evidence_requirements because one names which audience and the other the evidence quality, not because the requirements object was closed. - 3.2.10 opens all but one of 3.2's locations: it keeps the lookup_unavailable branch closed. The get_principal current and sync_principal applied and validated results open, so 747 locations open and 112 stay closed. - The lint hint points to the corrections register again and says a correction sets a bound or a required name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: flip probes for dropped rejections; add account-ref and bleed guards From Konstantin Mirin's review of #8866: - Probes for names the protocol never defines (step, mode on receipts, covered, gpc_honored, political, threshold_rate, per_ad_spend, verified) now expect the opened schema to accept them. - New tests: account-ref and image bleed key-presence guards, and the extended canonical-account-ref guard. - The list_products legacy-graph check skips not subtrees and descriptions structurally, instead of matching serialized strings. - The model-context projection omits named rejections beside declared properties, as it omits bounds: they are validation, and the descriptions carry them. A bare not branch of a union stays. The media-buy budget is unchanged; PARITY_COMPILE_LIMIT is 1,386,000, measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(lint): prune the additive-minor baseline to base 3.2.9 The 3.2.9 forward-merge makes 3.2.9 the default base. The 4 recorded entries are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: point the backport to the next 3.2.x patch; drop stale closure claims 3.2.10 shipped without the open-schemas backport, so the changeset and the migration guide now say the next 3.2.x patch (#8864) opens 3.2's locations, all but one. Docs that still called opened objects closed now match the schemas: the refine scope branches (a stray id fails on the missing product_id), policy entries, sync_plans plan items, and the account-ref variants. Where the reason was the commercial terms, the docs say they are held closed until 4.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: regenerate compliance symbol links on get_products Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: describe the account-ref guard errors, not additionalProperties Merged variants now fail on the named rejection (not) or a missing required name, and a budget object fails on type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): reserve the plan_hash bookkeeping names on sync_plans plans The opened sync_plans plan item accepted the governance agent's bookkeeping names: version, status, syncedAt, revisionHistory, committedBudget and committedByType. The plan_hash profile strips exactly those names from its preimage, so a buyer value under one of them would sit outside the hash and could collide with state the agent manages. The plan item now rejects them by name, and its description says they are reserved and a buyer MUST NOT send them. The list matches the profile's closed "Excluded fields" list. No storyboard, fixture or request example sends them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(3.3): say the commercial terms are held closed until 4.0 whats-new and the release notes no longer give the signed commercial_terms as the example of a structural reason: the rest stay closed where closure enforces something, and commercial_terms is held closed until 4.0. experimental-status no longer calls the buy_products purchase input and the list_products offer_filters closed; both are open in 3.3 and gain target_audiences in 4.0, with the accepted product-purchase snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(lint): describe the commercial terms as held closed until 4.0 The withdrawn-to-4-0 test comment, the additive-minor baseline $comment and the lint header drop the structural-reason framing for the commercial terms. All four baseline entries sit in commercial-terms schemas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(refinement): match the troubleshooting heading to the error it shows The heading quoted "must NOT have additional properties" while the body shows the missing product_id error. The body no longer says every scope branch requires its own identifier: the request scope requires ask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Konstantin Mirin <konstantin.mirin@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The WG decided on 2026-11-09 that 3.x opens the schemas that were meant to be open, so a 3.2 validator does not reject a newer 3.x payload just because a schema was closed. This is the 3.2.10 backport of the 3.3 opening. additionalProperties goes from false to true at 671 of the 775 closed locations in the 3.2 source tree. 104 locations stay closed, by the keep-closed list approved for 3.3; every file/pointer the two lines share has the same class: oneOf branches told apart only by key presence, the signed commercial terms and the must-understand proposal budget constraint, metric qualifiers and forecast dimensions (join keys), the JWS response envelope, privacy boundaries (TMP match requests, the not-found error, permission-denied and billing-not-permitted details, get_principal and sync_principal results, the get_reporting_status unavailable lookup), the credential-free reporting destinations and their setup action, placement-presentation (no key material or markup), and patternProperties maps keyed by IDs. The get_reporting_status unavailable-lookup branch stays closed on 3.2.x with the envelope redeclared; 3.3 opens it under #8547's envelope rule and enforces the same clamp with named rejections. The MCP model-context projection drops boolean additionalProperties, true as well as false (ported from f5c56db on main), so the opening does not inflate the prompt view. Tests that pinned closure now assert the opened behavior or compare against the source schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt) (#8864) * fix(schemas): open the 3.2 schemas that were meant to be open The WG decided on 2026-11-09 that 3.x opens the schemas that were meant to be open, so a 3.2 validator does not reject a newer 3.x payload just because a schema was closed. This is the 3.2.10 backport of the 3.3 opening. additionalProperties goes from false to true at 671 of the 775 closed locations in the 3.2 source tree. 104 locations stay closed, by the keep-closed list approved for 3.3; every file/pointer the two lines share has the same class: oneOf branches told apart only by key presence, the signed commercial terms and the must-understand proposal budget constraint, metric qualifiers and forecast dimensions (join keys), the JWS response envelope, privacy boundaries (TMP match requests, the not-found error, permission-denied and billing-not-permitted details, get_principal and sync_principal results, the get_reporting_status unavailable lookup), the credential-free reporting destinations and their setup action, placement-presentation (no key material or markup), and patternProperties maps keyed by IDs. The get_reporting_status unavailable-lookup branch stays closed on 3.2.x with the envelope redeclared; 3.3 opens it under #8547's envelope rule and enforces the same clamp with named rejections. The MCP model-context projection drops boolean additionalProperties, true as well as false (ported from f5c56db on main), so the opening does not inflate the prompt view. Tests that pinned closure now assert the opened behavior or compare against the source schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): name the fields closure used to reject in opened 3.2 schemas Ports #8866's named rejections to 3.2.x for every node that exists there: where closure was the only thing rejecting a specific field the protocol defines elsewhere or forbids, the opened schema rejects it by name in `not`, next to additionalProperties: true. 31 of #8866's 51 named rejections apply on 3.2.x; the rest target schemas or nodes that 3.2.x does not have. Also ports #8866's description cleanups: opened schemas no longer say they are closed, and the metric-qualifier descriptions say new keys ship in a major release. The 3.3-only boolean-demographics deprecation and gender-targeting wording are not ported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: expect opened 3.2 schemas to accept unknown keys Ports #8866's test edits for the test files 3.2.x has: generic unknown-key assertions now expect the key to validate, citing DR-0009 (DR-0036 is not on 3.2.x), and the training agent test shows an unknown hard overlay requirement still matches no product. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): keep key-presence anyOf unions apart by name on 3.2.x Ports #8866's 9bda16d and 32768d4 to 3.2.x. Opening exposed anyOf unions whose branches were told apart only by which keys are present; each branch now rejects its sibling's keys by name: geo-region requirement and support countries (all_values vs values), the canonical account ref (account_id vs brand + operator), the deprecated fused-system postal area (no country), and sync_reporting_receipts results (receipt vs adjustment_receipt). Also names `step` on frequency cap impression constraints and `mode` on list-application receipts. Tests: adds canonical-account-ref.test.cjs (loading source schemas, as 3.2.x tests do), strips the format-key prohibitions from the MCP legacy-graph check, and flips the generic unknown-key vectors, citing DR-0009. PARITY_COMPILE_LIMIT is unchanged on 3.2.x; it does not trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(training-agent): omit default openness from bundled tools/list definitions on 3.2.x Ports the tools/list and parity-test parts of 9c1c52d (#8866): bundled definitions in the product-discovery tools/list schemas omit `additionalProperties: true`, which is JSON Schema's default and carries no information. The root keeps the keyword. The parity test compares openness rather than the literal keyword. The sync_principal part of 9c1c52d does not apply: the 3.2 training agent does not implement sync_principal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): mirror #8866's review fixes on 3.2.x Ports cd8bc76 (Konstantin Mirin's review of #8866) for every node 3.2.x shares: - Key-presence guards: account-ref's account_id and natural-key forms exclude each other, as canonical-account-ref's now do for all six natural-key fields; image bleed is uniform or per-side; a failed reporting-receipt result carries no receipt. - Named rejections only for names the protocol defines elsewhere or forbids: step, gpc_honored, political, threshold_rate and per_ad_spend are dropped, and list-application receipts reject auth_token alone. access_token on the delivery-ready webhook stays as a credential clamp. - Each node or union with a named rejection says in its description what it rejects and why. The sync_creatives sentence omits the merge op 3.2.x does not have, and the closed lookup_unavailable branch states its clamp in 3.2.x terms. - The get_principal current branch and the sync_principal applied and validated branches open; the validated branch rejects destination_ref and configuration_version. The changeset calls the commercial terms a hold until 4.0 rather than a structural reason. 674 of 775 locations open; 101 stay closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: flip probes for dropped rejections; add account-ref and bleed guards Ports c01b095 (#8866) for the test files 3.2.x has: probes for the dropped rejections expect acceptance (DR-0009); account-ref.test.cjs and image-bleed.test.cjs are new (loading source schemas, as 3.2.x tests do) and wired into test:schemas; canonical-account-ref covers the extended guard. The MCP legacy-graph check walks the parsed schema and skips `not` and descriptions, and the model-context projection omits named rejections beside declared properties, as it omits bounds. PARITY_COMPILE_LIMIT is unchanged on 3.2.x; it does not trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: describe refine scope branches and policy entries as open on 3.2.x The refine[] scope branches and governance policy entries are open on 3.2.x now. A pre-rename refine entry still fails validation, because its generic `id` doesn't supply the `product_id` or `proposal_id` its scope requires; the troubleshooting section shows the error the opened schema gives. A policy-entry field outside the table has no defined meaning, so receivers ignore it; partner metadata still belongs under `ext`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: give the real reasons for the sync_plans and account-ref statements The sync_plans plan item is open on 3.2.x, so the governance agent's bookkeeping fields are simply not declared there, rather than excluded by closure. The two AccountRef variants are told apart by named rejections: the account_id variant rejects the natural-key fields and the natural-key variant rejects account_id, so {account_id, brand} still fails both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: match #8866's wording for the opened-schema statements Aligns the refine troubleshooting, policy-entry, sync_plans and AccountRef passages with #8866 (6c689cc), including the two calling-an-agent error-table rows that named additionalProperties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(schemas): reserve the plan_hash bookkeeping names on sync_plans plans Ports fa7aefa (#8866). The opened sync_plans plan item accepted the governance agent's bookkeeping names: version, status, syncedAt, revisionHistory, committedBudget and committedByType. The plan_hash profile strips exactly those names from its preimage, so a buyer value under one of them would sit outside the hash and could collide with state the agent manages. The plan item now rejects them by name, and its description says they are reserved and a buyer MUST NOT send them. The list matches 3.2's closed "Excluded fields" list. No storyboard, fixture or request example sends them. The test loads source schemas, as 3.2.x tests do, and cites DR-0009. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(refinement): match the troubleshooting heading to the error it shows Ports 7d4dd1b (#8866): the heading names the error the opened refine branches give, and the text says which identifier each scope requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #8518. Fixes #8553. Supersedes #8523 (see #8523 (comment)).
Builds on #8531, now on main (DR-0036 correction (a)): composing the envelope through
allOfwith no build step needs the 3.2 proposal-flow response roots open.Problem
Task response schemas don't compose the response envelope (
core/protocol-envelope.json) consistently. On MCP and REST, envelope fields (status,task_id,context_id,message,timestamp,replayed,adcp_error,push_notification_config,governance_context,adcp_version) sit next to the body fields at the response root. A schema that doesn't compose the envelope has no place for them, so the types generated from it can't represent a conforming response. Closed schemas reject the fields outright, and generated models drop them.On main, 47 of 124 tool response and async status schemas didn't compose the envelope, and every closed root and branch (18 of 18) rejected at least one envelope field. SDKs worked around it:
@adcp/clientturns offadditionalProperties: falseon every response root (relaxResponseRoot()), and@adcp/sdkstripsstatusfromlist_products,decline_proposals,list_collectionsandlist_proposalsso their responses pass the closed schemas.Fix
protocol-envelope.jsoncomposesversion-envelope.json. On a response,adcp_versionandadcp_major_versionare envelope fields. Request schemas keep composingversion-envelope.jsondirectly.protocol-envelopethrough rootallOf. Source schemas declare only body fields. A local declaration of an envelope field stays only where it narrows: astatusconst or enum discriminator, aconstpin (the fixedmessageandadcp_errorof an unavailableget_reporting_statuslookup), orcontextas a$reftocore/context.json.additionalProperties: falsedoesn't seeallOf, so a closed node would reject the envelope fields itsallOfcomposes. The 18 closed ones open: the 3.2 proposal-flow roots and branches through fix(schemas): reopen the 3.2 proposal-flow schemas closed against DR-0019 #8531, thelist_collections,list_proposals,get_media_buy_issueandreport_media_buy_issueresponses (new in 3.3), and theget_reporting_statusfailure branches (experimental). Where closure carried a branch exclusion, it is now explicit: a found or recorded issue carries noerrorsand a failed one noissue, a failed reporting read carries no ledger result, andlist_collectionscarries noproducts. Closed value objects and rows inside bodies stay closed.build:schemas, the MCP projection and the additive-minor lint read source as-is.$refcopies (replayed×4,adcp_version×3) and 66 inline copies (task_id×23,message×21,replayed×11,governance_contextoncheck_governance,statusonget_task_status, andadcp_version,adcp_major_version,context_id,timestampandadcp_erroron theget_reporting_statusfailure branches). The untrusted-input handling that only the asyncmessagecopies carried moves tocore/task-message.json. The rootstatusenums ofdecline_proposals,request_proposalsandrefine_proposalsare removed; their branch consts discriminate.task_idhas one definition,core/task-id.json(type: string,minLength: 1,x-entity: task,x-max-length-4.0: 128), and everytask_iddeclaration refers to it: the envelope,tasks/getandget_task_statusrequests and responses, thetasks/listandlist_tasksresponses andtask_idsfilters, the MCP webhook payload, and the compliance controller's request and response (which keep their ownmaxLength: 128). An emptytask_ididentifies no task, and 3.2.7 already publishedminLength: 1oncore/compact-task-submitted.json; DR-0036 records the bound as correction (c).task_idthrough one schema,core/task-submitted.json(status: submittedand a requiredtask_id), which every submitted response body and branch composes throughallOf:core/compact-task-submitted.json, the eight*-async-response-submitted.jsonbodies that don't go through it, and the ten inline submittedoneOfbranches. They no longer restatestatus: submittedor therequiredlist, except the two branches whose OpenAPIdiscriminatorneeds the localstatusconst (control_media_buy,buy_products/accept_proposal). Completed branches keep theirnot: {required: ["task_id"]}. Unlike anif/then, the composition is visible to code generators. TheoneOfaudit now fuses a variant'sallOfmembers, as generators do.core/task-working.json(status: working) and every input-required body composescore/task-input-required.json(status: input-required): the 15 async working and input-required bodies andcore/compact-task-working/core/compact-task-input-required, which the proposal-flow branches name. An async status payload now accepts only its own status.bound,value) or newly requires a name (required), not only one that admits a property. It compares composedrequiredsets, followingallOfand$ref, so a name required through a newallOfentry counts. Each entry must appear in DR-0036's tables; an unrecorded tightening still fails.dist/schemas/latest. That covers the training agent's validator (which no longer loadsscripts/at runtime or injects a missingstatus), the storyboardsample_responselint, and the tests that validated response instances.scripts/built-schemas.cjsrebuilds the tree when its inputs change, under a lock thatbuild:schemasalso takes.decline_proposals: the completed branch forbadestatus, which contradicted the envelope. It now takesstatus: completed, likerefine_proposalsandrequest_proposals.replayed: {const: true}is removed at 12 locations in the compact proposal-flow responses (list_products,list_collections,list_proposals,request_proposals,refine_proposals,decline_proposals,buy_products/accept_proposal,control_media_buy). The envelope'sreplayedis a boolean that defaults tofalse, so a seller that always sendsreplayed: falseconforms. The envelope'sreplayedtext now says that an absentreplayedmeansfalseand that a response may carryreplayed: false.message, in one definition:core/task-message.json(type: string,maxLength: 2000, untrusted input to the receiver). The envelope, the MCP webhook payload and the response wrapper (core/response.json) refer to it, so the bound does not depend on the delivery path. A seller whose text is longer MUST truncate it rather than fail the task. The 23 response nodes that restatedmaxLength: 2000(7 compact branches andcore/compact-task-*, 16 async submitted roots and branches) no longer do. Aconstpin onmessageis kept: the unavailableget_reporting_statuslookup returns fixed text on purpose.core/error.json,core/warning.jsonand body-level messages keep their own bounds. DR-0036 records the bound as correction (d).allOfwhen it decides that a property moved into a composition.adcp_versionreaches a response through the envelope's ownallOfofversion-envelope.json, so an open response root that takes it from the envelope no longer reads as removing it.Training agent
statusmust now be a task status on every response. Training-agent handlers emit the 3.1 shape, and one table maps the outcomes that 3.1 renamed out ofstatus(#4897), found by comparing the 3.0.28 and 3.1.29 schemas:statusacquire_rightsrights_statuscheck_governanceverdictcreative_approvalapproval_statusreport_plan_outcomeoutcome_stateFor a served 3.0, the transport adapters (the governance and brand platforms, custom tools and the legacy dispatcher) move the outcome back into
statusbefore the SDK shapes the response. Caches keep the release-neutral result, so replays are shaped for the replaying caller. Thecreate_media_buy/update_media_buymedia_buy_statussplit is already handled by the SDK for a served 3.0. Custom tools now always declareadcp_versionandadcp_major_versionand forward the served release to their handlers.SDK
@adcp/sdkis17.0.0-beta.6, as on main (#8597, which also moves the training agent's 3.3 pins to3.3-beta.1). It carries adcontextprotocol/adcp-client#3493: the SDK keepsstatusonlist_products,decline_proposals,list_collectionsandlist_proposalswhere the served release's response schema composes the envelope, and omits it where that release's schema has none (3.2.x). Its pinned3.3.0-beta.1bundle predates this PR, so a trusted candidate process (NODE_ENV=testand candidate mode) registers the builtdist/schemas/latesttree under3.3.0-beta.1at training-agent startup. It registers with both the SDK's ESM and CommonJS module instances, because the SDK's ESM status rule reads schemas through its CommonJS loader. Production keeps the SDK's published pin. The registration goes once the SDK pins a release that contains this PR. The CommonJS registration works around adcontextprotocol/adcp-client#3519.DR-0036 correction (b)
DR-0036 recorded correction (b) as declaring the
adcp_versionecho on closed task responses (#8523/#8524). This PR rewrites it as Compose the protocol envelope on every task response. The source text iscore/protocol-envelope.jsonas published in 3.2.7 ("Thestatusfield is REQUIRED on every task response envelope", and "Task response schemas … define ONLY the body fields; protocol-layer fields live on this envelope") and the echo indocs/reference/versioning.mdx. It ships in 3.3.0 (this PR) and a 3.2.x patch.Composing the envelope newly requires
statusat 35 locations (the async working and input-required payloads, the compact working and input-required payloads, and the proposal-flow andlist_productsresponses), and (b) records onerequired: statuscheck entry for each. Correction (c),task_ididentifies a task, makestask_idnon-empty throughcore/task-id.jsonand required on submitted responses throughcore/task-submitted.json. It tightenstask_idtominLength: 1at nine locations, quoting the envelope'stask_iddescription,tasks/getrequiringtask_id, and 3.2.7'scompact-task-submitted. It ships in 3.3.0 and a 3.2.x patch. The proposal-flow response roots and branches are reopened by (a). Thelist_collections,list_proposals,get_media_buy_issueandreport_media_buy_issueresponses are new in 3.3 and open under DR-0019. Theget_reporting_statusfailure branches were closed in 3.2.7, but that schema wasx-status: experimental, which DR-0036's scope rule exempts, so they open under DR-0019 too.Correction (d),
messageis bounded, tightens the envelope's, the webhook payload's and the response wrapper'smessagetomaxLength: 2000throughcore/task-message.json, one check entry each. 3.2.7 published that bound oncore/compact-task-*and the compact and submitted responses, and the envelope already told receivers to treatmessageas untrusted input. It is a bound tightened for one consistent guarantee at the same 2,000, not a value every path already excluded. It ships in 3.3.0 and a 3.2.x patch.Visible effects
statusis required on every task response, asprotocol-envelope.jsonalready stated. That includes compact responses, async status payloads and webhookresultpayloads. Fixtures, compliance vectors and doc examples now carry it.adcp_versionecho is accepted and declared on every response.replayed: false.messagelonger than 2,000 characters is rejected on every delivery path: response, webhook and wrapper. 3.2.7 bounded it only on the compact and submitted responses.status.statusonlist_products(closed) or thedecline_proposalssuccess branch, from their introduction in 3.2.3. That contradicted the envelope from the start; 3.2.7 did not remove anything. 3.3 requires it, so the 3.2.x backport is what lets one seller conform to both.@adcp/sdkthat stripstatusfrom the four tools above now fail validation (Remove envelope workarounds: status stripped from four tools, every response root relaxed adcp-client#3481). The SDK fix (fix(validation): stop stripping task-envelope status and relaxing response roots (#3481) adcp-client#3493) gates on the served release's schema: it keepsstatuswhere that schema composes the envelope and omits it only where the 3.2.x schema has none.Tests
The new
tests/response-envelope-composition.test.cjs(test:response-envelope-composition, run bynpm test) enumerates every tool's response and async status schemas fromdiscoverTools. It asserts that:protocol-envelope.replayed: falseand a 2,000-charactermessage. A response node is a root, a composition branch, or a document a branch names, includingcore/compact-task-*.messagelength, or pinsreplayed.task_id: ""adds exactly itsminLengtherror on every response node and on every request that carries atask_id.core/task-<status>.json(submitted, working, input-required), no schema keystask_idonstatusthroughif/then, and a submitted response withouttask_idfails oncore/task-submitted.json'srequiredalone.messagerefers tocore/task-message.json, and a 2,001-charactermessageadds exactly itsmaxLengtherror on every response node, the webhook payload and the response wrapper.Acceptance vectors run on every response node, conjoined with the envelope: a control instance with the node's and the envelope's required fields passes, and so does the control plus each envelope field, including the edge values. A branch's deliberate exclusion (a completed branch's
not: {required: ["task_id"]}) is honoured and counted. The test refuses to run below Node 24, aspackage.jsonrequires, and checks that every vector ran.The new
tenant-smokecase checkscheck_governance,report_plan_outcome(with its replay),creative_approvalandacquire_rightsat the current release and at a 3.0 pin.@adcp/sdk17.0.0-beta.6)compact_direct_buy_lifecycle(8P),compact_product_lifecycle(10P),declined_proposal_refinement(7P) anddeclined_proposal_execution(9P). /sales: 178 clean, 1,212 steps.Known red checks
post_generate_fixes.pymatches exact base-class lists. Response classes now inheritProtocolEnvelopeonly, so it stops atListCreativesResponse marker missing.45a42c13. It is a fixed string with no signing key behind it, and the next commit replaces it. The finding stays because the commit stays in the PR's history, so incident 39200612 is to be marked as a false positive in the dashboard.main,postProcessCompactTaskSubmittedEnvelopeinscripts/generate-zod-from-ts.tshard-codes the oldcompact-task-submittedfield layout and stops atexpected the generated envelope intersection. The fix is SDK-side: the pass is to be deleted. The schemas here do not change for it.Backport
A 3.2.x backport with the same design is in progress and will be linked here.
Changeset:
patch. The schemas are corrected to match the existing envelope contract, and no new contract is added.