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
22 changes: 22 additions & 0 deletions .changeset/storyboard-async-create-media-buy.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 46 additions & 1 deletion docs/building/implementation/comply-test-controller.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"]
Expand Down Expand Up @@ -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`](/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.

**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](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:**

```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.
Expand Down
169 changes: 7 additions & 162 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions static/compliance/source/protocols/media-buy/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading