From e5af80cb53363a5e6c172638a5d0bb6b3af29fba Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 25 Apr 2026 05:56:21 -0400 Subject: [PATCH 1/2] patch: storyboard for create_media_buy submitted-arm wire shape + force_create_media_buy_arm controller scenario (#3081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the conformance storyboard that anchors the AdCP-payload contract for create_media_buy when it returns the submitted task envelope: status='submitted', task_id present, no media_buy_id, no packages. Without this scenario, a regressed seller emitting media_buy_id under status='submitted' would silently pass — the not.anyOf clauses in create-media-buy-response.json existed but no storyboard exercised them. To make the scenario deterministic across implementations, adds a new test- controller scenario force_create_media_buy_arm. v1 supports two arms — submitted and input-required — that a buyer-supplied directive can shape without fabricating server state. completed is covered by seed_media_buy + a normal flow; working is an out-of-band progress signal, not an initial response arm. The directive is keyed to the caller's authenticated sandbox account, single-shot, and explicitly loses to buyer-side idempotency_key replay (cached response wins). The response schema gains a fifth oneOf branch ForcedDirectiveSuccess carrying forced.arm and forced.task_id — distinct from force_*_status because no entity transitions, so previous_state/current_state would be misleading. The list_scenarios enum gains the new scenario name so sellers advertising it do not schema-fail their own list response. The storyboard captures forced.task_id from the controller response and reuses it via \$context on the create_media_buy assertion, so a seller fabricating a fresh task_id instead of honoring the directive fails the round-trip. Transport-level wire-shape probes (A2A Task.state, artifact.metadata.adcp_task_id; MCP envelope details) are runner-side concerns tracked at adcp-client#904. The submitted -> completed transition is deferred to a follow-up — needs a force_task_completion controller scenario that does not exist yet. Why patch: conformance-suite content addition (new storyboard + new controller scenario, both opt-in via UNKNOWN_SCENARIO grading). No on-wire seller obligations change for sellers that already emit the submitted envelope per create-media-buy-response.json. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../storyboard-async-create-media-buy.md | 22 ++ .../implementation/comply-test-controller.mdx | 47 +++- .../source/protocols/media-buy/index.yaml | 1 + .../scenarios/create_media_buy_async.yaml | 232 ++++++++++++++++++ .../comply-test-controller-request.json | 47 +++- .../comply-test-controller-response.json | 53 +++- 6 files changed, 396 insertions(+), 6 deletions(-) create mode 100644 .changeset/storyboard-async-create-media-buy.md create mode 100644 static/compliance/source/protocols/media-buy/scenarios/create_media_buy_async.yaml diff --git a/.changeset/storyboard-async-create-media-buy.md b/.changeset/storyboard-async-create-media-buy.md new file mode 100644 index 0000000000..bd5f3de467 --- /dev/null +++ b/.changeset/storyboard-async-create-media-buy.md @@ -0,0 +1,22 @@ +--- +--- + +patch: storyboard for create_media_buy submitted-arm wire shape + new force_create_media_buy_arm test-controller scenario + +Adds the conformance scenario adcontextprotocol/adcp#3081 — the AdCP-payload-level invariant for `create_media_buy` when it returns the submitted task envelope. Anchors the spec contract that adcp-client#899 (A2A serve adapter) implements at the transport layer: + +- `status` MUST be the literal string `'submitted'` (not a MediaBuyStatus value, not omitted) +- `task_id` MUST be present at the top of the envelope (snake_case payload field; A2A adapters surface as `taskId` on the wire but the agent emits `task_id`) +- `media_buy_id` and `packages` MUST NOT appear on the envelope — they land on the task's completion artifact + +Without this storyboard, a regressed seller emitting `media_buy_id` under `status: submitted` (or returning a MediaBuyStatus value where `'submitted'` is required) would pass every conformance run. The submitted-arm `not.required` clauses in `create-media-buy-response.json` were silent until something exercised them. + +**New storyboard.** `static/compliance/source/protocols/media-buy/scenarios/create_media_buy_async.yaml`. Registered in `protocols/media-buy/index.yaml` `requires_scenarios:`. Uses `controller_seeding: true` to seed a guaranteed video product, then drives the submitted arm via the new controller scenario (below) before validating the envelope shape on `create_media_buy`. + +**New test-controller scenario.** `force_create_media_buy_arm` in `comply-test-controller.mdx` and the request/response schemas under `static/schemas/source/compliance/`. Shapes the next `create_media_buy` call from the caller's authenticated sandbox account into a specific arm. v1 supports `submitted` (the async task envelope) and `input-required` (errors-branch); `completed` is covered by `seed_media_buy` + a normal flow, and `working` is an out-of-band progress signal rather than an initial response arm. Single-shot — consumed by the next call from this account, then the seller resumes default behavior; buyer-side `idempotency_key` semantics are unchanged (replayed requests return the cached response, not a re-evaluated directive). Required for the storyboard to be deterministic across implementations: most sellers route most buys synchronously, and no buyer-side request shape reliably triggers the submitted arm. Sellers without this scenario return `UNKNOWN_SCENARIO` and the storyboard grades `not_applicable`. + +**Response schema.** `comply-test-controller-response.json` gains a fifth `oneOf` branch `ForcedDirectiveSuccess` with a `forced` envelope (`arm`, `task_id`). The directive is semantically distinct from `force_*_status` — there is no entity to transition, so `previous_state`/`current_state` would be misleading. The `list_scenarios` enum gains the new scenario name so sellers advertising it do not schema-fail their own list response. + +**Out of scope.** Transport-level wire-shape probes (A2A `Task.state`, `artifact.metadata.adcp_task_id`; MCP envelope details) are runner concerns tracked at adcp-client#904. The submitted → completed transition (forcing task resolution and asserting the completion artifact carries `media_buy_id`) is deferred to a follow-up — it needs a `force_task_completion` controller scenario that does not exist yet. + +Why patch: this is a conformance-suite content addition (new storyboard + new controller scenario, both opt-in via `UNKNOWN_SCENARIO`). No on-wire seller obligations change for sellers that already emit the submitted envelope per `create-media-buy-response.json`. Per the versioning rule clarified in `storyboards-patch-clarify.md`, conformance-suite changes version independently and are patch-level by default. diff --git a/docs/building/implementation/comply-test-controller.mdx b/docs/building/implementation/comply-test-controller.mdx index 9506a4e72f..dfcb16fe8d 100644 --- a/docs/building/implementation/comply-test-controller.mdx +++ b/docs/building/implementation/comply-test-controller.mdx @@ -56,6 +56,7 @@ Sellers that implement compliance test controller MUST: "force_creative_status", "force_account_status", "force_media_buy_status", + "force_create_media_buy_arm", "force_session_status", "simulate_delivery", "simulate_budget_spend", @@ -69,7 +70,7 @@ Sellers that implement compliance test controller MUST: }, "params": { "type": "object", - "description": "Scenario-specific parameters. Omit for list_scenarios. force_creative_status: {creative_id, status, rejection_reason?}. force_account_status: {account_id, status}. force_media_buy_status: {media_buy_id, status, rejection_reason?}. force_session_status: {session_id, status, termination_reason?}. simulate_delivery: {media_buy_id, impressions?, clicks?, reported_spend?, conversions?}. simulate_budget_spend: {account_id|media_buy_id, spend_percentage}. seed_product: {product_id, fixture?}. seed_pricing_option: {product_id, pricing_option_id, fixture?}. seed_creative: {creative_id, fixture?}. seed_plan: {plan_id, fixture?}. seed_media_buy: {media_buy_id, fixture?}." + "description": "Scenario-specific parameters. Omit for list_scenarios. force_creative_status: {creative_id, status, rejection_reason?}. force_account_status: {account_id, status}. force_media_buy_status: {media_buy_id, status, rejection_reason?}. force_create_media_buy_arm: {arm, task_id?, message?} — task_id required when arm = submitted. force_session_status: {session_id, status, termination_reason?}. simulate_delivery: {media_buy_id, impressions?, clicks?, reported_spend?, conversions?}. simulate_budget_spend: {account_id|media_buy_id, spend_percentage}. seed_product: {product_id, fixture?}. seed_pricing_option: {product_id, pricing_option_id, fixture?}. seed_creative: {creative_id, fixture?}. seed_plan: {plan_id, fixture?}. seed_media_buy: {media_buy_id, fixture?}." } }, "required": ["scenario"] @@ -156,6 +157,50 @@ Transitions a media buy to the specified status. The seller MUST enforce the med } ``` +### `force_create_media_buy_arm` + +Shapes the next [`create_media_buy`](/media-buy/task-reference/create_media_buy) call from the caller's authenticated sandbox account into a specific response arm. v1 supports two arms: `submitted` (the async task envelope, no `media_buy_id` yet) and `input-required` (the errors-branch). Unlike `force_media_buy_status`, no entity transitions — there is no media buy yet — so the response carries `forced.arm` rather than `previous_state`/`current_state`. + +The submitted-arm wire shape is otherwise implementation-dependent: most sellers route most buys synchronously and no buyer-side request shape reliably triggers async. This scenario lets storyboards pin the arm so a regressed seller (e.g., emitting `media_buy_id` under `status: submitted`) cannot pass conformance silently. + +**Params:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `arm` | `submitted` \| `input-required` | Yes | Target response arm for the next `create_media_buy` call | +| `task_id` | string | When `arm` = `submitted` | Deterministic task handle (max 128 chars) the seller MUST emit verbatim on the submitted envelope and MUST accept on subsequent `tasks/get` polls. Sandbox task_ids are caller-opaque strings; production task-id format rules do not apply. | +| `message` | string | No | Human-readable explanation surfaced verbatim on the seller's `create_media_buy` response. Plain text, max 2000 characters. Buyers consuming the resulting response MUST apply the prompt-injection sanitization documented for [`message` on the submitted envelope](/schemas/v3/media-buy/create-media-buy-response.json) — this scenario is the natural place for a runner to inject adversarial strings to test that buyer-side sanitization. | + +**Example:** + +```json +{ + "scenario": "force_create_media_buy_arm", + "params": { + "arm": "submitted", + "task_id": "task_async_signed_io_q2", + "message": "Awaiting IO signature from sales team; typical turnaround 2–4 hours" + } +} +``` + +**Response.** A `ForcedDirectiveSuccess` shape carrying the registered directive: + +```json +{ + "success": true, + "forced": { + "arm": "submitted", + "task_id": "task_async_signed_io_q2" + }, + "message": "Next create_media_buy call will return the submitted arm with task_id task_async_signed_io_q2" +} +``` + +`forced.task_id` is present only when `arm: submitted`. + +**Consumption and idempotency.** The directive is keyed to the caller's authenticated sandbox account (account + principal pair) and is consumed by the next `create_media_buy` call from that account. Subsequent calls without a fresh directive return the seller's default arm. Buyer-side `idempotency_key` semantics are unchanged: if the caller replays a `create_media_buy` request that already consumed a directive, the seller MUST replay the cached response (the request idempotency cache wins) and MUST NOT re-evaluate against the now-empty directive slot. Sellers MUST NOT match a directive against a `create_media_buy` call from a different account or principal, even within the same transport connection. A second `force_create_media_buy_arm` call before the directive is consumed overwrites the prior one. + ### `force_session_status` Transitions an SI session to a terminal status. Enables testing timeout and termination scenarios that would otherwise require waiting for real timeouts. The `termination_reason` param simulates the cause so the storyboard runner can verify sellers report the correct reason in subsequent responses. diff --git a/static/compliance/source/protocols/media-buy/index.yaml b/static/compliance/source/protocols/media-buy/index.yaml index 9eb5a1b5bb..38c421a694 100644 --- a/static/compliance/source/protocols/media-buy/index.yaml +++ b/static/compliance/source/protocols/media-buy/index.yaml @@ -16,6 +16,7 @@ requires_scenarios: - media_buy_seller/inventory_list_no_match - media_buy_seller/invalid_transitions - media_buy_seller/creative_fate_after_cancellation + - media_buy_seller/create_media_buy_async narrative: | You run a sell-side platform — a publisher, SSP, retail media network, or any system that 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 new file mode 100644 index 0000000000..c2ba4c3ec0 --- /dev/null +++ b/static/compliance/source/protocols/media-buy/scenarios/create_media_buy_async.yaml @@ -0,0 +1,232 @@ +id: media_buy_seller/create_media_buy_async +version: "1.0.0" +title: "Seller returns submitted task envelope when create_media_buy goes async" +category: media_buy_seller +summary: "Verifies the AdCP-payload wire shape of the submitted-arm response from create_media_buy: status='submitted', task_id present, no media_buy_id and no packages on the envelope." +track: media_buy +required_tools: + - create_media_buy + - comply_test_controller + +narrative: | + When create_media_buy cannot confirm the buy synchronously — e.g., the seller is + routing the request through IO signing, batch processing, or any out-of-band human + workflow — the task layer carries the result, not the response. The seller emits the + submitted task envelope: status='submitted', task_id present, no media_buy_id, no + packages. The buyer then polls tasks/get with task_id (or waits for a webhook) until + the task completes and the media_buy_id arrives on the completion artifact. + + This scenario anchors the AdCP-payload-level invariant for that envelope. Three things + matter and are easy to regress: + + 1. status MUST be the literal string 'submitted' (not 'pending', not a MediaBuyStatus + value, not omitted) + 2. task_id MUST be present at the top of the payload, snake_case (A2A adapters MAY + surface it as taskId on the wire, but the payload field emitted by the agent is + task_id) + 3. media_buy_id and packages MUST NOT appear on the envelope — they land on the task's + completion artifact, not here. Sellers that return media_buy_id with status='submitted' + break the buyer's polling contract; buyers cannot tell whether the buy is queued or + confirmed. + + Determinism. The submitted arm is implementation-dependent — most sellers route most + buys synchronously. To make this storyboard runnable across implementations, the test + harness uses comply_test_controller force_create_media_buy_arm to drive the next + create_media_buy call into the submitted arm. The directive is keyed to the caller's + authenticated sandbox account (account + principal pair); sellers that do not implement + the controller scenario return UNKNOWN_SCENARIO and the runner grades this storyboard + not_applicable rather than failed. + + Round-trip integrity. The deterministic task_id is captured from the controller + response and reused as the expected task_id on the create_media_buy assertion, so the + storyboard catches sellers that fabricate a fresh task_id instead of honoring the + registered directive. + + Out of scope (by design). Transport-level wire-shape assertions — A2A Task.state and + artifact.metadata.adcp_task_id placement, MCP structuredContent envelope details — are + runner-side concerns, not storyboard assertions. The runner exercises this scenario + against both transports and probes the transport envelope independently. See + adcp-client#904 for the runner-side probes; this storyboard provides the deterministic + driver. + + The submitted → completed transition (forcing the task to resolve and asserting the + completion artifact carries media_buy_id) is deferred to a follow-up scenario. It needs + a force_task_completion controller scenario that does not exist yet. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - supports_test_controller + examples: + - "Sellers that route some create_media_buy calls through IO signing or batch processing" + - "Any seller exposing comply_test_controller in sandbox mode" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller exposes comply_test_controller in sandbox mode and supports + force_create_media_buy_arm. The directive is keyed to the caller's + authenticated sandbox account; without controller support, the storyboard + grades not_applicable — sellers cannot be deterministically driven into + the submitted arm by buyer-initiated requests alone. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "async_signed_io_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_30s" + pricing_options: + - product_id: "async_signed_io_q2" + pricing_option_id: "cpm_guaranteed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 18.0 + +phases: + - id: force_submitted_arm + title: "Drive next create_media_buy into submitted arm" + narrative: | + Tell the controller that the next create_media_buy call from the caller's + authenticated sandbox account should return the submitted task envelope. + The controller stores the directive against the (account, principal) pair + and consumes it on the next create_media_buy call. Sellers that do not + implement force_create_media_buy_arm return UNKNOWN_SCENARIO and the + runner grades this storyboard not_applicable. + + steps: + - id: force_arm_submitted + title: "Force submitted arm on next create_media_buy" + requires_tool: comply_test_controller + narrative: | + Direct the controller to return the submitted envelope with a + deterministic task_id on the buyer's next create_media_buy call. The + message field is set to a representative IO-signing explanation so + buyers exercising prompt-injection sanitization on submitted.message + have a stable string to assert against. + task: comply_test_controller + comply_scenario: create_media_buy + stateful: true + context_outputs: + - name: forced_task_id + path: "forced.task_id" + expected: | + Return a successful directive: + - success: true + - forced.arm: submitted + - forced.task_id: deterministic task handle the next create_media_buy will return + + sample_request: + scenario: "force_create_media_buy_arm" + params: + arm: "submitted" + task_id: "task_async_signed_io_q2" + message: "Awaiting IO signature from sales team; typical turnaround 2–4 hours" + context: + correlation_id: "create_media_buy_async--force_arm_submitted" + validations: + - check: field_value + path: "success" + allowed_values: [true] + description: "Controller accepts the directive" + - check: field_value + path: "forced.arm" + value: "submitted" + description: "Controller echoes the forced arm" + - check: field_present + path: "forced.task_id" + description: "Controller echoes the deterministic task_id" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "create_media_buy_async--force_arm_submitted" + description: "Context correlation_id returned unchanged" + + - id: submitted_arm_response + title: "create_media_buy returns submitted task envelope" + narrative: | + The buyer makes a normal create_media_buy call. Because the controller + registered a directive against this sandbox account, the seller MUST emit + the submitted task envelope: status='submitted', task_id matching the + forced value, no media_buy_id, no packages. + + The response_schema check carries the absence invariant — the submitted + arm in create-media-buy-response.json has not.anyOf clauses for both + media_buy_id and packages, so a seller that emits either under + status='submitted' fails schema validation. The explicit field_value + check on status pins the literal 'submitted' value, since a malformed + seller might omit the discriminator and still satisfy the parent oneOf + via the error or success branch. The task_id check uses the captured + $context.forced_task_id so the storyboard fails if the seller ignores + the registered directive and fabricates its own task_id. + + steps: + - id: create_media_buy_submitted + title: "Call create_media_buy and observe the submitted envelope" + task: create_media_buy + schema_ref: "media-buy/create-media-buy-request.json" + response_schema_ref: "media-buy/create-media-buy-response.json" + doc_ref: "/media-buy/task-reference/create_media_buy" + comply_scenario: create_media_buy + stateful: true + expected: | + Return the submitted task envelope: + - status: 'submitted' (literal, not a MediaBuyStatus value) + - task_id: matches the value registered by force_create_media_buy_arm + - no media_buy_id (issued on task completion, not here) + - no packages (issued on task completion, not here) + - message (optional): seller's explanation of the wait + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + start_time: "2026-07-01T00:00:00Z" + end_time: "2026-09-30T23:59:59Z" + packages: + - product_id: "async_signed_io_q2" + budget: 30000 + pricing_option_id: "cpm_guaranteed" + push_notification_config: + url: "https://buyer.example/webhooks/adcp" + authentication: + schemes: + - "HMAC-SHA256" + credentials: "media-buy-seller-webhook-secret-token" + idempotency_key: "$generate:uuid_v4#media_buy_seller_create_media_buy_async_submitted_arm_response_create_media_buy_submitted" + context: + correlation_id: "create_media_buy_async--create_media_buy_submitted" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json — submitted-arm not.required clauses block media_buy_id and packages" + - check: field_value + path: "status" + value: "submitted" + description: "Status is the literal 'submitted' task-status value, not a MediaBuyStatus" + - check: field_present + path: "task_id" + description: "task_id is present at the top of the envelope (snake_case payload field, even when the A2A adapter surfaces it as taskId on the wire)" + - check: field_value + path: "task_id" + value: "$context.forced_task_id" + description: "task_id matches the captured value from the controller directive — sellers that fabricate a fresh task_id instead of honoring the registered one fail here" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "create_media_buy_async--create_media_buy_submitted" + description: "Context correlation_id returned unchanged" diff --git a/static/schemas/source/compliance/comply-test-controller-request.json b/static/schemas/source/compliance/comply-test-controller-request.json index c7b37c28e1..72ce31419f 100644 --- a/static/schemas/source/compliance/comply-test-controller-request.json +++ b/static/schemas/source/compliance/comply-test-controller-request.json @@ -13,6 +13,7 @@ "force_creative_status", "force_account_status", "force_media_buy_status", + "force_create_media_buy_arm", "force_session_status", "simulate_delivery", "simulate_budget_spend", @@ -58,7 +59,23 @@ }, "required": ["amount", "currency"] }, - "spend_percentage": { "type": "number", "minimum": 0, "maximum": 100, "description": "Spend to this percentage of budget (0–100). Used by simulate_budget_spend." } + "spend_percentage": { "type": "number", "minimum": 0, "maximum": 100, "description": "Spend to this percentage of budget (0–100). Used by simulate_budget_spend." }, + "arm": { + "type": "string", + "enum": ["submitted", "input-required"], + "description": "Response arm for the next create_media_buy call. Used by force_create_media_buy_arm. v1 supports the two arms a buyer-supplied directive can shape without fabricating server state: 'submitted' (async task envelope) and 'input-required' (errors-branch). 'completed' is covered by seed_media_buy + a normal flow; 'working' is an out-of-band progress signal, not an initial response arm." + }, + "task_id": { + "type": "string", + "maxLength": 128, + "description": "Deterministic task handle the seller MUST emit verbatim on the next create_media_buy response when arm is 'submitted'. The seller MUST accept this exact value on subsequent tasks/get calls within the same authenticated sandbox account. Sandbox task_ids are caller-opaque strings — the seller's production task-id format rules do not apply.", + "x-entity": "task" + }, + "message": { + "type": "string", + "maxLength": 2000, + "description": "Optional human-readable explanation surfaced on the next create_media_buy response. Used by force_create_media_buy_arm for the 'submitted' and 'working' arms. Plain text only." + } }, "additionalProperties": true }, @@ -79,6 +96,23 @@ "if": { "properties": { "scenario": { "const": "force_media_buy_status" } } }, "then": { "required": ["params"], "properties": { "params": { "required": ["media_buy_id", "status"], "properties": { "status": { "$ref": "/schemas/enums/media-buy-status.json" } } } } } }, + { + "if": { "properties": { "scenario": { "const": "force_create_media_buy_arm" } } }, + "then": { + "required": ["params"], + "properties": { + "params": { + "required": ["arm"], + "allOf": [ + { + "if": { "properties": { "arm": { "const": "submitted" } } }, + "then": { "required": ["task_id"] } + } + ] + } + } + } + }, { "if": { "properties": { "scenario": { "const": "force_session_status" } } }, "then": { "required": ["params"], "properties": { "params": { "required": ["session_id", "status"], "properties": { "status": { "type": "string", "enum": ["complete", "terminated"] } } } } } @@ -152,6 +186,17 @@ } } }, + { + "description": "Force the next create_media_buy call into the submitted arm with a deterministic task_id", + "data": { + "scenario": "force_create_media_buy_arm", + "params": { + "arm": "submitted", + "task_id": "task_async_signed_io_q2", + "message": "Awaiting IO signature from sales team; typical turnaround 2–4 hours" + } + } + }, { "description": "Simulate session timeout", "data": { diff --git a/static/schemas/source/compliance/comply-test-controller-response.json b/static/schemas/source/compliance/comply-test-controller-response.json index 6f36463249..24db95e56e 100644 --- a/static/schemas/source/compliance/comply-test-controller-response.json +++ b/static/schemas/source/compliance/comply-test-controller-response.json @@ -19,6 +19,7 @@ "force_creative_status", "force_account_status", "force_media_buy_status", + "force_create_media_buy_arm", "force_session_status", "simulate_delivery", "simulate_budget_spend", @@ -36,7 +37,7 @@ }, "required": ["success", "scenarios"], "additionalProperties": true, - "not": { "anyOf": [{ "required": ["error"] }, { "required": ["previous_state"] }, { "required": ["simulated"] }] } + "not": { "anyOf": [{ "required": ["error"] }, { "required": ["previous_state"] }, { "required": ["simulated"] }, { "required": ["forced"] }] } }, { "title": "StateTransitionSuccess", @@ -52,7 +53,7 @@ }, "required": ["success", "previous_state", "current_state"], "additionalProperties": true, - "not": { "anyOf": [{ "required": ["error"] }, { "required": ["scenarios"] }, { "required": ["simulated"] }] } + "not": { "anyOf": [{ "required": ["error"] }, { "required": ["scenarios"] }, { "required": ["simulated"] }, { "required": ["forced"] }] } }, { "title": "SimulationSuccess", @@ -76,7 +77,40 @@ }, "required": ["success", "simulated"], "additionalProperties": true, - "not": { "anyOf": [{ "required": ["error"] }, { "required": ["scenarios"] }, { "required": ["previous_state"] }] } + "not": { "anyOf": [{ "required": ["error"] }, { "required": ["scenarios"] }, { "required": ["previous_state"] }, { "required": ["forced"] }] } + }, + { + "title": "ForcedDirectiveSuccess", + "description": "A force_create_media_buy_arm directive was registered. The directive shapes the next create_media_buy call from this caller's authenticated sandbox account into the requested arm, then is consumed. No entity transitioned — there is no media buy yet — so this branch carries 'forced' rather than previous_state/current_state.", + "type": "object", + "properties": { + "success": { "type": "boolean", "const": true }, + "forced": { + "type": "object", + "description": "Echo of the registered directive. The next create_media_buy call from this sandbox account will return the named arm.", + "properties": { + "arm": { + "type": "string", + "enum": ["submitted", "input-required"], + "description": "Arm the seller will emit on the next create_media_buy response." + }, + "task_id": { + "type": "string", + "maxLength": 128, + "description": "Echo of the registered task_id. Present only when arm is 'submitted' (the arm that emits a task envelope).", + "x-entity": "task" + } + }, + "required": ["arm"], + "additionalProperties": false + }, + "message": { "type": "string", "description": "Human-readable acknowledgement." }, + "context": { "$ref": "/schemas/core/context.json" }, + "ext": { "$ref": "/schemas/core/ext.json" } + }, + "required": ["success", "forced"], + "additionalProperties": true, + "not": { "anyOf": [{ "required": ["error"] }, { "required": ["scenarios"] }, { "required": ["previous_state"] }, { "required": ["simulated"] }] } }, { "title": "ControllerError", @@ -96,7 +130,7 @@ }, "required": ["success", "error"], "additionalProperties": true, - "not": { "anyOf": [{ "required": ["scenarios"] }, { "required": ["simulated"] }, { "required": ["previous_state"] }] } + "not": { "anyOf": [{ "required": ["scenarios"] }, { "required": ["simulated"] }, { "required": ["previous_state"] }, { "required": ["forced"] }] } } ], "examples": [ @@ -145,6 +179,17 @@ "message": "Budget for mb-789 set to 95% consumed ($950.00 of $1000.00)" } }, + { + "description": "force_create_media_buy_arm directive registered", + "data": { + "success": true, + "forced": { + "arm": "submitted", + "task_id": "task_async_signed_io_q2" + }, + "message": "Next create_media_buy call from this sandbox account will return the submitted arm with task_id task_async_signed_io_q2" + } + }, { "description": "Invalid transition error", "data": { From e8000b532b3d34d1fdfbaa4634fb6d9b8a354cfd Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 25 Apr 2026 07:28:36 -0400 Subject: [PATCH 2/2] fix: dedupe react in npm overrides + correct two broken doc links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes needed to ship #3081 cleanly: **React dedupe.** @workos-inc/widgets pinned react@19.2.4 transitively while @mintlify/cli pinned 19.2.3 — slightly different patches that npm refused to dedupe, leaving two copies of react in node_modules. Mintlify's preview tooling renders with hooks and crashed on "Invalid hook call / Cannot read properties of null (reading 'useState')" the moment any docs file changed and the pre-push broken-links check ran. Adds react and react-dom to package.json overrides at ^19.2.0 so npm collapses every transitive react into one resolution. After the fix both packages resolve to 19.2.5 (the current latest patch). Bug reproduced on main too — not caused by this branch. **Doc link paths.** Two new links added in the previous commit pointed at spec-style paths that mintlify could not resolve: - /media-buy/task-reference/create_media_buy → /docs/media-buy/task-reference/create_media_buy (mintlify routes need the /docs/ prefix that storyboard doc_ref entries do not) - /schemas/v3/media-buy/create-media-buy-response.json → https://adcontextprotocol.org/schemas/v3/media-buy/create-media-buy-response.json (schema URLs are external; matches the pattern used in every task-reference page) Both caught by mintlify broken-links once the React dedupe unblocked the scan from running to completion. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../implementation/comply-test-controller.mdx | 4 +- package-lock.json | 169 +----------------- package.json | 4 +- 3 files changed, 12 insertions(+), 165 deletions(-) diff --git a/docs/building/implementation/comply-test-controller.mdx b/docs/building/implementation/comply-test-controller.mdx index dfcb16fe8d..3801f538c8 100644 --- a/docs/building/implementation/comply-test-controller.mdx +++ b/docs/building/implementation/comply-test-controller.mdx @@ -159,7 +159,7 @@ Transitions a media buy to the specified status. The seller MUST enforce the med ### `force_create_media_buy_arm` -Shapes the next [`create_media_buy`](/media-buy/task-reference/create_media_buy) call from the caller's authenticated sandbox account into a specific response arm. v1 supports two arms: `submitted` (the async task envelope, no `media_buy_id` yet) and `input-required` (the errors-branch). Unlike `force_media_buy_status`, no entity transitions — there is no media buy yet — so the response carries `forced.arm` rather than `previous_state`/`current_state`. +Shapes the next [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) call from the caller's authenticated sandbox account into a specific response arm. v1 supports two arms: `submitted` (the async task envelope, no `media_buy_id` yet) and `input-required` (the errors-branch). Unlike `force_media_buy_status`, no entity transitions — there is no media buy yet — so the response carries `forced.arm` rather than `previous_state`/`current_state`. The submitted-arm wire shape is otherwise implementation-dependent: most sellers route most buys synchronously and no buyer-side request shape reliably triggers async. This scenario lets storyboards pin the arm so a regressed seller (e.g., emitting `media_buy_id` under `status: submitted`) cannot pass conformance silently. @@ -169,7 +169,7 @@ The submitted-arm wire shape is otherwise implementation-dependent: most sellers |-------|------|----------|-------------| | `arm` | `submitted` \| `input-required` | Yes | Target response arm for the next `create_media_buy` call | | `task_id` | string | When `arm` = `submitted` | Deterministic task handle (max 128 chars) the seller MUST emit verbatim on the submitted envelope and MUST accept on subsequent `tasks/get` polls. Sandbox task_ids are caller-opaque strings; production task-id format rules do not apply. | -| `message` | string | No | Human-readable explanation surfaced verbatim on the seller's `create_media_buy` response. Plain text, max 2000 characters. Buyers consuming the resulting response MUST apply the prompt-injection sanitization documented for [`message` on the submitted envelope](/schemas/v3/media-buy/create-media-buy-response.json) — this scenario is the natural place for a runner to inject adversarial strings to test that buyer-side sanitization. | +| `message` | string | No | Human-readable explanation surfaced verbatim on the seller's `create_media_buy` response. Plain text, max 2000 characters. Buyers consuming the resulting response MUST apply the prompt-injection sanitization documented for [`message` on the submitted envelope](https://adcontextprotocol.org/schemas/v3/media-buy/create-media-buy-response.json) — this scenario is the natural place for a runner to inject adversarial strings to test that buyer-side sanitization. | **Example:** diff --git a/package-lock.json b/package-lock.json index 53c7b76715..2ba64c4751 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2646,16 +2646,6 @@ "node": ">=20" } }, - "node_modules/@mintlify/cli/node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@mintlify/cli/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -2905,46 +2895,6 @@ "react-dom": ">= 18.3.0 < 19.0.0" } }, - "node_modules/@mintlify/common/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@mintlify/common/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/@mintlify/common/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/@mintlify/link-rot": { "version": "3.0.1037", "resolved": "https://registry.npmjs.org/@mintlify/link-rot/-/link-rot-3.0.1037.tgz", @@ -4297,16 +4247,6 @@ "node": ">= 0.8" } }, - "node_modules/@mintlify/previewing/node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@mintlify/previewing/node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -4908,35 +4848,6 @@ "node": ">=18" } }, - "node_modules/@mintlify/scraping/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@mintlify/scraping/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, "node_modules/@mintlify/scraping/node_modules/remark-mdx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.0.1.tgz", @@ -4952,17 +4863,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@mintlify/scraping/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/@mintlify/scraping/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5226,46 +5126,6 @@ "react-dom": ">= 18.3.0 < 19.0.0" } }, - "node_modules/@mintlify/validation/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@mintlify/validation/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/@mintlify/validation/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/@mintlify/validation/node_modules/zod-to-json-schema": { "version": "3.20.4", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.20.4.tgz", @@ -16517,20 +16377,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lop": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", @@ -20180,26 +20026,25 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.5" } }, "node_modules/react-reconciler": { diff --git a/package.json b/package.json index 115b8e8d85..6c3f2c5fbd 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,8 @@ "js-yaml": "^4.1.1", "axios": "^1.13.6", "@types/express-serve-static-core": "5.1.0", - "zod": "$zod" + "zod": "$zod", + "react": "^19.2.0", + "react-dom": "^19.2.0" } }