Skip to content

feat(schemas): acceptance of signed commercial terms binds every member - #8884

Open
KonstantinMirin wants to merge 2 commits into
adcontextprotocol:mainfrom
KonstantinMirin:schemas/signed-terms-acceptance
Open

KonstantinMirin wants to merge 2 commits into
adcontextprotocol:mainfrom
KonstantinMirin:schemas/signed-terms-acceptance

Conversation

@KonstantinMirin

@KonstantinMirin KonstantinMirin commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

terms_digest is SHA-256 over the RFC 8785 JCS bytes of the whole commercial_terms value. On main, many of the 92 schemas reachable from media-buy/commercial-terms.json have open nodes (core/targeting.json, core/format-option-ref.json, core/ext.json, and others), so members a party doesn't recognize can already sit inside the signed bytes. The spec doesn't say what accepting those terms means for such members. This PR states it:

  1. The digest covers the entire commercial_terms JSON value: every member at every depth, including ext and members the party doesn't recognize.
  2. Accepting signed terms binds the accepting party to every member. A party that can't interpret every member MUST NOT accept them: a buyer declines with decline_proposals reason UNRECOGNIZED_TERMS, a seller rejects the request with UNRECOGNIZED_TERMS, and a governance agent denies through its existing deny path. A member whose schema defines it as opaque, such as context, is interpreted by that definition.
  3. A party computes or verifies the digest over the JSON value as received, never over a re-serialized parsed model, which can drop members the model doesn't declare.

Changes

  • enums/error-code.json: new UNRECOGNIZED_TERMS, with description and enumMetadata. I classified it correctable: the buyer can remove the member, or send it only to a seller that interprets it, and retry. Nothing about it needs operator intervention, so terminal didn't fit. It is distinct from UNSUPPORTED_FEATURE (a field the seller recognizes but doesn't support) and INVALID_REQUEST (a schema failure). scripts/error-code-drift-dispositions.json holds it for 3.3, and docs/snippets/compliance-error-codes.mdx is regenerated.
  • enums/proposal-decline-reason.json: new UNRECOGNIZED_TERMS. Buyer and seller use the same UNRECOGNIZED_TERMS on purpose, with the same spelling: it is one condition, seen from either side, so it should be one code. That makes it the only uppercase value in proposal-decline-reason, which shows the inconsistency between our error and reason vocabularies. I've suggested addressing that in 4.0 in 4.0: one UPPER_SNAKE_CASE vocabulary for error, reason and outcome codes, defined per protocol and checked by the build #8885.
  • The rule itself: the media-buy/commercial-terms.json description, the terms_digest description in core/canonical-proposal.json, and the proposal description in governance/check-governance-request.json.
  • Docs: step 3 "Verify terms independently" in the proposal negotiation guide (the digest is computed over the value as received; the acceptance rule for every party), buy_products (the seller side), decline_proposals, check_governance, and a line in pricing models so "ignore unrecognized fields in price_breakdown" doesn't read as permission to accept terms carrying them.
  • Corrected the release notes, What's new in 3.3, the 3.2 to 3.3 migration guide, and the 4.0 compatibility page. They said commercial-terms stays closed because "terms_digest signs exactly those terms". The schema stays closed because a minor release never changes a closed schema (DR-0036), and the pages now state the acceptance rule instead.
  • Storyboard media_buy_seller/unrecognized_terms_rejected, listed in the media-buy protocol's requires_scenarios and gated on media_buy.lifecycle_tools containing buy_products. The buyer's buy_products purchases become the accepted proposal's commercial_terms, so the seller is the accepting party there. A control phase first buys the product with a plain purchase and expects a MediaBuy. The rejection phase depends on it and sends the identical purchase with one added member, adcp_compliance_undefined_member, in format_option_refs[0], and expects UNRECOGNIZED_TERMS. The name is reserved as undefined, so no conformant seller can legitimately interpret it. core/format-option-ref.json is open, so the request is schema-valid, and the member is the only difference between the two requests. If the control fails, the runner skips the rejection step (prerequisite_failed) instead of grading it. Nothing in the spec makes a second buy of the same product fail: the two requests use different idempotency keys, and there is no duplicate-buy rule. accept_proposal isn't covered because the buyer sends only proposal_id and proposal_terms_digest there, never the terms.
  • Training agent: before this change it accepted that purchase and recorded the undefined member inside the signed commercial_terms. handleBuyProducts now rejects any purchase member the source schemas don't declare, at any depth, with UNRECOGNIZED_TERMS and error.field set to its path. The walk is undeclaredMembers in served-release-fields.ts and reuses that module's schema walk. A unit test covers an undeclared member, an ext member, and an opaque context member.
  • Changeset: minor (new enum values and a new normative requirement).

Not changed

Testing

All run in a clean worktree off origin/main at 0f337b4, on Node 24.21.0:

  • npm run build:schemas: passes.
  • npm run test:schemas: passes.
  • npm run typecheck: passes.
  • npm run build:compliance -- --check: passes.
  • npm run test:compliance-snippets: passes.
  • npm run test:additive-minor: passes (enum additions are allowed in a minor release).
  • npm run test:docs-nav, npm run check:owned-links, npm run lint:schema-links: pass.
  • npm run test:mcp-schema-projection: passes.
  • All 23 npm run test:storyboard-* lints: pass.
  • node scripts/lint-error-code-drift.cjs: passes.
  • node tests/json-schema-validation.test.cjs --file and node tests/snippet-validation.test.cjs --file on each of the 9 edited docs pages: pass.
  • node scripts/check-changeset-protocol-scope.cjs origin/main and npx @changesets/cli status --since=origin/main: pass, one minor bump.
  • npx vitest run --config server/vitest.config.ts server/tests/unit/training-agent-served-release-fields.test.ts: 6 of 6 pass.
  • New storyboard against /sales, in the same candidate-version mode CI uses: with the training-agent change reverted, the control step passes and only the UNRECOGNIZED_TERMS check in reject_unrecognized_member fails (5 of 6 steps pass). With the fix it passes 6 of 6. With the control deliberately broken (an unknown pricing option), the control fails and the rejection step is reported as skipped (prerequisite_failed), not graded. media_buy_seller/compact_direct_buy_lifecycle still passes 8 of 8.
  • The pre-commit hook (root unit tests, the full server unit suite, typecheck) passed.
  • The pre-push storyboard matrix met every floor on both the current source and the released 3.0.28 bundle. On current source, /sales had 196 of 196 applicable storyboards clean and 1422 passing steps (floors 191 and 1400).

Merge timing

Intended for 3.3, before the Nov 16 release candidate.

terms_digest is SHA-256 over the JCS bytes of the whole commercial_terms
value, and many schemas reachable from commercial-terms.json are already
open, so members a party does not recognize can sit inside the signed
bytes. The spec never said what accepting such terms means.

State the rule in commercial-terms.json, the terms_digest description,
check-governance-request.json, and the docs that specify the digest and
acceptance:

- The digest covers the entire commercial_terms JSON value, every member
  at every depth, including ext and unrecognized members.
- Accepting the terms binds the accepting party to every member. A party
  that cannot interpret every member must not accept them: a buyer
  declines with the new decline reason unrecognized_terms, a seller
  rejects the request with the new error code UNRECOGNIZED_TERMS, and a
  governance agent denies the check. A member whose schema defines it as
  opaque, such as context, is interpreted by that definition.
- A party computes or verifies the digest over the JSON value as
  received, never over a re-serialized parsed model.

Correct the release notes, What's new in 3.3, and both migration guides,
which gave "terms_digest signs exactly those terms" as a reason to keep
commercial-terms closed. It stays closed because a minor release never
changes a closed schema.

Add the media_buy_seller/unrecognized_terms_rejected storyboard: a
buy_products purchase carries an undefined member in format_option_refs[0],
an open schema, and must fail with UNRECOGNIZED_TERMS; the same purchase
without it must succeed. The training agent recorded the member inside
the accepted commercial_terms; it now rejects any purchase member the
source schemas do not declare.
Spell the decline reason UNRECOGNIZED_TERMS, like the error code. It
names the same condition from the buyer's side, and its description now
says so.

Split the storyboard so the control purchase runs in its own phase
before the rejection, and make the rejection phase depend on it. The
UNRECOGNIZED_TERMS check is graded only after the identical purchase is
proven acceptable; if the control fails, the runner skips the check.

Replace the undefined member with adcp_compliance_undefined_member, a
name reserved as undefined so that no conformant seller can legitimately
interpret it.
@mintlify

mintlify Bot commented Nov 10, 2026

Copy link
Copy Markdown

Preview approval required

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

Approve preview

@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 five files under static/schemas/source/**, which fall under the repo's hard, path-based approval gate (gated_paths: true). The current review_decision is REVIEW_REQUIRED, not APPROVED, so the gate is not satisfied (decision table row 2).

Gated files requiring review:

  • static/schemas/source/core/canonical-proposal.json
  • static/schemas/source/enums/error-code.json
  • static/schemas/source/enums/proposal-decline-reason.json
  • static/schemas/source/governance/check-governance-request.json
  • static/schemas/source/media-buy/commercial-terms.json

The reviewer found the change clean: additive Normative surface (new UNRECOGNIZED_TERMS error code + decline_proposals reason, acceptance rule in commercial-terms.json/terms_digest/check-governance-request.json), schema↔docs coherent across all nine edited pages, a minor changeset matching the additive enum/requirement surface, no oneOf regression, and no released-artifact mutation. No blocking or medium findings. The uppercase UNRECOGNIZED_TERMS casing is acknowledged and deferred to #8885 — not a finding.

Despite the clean diff, the gated-paths hard rule forbids auto-approve until a human/CODEOWNERS approval lands. Once review_decision becomes APPROVED, this PR falls through to the normal table and can approve on its clean findings.

Why human review

  • Touches gated schema paths under static/schemas/source/** (canonical-proposal.json, error-code.json, proposal-decline-reason.json, check-governance-request.json, commercial-terms.json) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS review required before merge.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/canonical-proposal.json (modified) matches static/schemas/source/**; static/schemas/source/enums/error-code.json (modified) matches static/schemas/source/**; static/schemas/source/enums/proposal-decline-reason.json (modified) matches static/schemas/source/**; static/schemas/source/governance/check-governance-request.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/commercial-terms.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

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

Thanks Konstantin. This is the rule that makes open schemas inside signed terms safe, and I want it in the 3.3 RC. I'm approving the rule as written:

  • the digest covers every member at every depth, including ext;
  • accepting binds you to all of them;
  • a party that can't interpret a member refuses;
  • the digest is computed over the value as received.

I'm also happy with the correctable classification and the storyboard.

One change before merge: please make the decline reason lowercase, unrecognized_terms, in enums/proposal-decline-reason.json. Every other value in that enum is lowercase snake_case, and the enum is closed. An uppercase value would stay inconsistent through all of 3.x, which #8885 can only fix in 4.0. Keep UNRECOGNIZED_TERMS as the error code in enums/error-code.json, matching the other error codes.

It's still one condition seen from either side, so keep the cross-references. Each vocabulary just keeps its own casing, e.g. "a buyer declines with reason unrecognized_terms; a seller reports error code UNRECOGNIZED_TERMS". The decline-reason spelling also appears in:

  • the commercial-terms.json and canonical-proposal.json descriptions;
  • check-governance-request.json, if it names the reason;
  • the decline-reason enumDescriptions entry;
  • the proposal-negotiation, decline_proposals, pricing-models and migration/release docs;
  • the changeset.

Ordering with #8866: whichever of the two lands first, the other rebases. #8866 is taking your review now. It drops the "signed commercial terms" structural reason in favour of "held closed until 4.0", so the three docs pages should end up saying the same thing as this PR.

This branch has not been deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants