Skip to content

Commit 2ee6ac7

Browse files
bokelleyclaude
andauthored
fix(schemas): accept envelope-level replayed on 15 response schemas (#2839) (#2862)
Property-list, collection-list, and governance response schemas sealed their envelope root with additionalProperties: false, rejecting the replayed boolean that security.mdx requires sellers to inject on mutating responses. Flip root additionalProperties to true on all 15; declare replayed explicitly on the 8 mutating ones so AJV still type-checks it. Nested-body strictness preserved. Regression coverage in composed-schema-validation: per-schema acceptance, a negative test, a branch-aware structural lint, and a drift guard against core/protocol-envelope.json. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2df6cab commit 2ee6ac7

17 files changed

Lines changed: 242 additions & 16 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"adcontextprotocol": patch
3+
---
4+
5+
Response schemas across property-list, collection-list, and governance families now accept the envelope-level `replayed` field that the seller's idempotency layer injects at response time.
6+
7+
Fifteen `*-response.json` schemas previously declared `additionalProperties: false` at the root, so AJV validators compiled from them rejected `replayed: true` / `replayed: false` — even though `docs/building/implementation/security.mdx` (the idempotency storyboard) requires sellers to emit it on mutating responses. This produced a two-faced contract where `create_media_buy` accepted the same envelope field (via branch-level `additionalProperties: true` on its `oneOf`) while `create_property_list`, `sync_plans`, and others did not. Media-buy, signals, creative, content-standards, and sponsored-intelligence responses already accept envelope-level fields through their `oneOf` branches and did not need changing — only schemas with a root-level seal were affected.
8+
9+
Affected schemas: property-list family (`create`, `update`, `delete`, `get`, `list`, `validate_property_delivery`), collection-list family (`create`, `update`, `delete`, `get`, `list`), and governance (`check_governance`, `get_plan_audit_logs`, `report_plan_outcome`, `sync_plans`).
10+
11+
Fix: root-level `additionalProperties` flipped to `true` on all 15 so envelope-level fields pass through. The eight mutating responses (`create_*`, `update_*`, `delete_*` × 2 families, `report_plan_outcome`, `sync_plans`) also declare `replayed: { type: boolean }` explicitly — consistent with how `context` and `ext` are declared today — so AJV still type-checks it. Nested body `additionalProperties: false` is left intact; envelope extensibility is a root-level concession, not a license for drift inside list bodies.
12+
13+
Regression coverage added in `tests/composed-schema-validation.test.cjs`: per-schema acceptance tests, a negative test (`replayed: "true"` as string must fail), a structural lint that walks every task-family `*-response.json` (including `oneOf`/`anyOf`/`allOf` branches) and fails on any sealed envelope without `replayed` declared, and a drift guard that asserts every inlined `replayed` description matches the canonical definition in `core/protocol-envelope.json`.
14+
15+
Resolves #2839.

static/schemas/source/collection/create-collection-list-response.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
"type": "string",
1414
"description": "Token that authorizes sellers to fetch this list via get_collection_list. Only returned at creation time — buyers MUST store it in a secret manager. Scoped to this one list_id; MUST NOT be reused across lists. Governance agents MUST issue a distinct token per seller so per-relationship revocation is possible. Tokens MUST NOT be logged, appear in cache keys, or echo in error responses. delete_collection_list MUST revoke the token immediately; compromise-driven revocation MUST also signal cache invalidation to sellers (reduced cache_valid_until or a list-changed webhook). See Security considerations in docs/governance/collection/tasks/collection_lists."
1515
},
16+
"replayed": {
17+
"type": "boolean",
18+
"description": "Set to true when this response is a cached replay returned for an idempotency_key that was already processed. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, and any downstream system that assumes exactly-once event semantics. Only present on responses to mutating requests that carry idempotency_key.",
19+
"default": false
20+
},
1621
"context": {
1722
"$ref": "/schemas/core/context.json"
1823
},
@@ -24,5 +29,5 @@
2429
"list",
2530
"auth_token"
2631
],
27-
"additionalProperties": false
32+
"additionalProperties": true
2833
}

static/schemas/source/collection/delete-collection-list-response.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
"description": "ID of the deleted list",
1515
"x-entity": "collection_list"
1616
},
17+
"replayed": {
18+
"type": "boolean",
19+
"description": "Set to true when this response is a cached replay returned for an idempotency_key that was already processed. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, and any downstream system that assumes exactly-once event semantics. Only present on responses to mutating requests that carry idempotency_key.",
20+
"default": false
21+
},
1722
"context": {
1823
"$ref": "/schemas/core/context.json"
1924
},
@@ -25,5 +30,5 @@
2530
"deleted",
2631
"list_id"
2732
],
28-
"additionalProperties": false
33+
"additionalProperties": true
2934
}

static/schemas/source/collection/get-collection-list-response.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,5 @@
125125
"required": [
126126
"list"
127127
],
128-
"additionalProperties": false
128+
"additionalProperties": true
129129
}

static/schemas/source/collection/list-collection-lists-response.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@
2525
"required": [
2626
"lists"
2727
],
28-
"additionalProperties": false
28+
"additionalProperties": true
2929
}

static/schemas/source/collection/update-collection-list-response.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"$ref": "/schemas/collection/collection-list.json",
1010
"description": "The updated collection list"
1111
},
12+
"replayed": {
13+
"type": "boolean",
14+
"description": "Set to true when this response is a cached replay returned for an idempotency_key that was already processed. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, and any downstream system that assumes exactly-once event semantics. Only present on responses to mutating requests that carry idempotency_key.",
15+
"default": false
16+
},
1217
"context": {
1318
"$ref": "/schemas/core/context.json"
1419
},
@@ -19,5 +24,5 @@
1924
"required": [
2025
"list"
2126
],
22-
"additionalProperties": false
27+
"additionalProperties": true
2328
}

static/schemas/source/governance/check-governance-response.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@
149149
"plan_id",
150150
"explanation"
151151
],
152-
"additionalProperties": false,
152+
"additionalProperties": true,
153153
"allOf": [
154154
{
155155
"if": {

static/schemas/source/governance/get-plan-audit-logs-response.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,5 +420,5 @@
420420
"required": [
421421
"plans"
422422
],
423-
"additionalProperties": false
423+
"additionalProperties": true
424424
}

static/schemas/source/governance/report-plan-outcome-response.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
},
7070
"additionalProperties": false
7171
},
72+
"replayed": {
73+
"type": "boolean",
74+
"description": "Set to true when this response is a cached replay returned for an idempotency_key that was already processed. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, and any downstream system that assumes exactly-once event semantics. Only present on responses to mutating requests that carry idempotency_key.",
75+
"default": false
76+
},
7277
"context": {
7378
"$ref": "/schemas/core/context.json"
7479
},
@@ -80,5 +85,5 @@
8085
"outcome_id",
8186
"status"
8287
],
83-
"additionalProperties": false
88+
"additionalProperties": true
8489
}

static/schemas/source/governance/sync-plans-response.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@
100100
"additionalProperties": false
101101
}
102102
},
103+
"replayed": {
104+
"type": "boolean",
105+
"description": "Set to true when this response is a cached replay returned for an idempotency_key that was already processed. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, and any downstream system that assumes exactly-once event semantics. Only present on responses to mutating requests that carry idempotency_key.",
106+
"default": false
107+
},
103108
"context": {
104109
"$ref": "/schemas/core/context.json"
105110
},
@@ -110,5 +115,5 @@
110115
"required": [
111116
"plans"
112117
],
113-
"additionalProperties": false
118+
"additionalProperties": true
114119
}

0 commit comments

Comments
 (0)