Skip to content

fix(schemas): compose the protocol envelope on every task response (#8518) - #8547

Merged
bokelley merged 42 commits into
mainfrom
fix/compose-protocol-envelope-on-task-responses
Oct 30, 2026
Merged

bokelley merged 42 commits into
mainfrom
fix/compose-protocol-envelope-on-task-responses

Conversation

@KonstantinMirin

@KonstantinMirin KonstantinMirin commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #8518. Fixes #8553. Supersedes #8523 (see #8523 (comment)).

Builds on #8531, now on main (DR-0036 correction (a)): composing the envelope through allOf with 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/client turns off additionalProperties: false on every response root (relaxResponseRoot()), and @adcp/sdk strips status from list_products, decline_proposals, list_collections and list_proposals so their responses pass the closed schemas.

Fix

  • protocol-envelope.json composes version-envelope.json. On a response, adcp_version and adcp_major_version are envelope fields. Request schemas keep composing version-envelope.json directly.
  • Every task response and async status payload composes protocol-envelope through root allOf. Source schemas declare only body fields. A local declaration of an envelope field stays only where it narrows: a status const or enum discriminator, a const pin (the fixed message and adcp_error of an unavailable get_reporting_status lookup), or context as a $ref to core/context.json.
  • Every response root and status branch is open, as DR-0019 keeps response roots. Draft-07 additionalProperties: false doesn't see allOf, so a closed node would reject the envelope fields its allOf composes. 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, the list_collections, list_proposals, get_media_buy_issue and report_media_buy_issue responses (new in 3.3), and the get_reporting_status failure branches (experimental). Where closure carried a branch exclusion, it 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 bodies stay closed.
  • No build step adds the envelope. Source and published schemas agree; build:schemas, the MCP projection and the additive-minor lint read source as-is.
  • Hand-copied envelope declarations are removed from source: the $ref copies (replayed ×4, adcp_version ×3) and 66 inline copies (task_id ×23, message ×21, replayed ×11, governance_context on check_governance, status on get_task_status, and adcp_version, adcp_major_version, context_id, timestamp and adcp_error on the get_reporting_status failure branches). The untrusted-input handling that only the async message copies carried moves to core/task-message.json. The root status enums of decline_proposals, request_proposals and refine_proposals are removed; their branch consts discriminate.
  • task_id has one definition, core/task-id.json (type: string, minLength: 1, x-entity: task, x-max-length-4.0: 128), and every task_id declaration refers to it: the 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). An empty task_id identifies no task, and 3.2.7 already published minLength: 1 on core/compact-task-submitted.json; DR-0036 records the bound as correction (c).
  • A submitted response requires task_id through one schema, core/task-submitted.json (status: submitted and a required task_id), which every submitted response body and branch composes through allOf: core/compact-task-submitted.json, the eight *-async-response-submitted.json bodies that don't go through it, and the ten inline submitted oneOf branches. They no longer restate status: submitted or the required list, except the two branches whose OpenAPI discriminator needs the local status const (control_media_buy, buy_products/accept_proposal). Completed branches keep their not: {required: ["task_id"]}. Unlike an if/then, the composition is visible to code generators. The oneOf audit now fuses a variant's allOf members, as generators do.
  • Likewise every working body composes core/task-working.json (status: working) and every input-required body composes core/task-input-required.json (status: input-required): the 15 async working and input-required bodies and core/compact-task-working/core/compact-task-input-required, which the proposal-flow branches name. An async status payload now accepts only its own status.
  • The additive-minor check can record a correction that tightens a bound (bound, value) or newly requires a name (required), not only one that admits a property. It compares composed required sets, following allOf and $ref, so a name required through a new allOf entry counts. Each entry must appear in DR-0036's tables; an unrecorded tightening still fails.
  • One validation path: every response validator reads the built tree, dist/schemas/latest. That covers the training agent's validator (which no longer loads scripts/ at runtime or injects a missing status), the storyboard sample_response lint, and the tests that validated response instances. scripts/built-schemas.cjs rebuilds the tree when its inputs change, under a lock that build:schemas also takes.
  • decline_proposals: the completed branch forbade status, which contradicted the envelope. It now takes status: completed, like refine_proposals and request_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's replayed is a boolean that defaults to false, so a seller that always sends replayed: false conforms. The envelope's replayed text now says that an absent replayed means false and that a response may carry replayed: false.
  • One bound of 2,000 for a task-level 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 restated maxLength: 2000 (7 compact branches and core/compact-task-*, 16 async submitted roots and branches) no longer do. A const pin on message is kept: the unavailable get_reporting_status lookup returns fixed text on purpose. core/error.json, core/warning.json and body-level messages keep their own bounds. DR-0036 records the bound as correction (d).
  • The additive-minor check follows nested allOf when it decides that a property moved into a composition. adcp_version reaches a response through the envelope's own allOf of version-envelope.json, so an open response root that takes it from the envelope no longer reads as removing it.

Training agent

status must 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 of status (#4897), found by comparing the 3.0.28 and 3.1.29 schemas:

Tool 3.0 status Field from 3.1
acquire_rights rights status rights_status
check_governance verdict verdict
creative_approval approval decision approval_status
report_plan_outcome outcome state outcome_state

For a served 3.0, the transport adapters (the governance and brand platforms, custom tools and the legacy dispatcher) move the outcome back into status before the SDK shapes the response. Caches keep the release-neutral result, so replays are shaped for the replaying caller. The create_media_buy/update_media_buy media_buy_status split is already handled by the SDK for a served 3.0. Custom tools now always declare adcp_version and adcp_major_version and forward the served release to their handlers.

SDK

@adcp/sdk is 17.0.0-beta.6, as on main (#8597, which also moves the training agent's 3.3 pins to 3.3-beta.1). It carries adcontextprotocol/adcp-client#3493: the SDK keeps status on list_products, decline_proposals, list_collections and list_proposals where the served release's response schema composes the envelope, and omits it where that release's schema has none (3.2.x). Its pinned 3.3.0-beta.1 bundle predates this PR, so a trusted candidate process (NODE_ENV=test and candidate mode) registers the built dist/schemas/latest tree under 3.3.0-beta.1 at 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_version echo on closed task responses (#8523/#8524). This PR rewrites it as Compose the protocol envelope on every task response. The source text is core/protocol-envelope.json as published in 3.2.7 ("The status field 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 in docs/reference/versioning.mdx. It ships in 3.3.0 (this PR) and a 3.2.x patch.

Composing the envelope newly requires status at 35 locations (the async working and input-required payloads, the compact working and input-required payloads, and the proposal-flow and list_products responses), and (b) records one required: status check entry for each. Correction (c), task_id identifies a task, makes task_id non-empty through core/task-id.json and required on submitted responses through core/task-submitted.json. It tightens task_id to minLength: 1 at nine locations, quoting the envelope's task_id description, tasks/get requiring task_id, and 3.2.7's compact-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). The list_collections, list_proposals, get_media_buy_issue and report_media_buy_issue responses are new in 3.3 and open under DR-0019. The get_reporting_status failure branches were closed in 3.2.7, but that schema was x-status: experimental, which DR-0036's scope rule exempts, so they open under DR-0019 too.

Correction (d), message is bounded, tightens the envelope's, the webhook payload's and the response wrapper's message to maxLength: 2000 through core/task-message.json, one check entry each. 3.2.7 published that bound on core/compact-task-* and the compact and submitted responses, and the envelope already told receivers to treat message as 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

  • status is required on every task response, as protocol-envelope.json already stated. That includes compact responses, async status payloads and webhook result payloads. Fixtures, compliance vectors and doc examples now carry it.
  • The adcp_version echo is accepted and declared on every response.
  • Responses accept replayed: false.
  • A task-level message longer 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.
  • A working or input-required payload rejects any other status.
  • 3.2.x never allowed status on list_products (closed) or the decline_proposals success 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.
  • Sellers on @adcp/sdk that strip status from 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 keeps status where 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 by npm test) enumerates every tool's response and async status schemas from discoverTools. It asserts that:

  1. Every source schema composes protocol-envelope.
  2. No response root or status branch is closed.
  3. Every response node accepts the envelope's edge values: replayed: false and a 2,000-character message. A response node is a root, a composition branch, or a document a branch names, including core/compact-task-*.
  4. No response node redeclares an envelope field except the narrowings listed above, narrows message length, or pins replayed.
  5. task_id: "" adds exactly its minLength error on every response node and on every request that carries a task_id.
  6. Every status body composes its core/task-<status>.json (submitted, working, input-required), no schema keys task_id on status through if/then, and a submitted response without task_id fails on core/task-submitted.json's required alone.
  7. Every task-level message refers to core/task-message.json, and a 2,001-character message adds exactly its maxLength error 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, as package.json requires, and checks that every vector ran.

The new tenant-smoke case checks check_governance, report_plan_outcome (with its replay), creative_approval and acquire_rights at the current release and at a 3.0 pin.

Check Result
build:schemas, test:schemas, test:composed, test:json-schema, test:examples, test:example-validation pass
test:response-envelope-composition 16 tests, 230 with the per-node subtests, pass
test:version-envelope, test:oneof-discriminators, test:storyboard-response-schema, test:sdk-response-conformance, typecheck pass
test:mcp-schema-projection pass. The projection grows from 22,959,518 to 23,426,967 B (every response bundles the envelope), so the cap is raised from 22 MiB to 23 MiB.
test:release-workflow 81 failures, identical on main
test:additive-minor pass, 0 new. The baseline is not grown.
Storyboards, current surface (candidate mode, @adcp/sdk 17.0.0-beta.6) every tenant meets its floors, and every required-clean and required-exact storyboard passes, including compact_direct_buy_lifecycle (8P), compact_product_lifecycle (10P), declined_proposal_refinement (7P) and declined_proposal_execution (9P). /sales: 178 clean, 1,212 steps.
Storyboards, 3.0-compat every tenant meets its floors

Known red checks

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.

KonstantinMirin and others added 10 commits October 27, 2026 21:27
…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.
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; #8523 and #8524 add its rows and entries together.
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

gitguardian Bot commented Oct 28, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
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
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. 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


🦉 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.

@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 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:

  1. Gated paths (static/schemas/source/**) with review_decision != APPROVED.
  2. The change tightens status to 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) matches static/schemas/source/**; static/schemas/source/account/list-account-changes-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/list-accounts-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/report-usage-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/request-account-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/sync-accounts-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/sync-governance-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/acquire-rights-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/creative-approval-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/get-brand-identity-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/get-rights-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/search-brands-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/update-rights-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/verify-brand-claim-response.json (modified) matches static/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.

@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 — 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) and verdict/outcome_state/approval_status with status: 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) matches static/schemas/source/**; static/schemas/source/account/list-account-changes-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/list-accounts-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/report-usage-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/request-account-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/sync-accounts-response.json (modified) matches static/schemas/source/**; static/schemas/source/account/sync-governance-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/acquire-rights-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/creative-approval-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/get-brand-identity-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/get-rights-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/search-brands-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/update-rights-response.json (modified) matches static/schemas/source/**; static/schemas/source/brand/verify-brand-claim-response.json (modified) matches static/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.
@KonstantinMirin

Copy link
Copy Markdown
Collaborator Author

@adcp/sdk is bumped to 17.0.0-beta.6 (91bd213), which carries adcontextprotocol/adcp-client#3493. This PR's built schemas are registered in the training agent under the SDK's pinned 3.3.0-beta.1 (24aa1b0). Only a trusted candidate process registers them (NODE_ENV=test and candidate mode); production keeps the published pin. The registration goes once the SDK pins a 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 compact_direct_buy_lifecycle (8P), compact_product_lifecycle (10P), declined_proposal_refinement (7P) and declined_proposal_execution (9P). /sales is at 178 clean, 1,212 steps.

Two things for the SDK:

  • The ESM status rule (envelope-status-compat.mjs) reads schemas through the SDK's CommonJS schema loader, which is a separate module instance. An external root registered through the ESM @adcp/sdk/testing, including the storyboard runner's scoped root, doesn't reach it. The training agent registers with both instances to work around this.
  • beta.6 no longer packages 3.3.0-beta.0. The training agent retains it from dist/schemas for the creative 3.3 beta sandbox.

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.

Copy link
Copy Markdown
Contributor

main is now on @adcp/sdk 17.0.0-beta.6 (#8597, 53e7677df), which carries adcp-client#3493: version-gated envelope handling.

For rebasing and re-running storyboards:

  • list_products, decline_proposals, list_collections and list_proposals now keep status whenever the served release's published schema composes core/protocol-envelope.json; a 3.2.x seller (closed arms with no status) still omits it. The release's own schema decides, so this branch's 3.3 composition should now see status kept without a code change.
  • relaxResponseRoot() is gone. A bundle that composes the envelope is validated as published, status required. A closed pre-composition root (3.2.7 and earlier) accepts only the envelope fields that release's own protocol-envelope.json defines.
  • Training agent and storyboards on main behave as before for 3.2-shaped responses; no status expectation changed in tenant smoke tests or the storyboard matrix.
  • Heads-up from beta.5: the SDK now packages, advertises and negotiates only 3.3-beta.1. chore(deps): @adcp/sdk 17.0.0-beta.6 #8597 moved the training agent's 3.3 candidate and creative-sandbox pins from 3.3-beta.0 to 3.3-beta.1. If this branch touches those pins or tests that hard-code 3.3-beta.0, expect conflicts there.

Rebase onto main and re-run the storyboards.

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.
@KonstantinMirin

Copy link
Copy Markdown
Collaborator Author

Merged main with #8597 (bbaf49e). On the overlap I took main's version. The 3.3 candidate and creative sandbox pins are now 3.3-beta.1, and this branch's workaround that kept 3.3.0-beta.0 from dist/schemas is gone. The registration of this PR's built schemas under 3.3.0-beta.1 in candidate mode stays. Its second registration with the SDK's CommonJS instance is still needed on beta.6: without it, compact_product_lifecycle fails on a missing status again. The code comment now cites adcontextprotocol/adcp-client#3519.

The tenant-smoke list_products case still grades against the newest published 3.2 schema, because the response is served at 3.2. It doesn't conflict with main: on main the 3.3 source doesn't require status, so main had no reason to change that case.

Storyboards, current surface in candidate mode: every tenant meets its floors, and all 41 required-clean and required-exact checks pass. That includes compact_direct_buy_lifecycle (8P), compact_product_lifecycle (10P), declined_proposal_refinement (7P) and declined_proposal_execution (9P). /sales is at 178 clean, 1,212 steps.

Still red, all SDK-side:

@bokelley
bokelley merged commit 931a9d6 into main Oct 30, 2026
66 of 68 checks passed
@bokelley
bokelley deleted the fix/compose-protocol-envelope-on-task-responses branch October 30, 2026 12:20
bokelley pushed a commit that referenced this pull request Oct 30, 2026
….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.
bokelley added a commit that referenced this pull request Nov 9, 2026
… 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>
bokelley added a commit that referenced this pull request Nov 9, 2026
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>
bokelley added a commit that referenced this pull request Nov 10, 2026
… 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>
bokelley added a commit that referenced this pull request Nov 10, 2026
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>
bokelley added a commit that referenced this pull request Nov 10, 2026
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>
bokelley added a commit that referenced this pull request Nov 12, 2026
* 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>
bokelley added a commit that referenced this pull request Nov 12, 2026
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>
bokelley added a commit that referenced this pull request Nov 12, 2026
…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>
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

2 participants