Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/reopen-proposal-flow-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"adcontextprotocol": patch
---

Reopen the 3.2 proposal-flow schemas that were closed against DR-0019, as a correction recorded in DR-0036 (#8501). DR-0019 keeps request roots, response roots, and container objects open. These schemas now set `additionalProperties: true` at 24 locations:

- 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.json` (`buy_products` and `accept_proposal`) and of `control-media-buy-response.json`;
- `core/canonical-proposal.json`, `core/canonical-product.json`, `media-buy/product-discovery-criteria.json`, `media-buy/proposal-refinement.json`, `media-buy/package-control.json`, and `core/opportunity-context.json`.

Every payload that was valid stays valid. The rejections that closure provided are now stated by name: `core/canonical-product.json` forbids each field that only the legacy `core/product.json` defines, such as `format_ids`, `is_custom`, and `installments`; the `request_proposals` response forbids a root `feed_version` or `pricing_version`; and the error and submitted branches of `media-buy-commitment-response.json` and `control-media-buy-response.json` forbid `warnings`. This patch adds no fields. `commercial-terms.json` and the purchases inside it stay closed.
19 changes: 19 additions & 0 deletions docs/reference/migration/3-1-to-3-2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,25 @@ Absence of the declaration means use the 3.x compatibility facades. Retry a
stateful request with the same tool name, idempotency key, and payload; an
idempotency key does not create replay identity across old and new task names.

**Correction: the proposal-flow schemas are open.** 3.2.7 and earlier published
these tasks' request and response schemas closed, with
`additionalProperties: false`. They also closed `canonical-proposal`,
`canonical-product`, `product-discovery-criteria`, `proposal-refinement`,
`package-control`, and `opportunity-context`.
[DR-0019](https://github.com/adcontextprotocol/adcp/blob/main/governance/decisions/DR-0019-closed-row-schemas-deliberate-class.md)
keeps roots and containers open, so later 3.2 patches reopen them. This is a
correction recorded in
[DR-0036](https://github.com/adcontextprotocol/adcp/blob/main/governance/decisions/DR-0036-closed-schemas-stay-closed-within-a-major.md)
([#8501](https://github.com/adcontextprotocol/adcp/issues/8501)). Every payload
that was valid stays valid. A field that only the legacy Product defines,
such as `format_ids`, `is_custom`, or `installments`, is still rejected on a
canonical product. A root `feed_version` or `pricing_version` on a
`request_proposals` response, and `warnings` on an error or submitted
`buy_products`, `accept_proposal`, or `control_media_buy` response, are also
still rejected. `commercial_terms` and the purchases inside it stay closed. A
validator on 3.2.7 or earlier still rejects unknown fields on these schemas, so
don't send fields the served release doesn't define.

### Scope idempotency to mutations

AdCP 3.2 requires durable replay for state-mutating requests, not guaranteed
Expand Down
9 changes: 5 additions & 4 deletions server/tests/unit/product-discovery-schema-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('product discovery MCP schema parity', () => {
const source = sourceSchema(fileName);
expect(runtime.required ?? []).toEqual(source.required ?? []);
expect(runtime.dependencies ?? {}).toEqual(source.dependencies ?? {});
expect(runtime.additionalProperties).toBe(false);
expect(runtime.additionalProperties).toBe(source.additionalProperties);
expect(Object.keys(runtime.properties).sort()).toEqual(Object.keys(source.properties).sort());
if (source.properties.idempotency_key) {
expect(runtime.properties.idempotency_key).toMatchObject({
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('product discovery MCP schema parity', () => {
expect(runtimeRefinement).toMatchObject({
type: sourceRefinement.type,
required: sourceRefinement.required,
additionalProperties: false,
additionalProperties: sourceRefinement.additionalProperties,
});

// Repository-local refs must be bundled because MCP consumers do not have
Expand All @@ -78,11 +78,12 @@ describe('product discovery MCP schema parity', () => {
uniqueItems: true,
items: { enum: expect.arrayContaining(['product_id', 'format_options', 'pricing_options']) },
});
expect(resolveLocalRef(list, list.properties.criteria)).toMatchObject({ additionalProperties: false });
expect(resolveLocalRef(list, list.properties.criteria))
.toMatchObject({ additionalProperties: sourceSchema('product-discovery-criteria').additionalProperties });
expect(resolveLocalRef(refineTool, refineTool.properties.refinements.items))
.toMatchObject({
required: ['proposal_id'],
additionalProperties: false,
additionalProperties: sourceRefinement.additionalProperties,
// Definition annotations are intentionally stripped from tools/list;
// dispatch still applies the source-schema default semantics.
properties: { action: { enum: ['revise', 'finalize'] } },
Expand Down
27 changes: 26 additions & 1 deletion static/schemas/source/core/canonical-product.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,30 @@
}
}
],
"additionalProperties": false
"not": {
"anyOf": [
{ "required": ["audience_activation"] },
{ "required": ["cancellation_policy"] },
{ "required": ["conversion_tracking"] },
{ "required": ["creative_policy"] },
{ "required": ["data_provider_signals"] },
{ "required": ["delivery_measurement"] },
{ "required": ["enforced_policies"] },
{ "required": ["format_ids"] },
{ "required": ["included_signals"] },
{ "required": ["installments"] },
{ "required": ["is_custom"] },
{ "required": ["material_submission"] },
{ "required": ["measurement_readiness"] },
{ "required": ["metric_optimization"] },
{ "required": ["outcome_measurement"] },
{ "required": ["product_card"] },
{ "required": ["product_card_detailed"] },
{ "required": ["property_targeting_allowed"] },
{ "required": ["signal_targeting_options"] },
{ "required": ["trusted_match"] },
{ "required": ["vendor_metric_optimization"] }
]
},
"additionalProperties": true
}
2 changes: 1 addition & 1 deletion static/schemas/source/core/canonical-proposal.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,5 @@
"then": { "properties": { "commercial_terms": { "required": ["cancellation_terms"] } } }
}
],
"additionalProperties": false
"additionalProperties": true
}
2 changes: 1 addition & 1 deletion static/schemas/source/core/opportunity-context.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,5 @@
"then": { "required": ["close_detail"] }
}
],
"additionalProperties": false
"additionalProperties": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
"ext": { "$ref": "/schemas/core/ext.json" }
},
"required": ["idempotency_key", "account", "proposal_id", "proposal_terms_digest"],
"additionalProperties": false,
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["packages"] },
Expand Down
2 changes: 1 addition & 1 deletion static/schemas/source/media-buy/buy-products-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
}
}
],
"additionalProperties": false,
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["packages"] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
}
}
],
"additionalProperties": false,
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["creatives"] },
Expand Down
20 changes: 15 additions & 5 deletions static/schemas/source/media-buy/control-media-buy-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"replayed": { "type": "boolean", "const": true }
},
"required": ["status", "media_buy_id", "revision"],
"additionalProperties": false,
"additionalProperties": true,
"not": { "required": ["errors"] }
},
{
Expand All @@ -61,8 +61,13 @@
"replayed": { "type": "boolean", "const": true }
},
"required": ["status", "errors"],
"additionalProperties": false,
"not": { "required": ["media_buy_id"] }
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["media_buy_id"] },
{ "required": ["warnings"] }
]
}
},
{
"title": "Control Submitted",
Expand All @@ -77,8 +82,13 @@
"replayed": { "type": "boolean", "const": true }
},
"required": ["status", "task_id"],
"additionalProperties": false,
"not": { "required": ["media_buy_id"] }
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["media_buy_id"] },
{ "required": ["warnings"] }
]
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@
}
},
"required": ["idempotency_key", "declines"],
"additionalProperties": false
"additionalProperties": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,5 @@
"ext": { "$ref": "/schemas/core/ext.json" },
"replayed": { "type": "boolean", "const": true }
},
"additionalProperties": false
"additionalProperties": true
}
2 changes: 1 addition & 1 deletion static/schemas/source/media-buy/list-products-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,5 @@
}
}
],
"additionalProperties": false
"additionalProperties": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@
{ "required": ["unchanged"] }
]
},
"additionalProperties": false
"additionalProperties": true
}
20 changes: 15 additions & 5 deletions static/schemas/source/media-buy/media-buy-commitment-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
}
},
"required": ["status", "media_buy_id", "revision", "accepted_proposal", "purchase_bindings", "available_actions"],
"additionalProperties": false,
"additionalProperties": true,
"not": { "required": ["errors"] }
},
{
Expand All @@ -137,8 +137,13 @@
"replayed": { "type": "boolean", "const": true }
},
"required": ["status", "errors"],
"additionalProperties": false,
"not": { "required": ["media_buy_id"] }
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["media_buy_id"] },
{ "required": ["warnings"] }
]
}
},
{
"title": "Commitment Submitted",
Expand All @@ -153,8 +158,13 @@
"replayed": { "type": "boolean", "const": true }
},
"required": ["status", "task_id"],
"additionalProperties": false,
"not": { "required": ["media_buy_id"] }
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["media_buy_id"] },
{ "required": ["warnings"] }
]
}
}
]
}
2 changes: 1 addition & 1 deletion static/schemas/source/media-buy/package-control.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
}
}
},
"additionalProperties": false,
"additionalProperties": true,
"not": {
"anyOf": [
{ "required": ["creatives"] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@
"ext": { "type": "object", "additionalProperties": true }
},
"minProperties": 1,
"additionalProperties": false
"additionalProperties": true
}
2 changes: 1 addition & 1 deletion static/schemas/source/media-buy/proposal-refinement.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,5 @@
}
]
},
"additionalProperties": false
"additionalProperties": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,5 @@
}
},
"required": ["idempotency_key", "refinements"],
"additionalProperties": false
"additionalProperties": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
}
}
],
"additionalProperties": false
"additionalProperties": true
},
"allOf": [
{
Expand Down Expand Up @@ -192,5 +192,5 @@
{ "required": ["pagination"] }
]
},
"additionalProperties": false
"additionalProperties": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@
}
}
],
"additionalProperties": false
"additionalProperties": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,10 @@
{ "required": ["refinement_applied"] },
{ "required": ["pagination"] },
{ "required": ["unchanged"] },
{ "required": ["wholesale_feed_version"] }
{ "required": ["wholesale_feed_version"] },
{ "required": ["feed_version"] },
{ "required": ["pricing_version"] }
]
},
"additionalProperties": false
"additionalProperties": true
}
5 changes: 4 additions & 1 deletion tests/buyer-fixture-products-schema.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,11 @@ test('a product valid under only one shape is rejected', () => {
},
};
assert.deepEqual(productErrors(ajv, dualShape), []);
// installments exists only on the legacy Product, so the canonical Product
// rejects it by name.
const errors = productErrors(ajv, { ...dualShape, installments: [] });
assert.ok(errors.some((e) => e.startsWith('/schemas/core/canonical-product.json') && e.includes('installments')), errors.join('\n'));
assert.ok(errors.some((e) => e.startsWith('/schemas/core/canonical-product.json')), errors.join('\n'));
assert.ok(!errors.some((e) => e.startsWith('/schemas/core/product.json')), errors.join('\n'));
const missing = productErrors(ajv, { product_id: 'p', name: 'P' });
assert.ok(missing.some((e) => e.startsWith('/schemas/core/product.json')), missing.join('\n'));
});
9 changes: 8 additions & 1 deletion tests/mcp-schema-projection.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const STORYBOARD_DIR = path.join(REPO_ROOT, 'static', 'compliance', 'source');
const LATEST_DIR = path.join(REPO_ROOT, 'dist', 'schemas', 'latest');
const PROJECTION_DIR = path.join(LATEST_DIR, 'mcp', MCP_PROTOCOL_VERSION);
const PRODUCTION_PROFILE_DIR = path.join(PROJECTION_DIR, 'profiles', 'production');
// The canonical Product's `not`, as the projection serializes it.
const CANONICAL_PRODUCT_PROHIBITION = JSON.stringify({
not: JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'core', 'canonical-product.json'), 'utf8')).not,
}).slice(1, -1);
// The generated profiles describe the active surface of the package's release
// line. Before the line's GA (x.y.0-rc.N) that is the x.y.0 surface the tool
// manifest declares via added_in; once the line has a stable release it is the
Expand Down Expand Up @@ -983,7 +987,10 @@ test('generated MCP projection covers every tool within AdCP safety bounds', ()
if (['list_products', 'request_proposals', 'refine_proposals'].includes(toolName)) {
assert.match(output, /Canonical Product/,
`${toolName} output must use the canonical-only Product view`);
assert.doesNotMatch(output, /format_ids|format-id\.json|v1_format_ref|update_packages|"update_media_buy"/,
// The canonical Product forbids the legacy Product's keys by name;
// forbidding them does not expose them.
const exposed = output.replaceAll(CANONICAL_PRODUCT_PROHIBITION, '');
assert.doesNotMatch(exposed, /format_ids|format-id\.json|v1_format_ref|update_packages|"update_media_buy"/,
`${toolName} output must not expose the legacy Product graph`);
}
}
Expand Down
4 changes: 4 additions & 0 deletions tests/products-only-brief-compatibility.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ test('2.5, 3.0, and 3.1 preserve products-only brief discovery through an explic
const fabricatedFence = structuredClone(vector.compact_projection);
fabricatedFence.feed_version = `synthetic-${vector.source_version}`;
assert.equal(validateCompact(fabricatedFence), false, `${vector.source_version} must not fabricate a feed fence`);

const fabricatedPricingFence = structuredClone(vector.compact_projection);
fabricatedPricingFence.pricing_version = `synthetic-${vector.source_version}`;
assert.equal(validateCompact(fabricatedPricingFence), false, `${vector.source_version} must not fabricate a pricing fence`);
}
});

Expand Down
Loading