From f2340f81490fb136e2be13f249db8a2873e1779f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Mon, 11 May 2026 08:13:59 -0400 Subject: [PATCH 1/2] spec(comply_test_controller): require account.sandbox: true (closes #4383) - Sweep all 25 comply_test_controller sample_request blocks across 6 storyboard YAMLs to include account: { sandbox: true }. - Tighten static/schemas/source/compliance/comply-test-controller-request.json to require account at the top level. Description promoted from SHOULD to MUST; the (Sandbox) verdict tier (#4379) now has schema backing. - Add account assertion to schema examples so docs and validators stay in sync. The existing storyboard sample_request schema lint (scripts/lint-storyboard-sample-request-schema.cjs, surfaced via tests/lint-storyboard-sample-request-schema.test.cjs) catches future omissions: any new comply_test_controller step lacking account fingerprints as required@/:account, blocking CI without an allowlist entry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scenarios/create_media_buy_async.yaml | 2 + .../scenarios/delivery_reporting.yaml | 2 + .../scenarios/measurement_accountability.yaml | 2 + .../vendor_metric_accountability.yaml | 2 + .../universal/deterministic-testing.yaml | 38 ++++++++++++++++++ ...pagination-integrity-creative-formats.yaml | 2 + .../comply-test-controller-request.json | 40 +++++++++++++++++-- 7 files changed, 85 insertions(+), 3 deletions(-) diff --git a/static/compliance/source/protocols/media-buy/scenarios/create_media_buy_async.yaml b/static/compliance/source/protocols/media-buy/scenarios/create_media_buy_async.yaml index c2ba4c3ec0..27983ee9ce 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/create_media_buy_async.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/create_media_buy_async.yaml @@ -124,6 +124,8 @@ phases: - forced.task_id: deterministic task handle the next create_media_buy will return sample_request: + account: + sandbox: true scenario: "force_create_media_buy_arm" params: arm: "submitted" diff --git a/static/compliance/source/protocols/media-buy/scenarios/delivery_reporting.yaml b/static/compliance/source/protocols/media-buy/scenarios/delivery_reporting.yaml index 75596a2306..c21e713a6e 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/delivery_reporting.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/delivery_reporting.yaml @@ -162,6 +162,8 @@ phases: expected: | The test controller acknowledges the simulated delivery data. sample_request: + account: + sandbox: true scenario: "simulate_delivery" params: media_buy_id: "$context.media_buy_id" diff --git a/static/compliance/source/protocols/media-buy/scenarios/measurement_accountability.yaml b/static/compliance/source/protocols/media-buy/scenarios/measurement_accountability.yaml index 4b449bcd07..6c76b0c029 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/measurement_accountability.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/measurement_accountability.yaml @@ -195,6 +195,8 @@ phases: expected: | The test controller acknowledges the simulated delivery data. sample_request: + account: + sandbox: true scenario: "simulate_delivery" params: media_buy_id: "$context.media_buy_id" diff --git a/static/compliance/source/protocols/media-buy/scenarios/vendor_metric_accountability.yaml b/static/compliance/source/protocols/media-buy/scenarios/vendor_metric_accountability.yaml index 664832e753..953f630b46 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/vendor_metric_accountability.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/vendor_metric_accountability.yaml @@ -226,6 +226,8 @@ phases: The test controller acknowledges the simulated delivery data including the vendor_metric_values. sample_request: + account: + sandbox: true scenario: "simulate_delivery" params: media_buy_id: "$context.media_buy_id" diff --git a/static/compliance/source/universal/deterministic-testing.yaml b/static/compliance/source/universal/deterministic-testing.yaml index 8e6a6c00e3..498b3b488f 100644 --- a/static/compliance/source/universal/deterministic-testing.yaml +++ b/static/compliance/source/universal/deterministic-testing.yaml @@ -112,6 +112,8 @@ phases: - scenarios: array or object of scenario names sample_request: + account: + sandbox: true scenario: 'list_scenarios' context: @@ -150,6 +152,8 @@ phases: - error: UNKNOWN_SCENARIO sample_request: + account: + sandbox: true scenario: 'nonexistent_scenario' params: {} @@ -196,6 +200,8 @@ phases: UNKNOWN_SCENARIO (when scenario not on this tenant) sample_request: + account: + sandbox: true scenario: 'force_creative_status' params: {} @@ -242,6 +248,8 @@ phases: UNKNOWN_SCENARIO (when scenario not on this tenant) sample_request: + account: + sandbox: true scenario: 'force_creative_status' params: creative_id: 'comply-test-nonexistent-000000000000' @@ -358,6 +366,8 @@ phases: - current_state: suspended sample_request: + account: + sandbox: true scenario: 'force_account_status' params: account_id: '$context.account_id' @@ -400,6 +410,8 @@ phases: - current_state: active sample_request: + account: + sandbox: true scenario: 'force_account_status' params: account_id: '$context.account_id' @@ -439,6 +451,8 @@ phases: Return a successful state transition to payment_required. sample_request: + account: + sandbox: true scenario: 'force_account_status' params: account_id: '$context.account_id' @@ -472,6 +486,8 @@ phases: Account restored to active state. sample_request: + account: + sandbox: true scenario: 'force_account_status' params: account_id: '$context.account_id' @@ -559,6 +575,8 @@ phases: Return a successful state transition to active. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'force_media_buy_status' @@ -639,6 +657,8 @@ phases: Return a successful transition to completed. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'force_media_buy_status' @@ -684,6 +704,8 @@ phases: - error: INVALID_TRANSITION sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'force_media_buy_status' @@ -783,6 +805,8 @@ phases: Successful transition to approved. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'force_creative_status' @@ -823,6 +847,8 @@ phases: Successful transition to archived. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'force_creative_status' @@ -861,6 +887,8 @@ phases: Return error with INVALID_TRANSITION. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'force_creative_status' @@ -952,6 +980,8 @@ phases: Successful transition to rejected with reason preserved. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'force_creative_status' @@ -1037,6 +1067,8 @@ phases: Successful transition to terminated. sample_request: + account: + sandbox: true scenario: 'force_session_status' params: session_id: '$context.session_id' @@ -1162,6 +1194,8 @@ phases: Return success with simulated delivery metrics acknowledged. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'simulate_delivery' @@ -1295,6 +1329,8 @@ phases: Return success with budget spend simulated to 95%. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'simulate_budget_spend' @@ -1331,6 +1367,8 @@ phases: Return success with budget fully depleted. sample_request: + account: + sandbox: true brand: domain: 'acmeoutdoor.example' scenario: 'simulate_budget_spend' diff --git a/static/compliance/source/universal/pagination-integrity-creative-formats.yaml b/static/compliance/source/universal/pagination-integrity-creative-formats.yaml index 4e9c40af96..e31888977f 100644 --- a/static/compliance/source/universal/pagination-integrity-creative-formats.yaml +++ b/static/compliance/source/universal/pagination-integrity-creative-formats.yaml @@ -109,6 +109,7 @@ phases: Return success: true. sample_request: account: + sandbox: true account_id: "acct_pagination_integrity_formats" scenario: "seed_creative_format" params: @@ -135,6 +136,7 @@ phases: Return success: true. sample_request: account: + sandbox: true account_id: "acct_pagination_integrity_formats" scenario: "seed_creative_format" params: diff --git a/static/schemas/source/compliance/comply-test-controller-request.json b/static/schemas/source/compliance/comply-test-controller-request.json index c4a2ec6a03..b59702e1e0 100644 --- a/static/schemas/source/compliance/comply-test-controller-request.json +++ b/static/schemas/source/compliance/comply-test-controller-request.json @@ -564,7 +564,7 @@ }, "account": { "type": "object", - "description": "Sandbox account assertion. The runner SHOULD set sandbox: true on every comply_test_controller request; required in a future version. The seller MUST refuse the request (returning a structured error) if the targeted account is not a sandbox account in the seller's persisted records. This field is a caller-side declaration of intent — it does not grant sandbox status; sellers verify against their own account state. The (Sandbox) verification tier is defined by this gate: real production endpoints accept sandbox-flagged traffic and process it without real-world side effects, no separate test-mode endpoint required. See spec issue #3755 and the (Sandbox) framing in #4379. NOTE: optional in this version while existing storyboards add the field; the const: true constraint applies when present, so a request asserting sandbox: false schema-rejects regardless. A subsequent version bump will move this field into the required array; follow-up issue tracks the storyboard sweep that unblocks that.", + "description": "Sandbox account assertion. The runner MUST set sandbox: true on every comply_test_controller request. The seller MUST refuse the request (returning a structured error) if the targeted account is not a sandbox account in the seller's persisted records. This field is a caller-side declaration of intent — it does not grant sandbox status; sellers verify against their own account state. The (Sandbox) verification tier is defined by this gate: real production endpoints accept sandbox-flagged traffic and process it without real-world side effects, no separate test-mode endpoint required. See spec issue #3755 and the (Sandbox) framing in #4379.", "required": [ "sandbox" ], @@ -579,14 +579,18 @@ } }, "required": [ - "scenario" + "scenario", + "account" ], "additionalProperties": true, "examples": [ { "description": "List supported scenarios", "data": { - "scenario": "list_scenarios" + "scenario": "list_scenarios", + "account": { + "sandbox": true + } } }, { @@ -597,6 +601,9 @@ "creative_id": "cr-123", "status": "rejected", "rejection_reason": "Brand safety policy violation" + }, + "account": { + "sandbox": true } } }, @@ -607,6 +614,9 @@ "params": { "account_id": "acct-456", "status": "suspended" + }, + "account": { + "sandbox": true } } }, @@ -618,6 +628,9 @@ "arm": "submitted", "task_id": "task_async_signed_io_q2", "message": "Awaiting IO signature from sales team; typical turnaround 2–4 hours" + }, + "account": { + "sandbox": true } } }, @@ -638,6 +651,9 @@ } ] } + }, + "account": { + "sandbox": true } } }, @@ -649,6 +665,9 @@ "session_id": "sess-abc", "status": "terminated", "termination_reason": "session_timeout" + }, + "account": { + "sandbox": true } } }, @@ -664,6 +683,9 @@ "amount": 150, "currency": "USD" } + }, + "account": { + "sandbox": true } } }, @@ -674,6 +696,9 @@ "params": { "media_buy_id": "mb-789", "spend_percentage": 95 + }, + "account": { + "sandbox": true } } }, @@ -697,6 +722,9 @@ } ] } + }, + "account": { + "sandbox": true } } }, @@ -712,6 +740,9 @@ "id": "video_30s" } } + }, + "account": { + "sandbox": true } } }, @@ -723,6 +754,9 @@ "since_timestamp": "2026-05-02T14:30:00Z", "endpoint_pattern": "POST *", "limit": 100 + }, + "account": { + "sandbox": true } } } From 9add9ac415bfbeebef0ed8b81c97db9f9965c0fa Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Mon, 11 May 2026 09:22:50 -0400 Subject: [PATCH 2/2] chore: add changeset for #4383 schema tightening Co-Authored-By: Claude Opus 4.7 (1M context) --- .../4383-comply-test-controller-require-account.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/4383-comply-test-controller-require-account.md diff --git a/.changeset/4383-comply-test-controller-require-account.md b/.changeset/4383-comply-test-controller-require-account.md new file mode 100644 index 0000000000..601bcb8204 --- /dev/null +++ b/.changeset/4383-comply-test-controller-require-account.md @@ -0,0 +1,9 @@ +--- +"adcontextprotocol": minor +--- + +`comply_test_controller`: `account.sandbox: true` is now **required** on every controller request. The follow-up to #4382 / #3755 — sample_request blocks across all 25 controller call-sites in the storyboard suite have been swept to include the field, and the request schema's `required` array now lists `account` alongside `scenario`. Schema examples updated to match. + +Lint coverage is automatic: the existing `lint-storyboard-sample-request-schema.cjs` runs ajv against every storyboard sample_request, so any new `comply_test_controller` step that omits `account.sandbox: true` fails CI with `required@/:account` and is blocked without an allowlist entry. No new lint code needed — the schema tightening is the gate. + +This operationalizes the (Sandbox) verdict's defense-in-depth: the seller-side persisted-record check is the load-bearing gate, and now the wire format enforces it too. Closes #4383.