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
11 changes: 11 additions & 0 deletions .changeset/comply-controller-mode-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"adcontextprotocol": patch
---

Add `comply_controller_mode_gate` universal storyboard and `acme-outdoor-live` test kit.

New storyboard exercises the live-account denial path for `comply_test_controller`:
a seller that exposes the controller must return `FORBIDDEN` when called by a
live-mode (non-sandbox) principal. Optional phase for two-deployment sellers;
required for single-endpoint sellers that implement per-account gating.
Closes #4028.
1 change: 1 addition & 0 deletions docs/building/compliance-catalog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Every agent runs every storyboard in `/compliance/{version}/universal/` regardle
| `collection-lists-pagination-integrity` | `cursor` ↔ `has_more` invariant verified by walking a paginated `list_collection_lists` response — storyboard bootstraps three collection lists via `create_collection_list`, then asserts page 1 is non-terminal and page 2 is terminal with no stale cursor. |
| `property-lists-pagination-integrity` | `cursor` ↔ `has_more` invariant verified by walking a paginated `list_property_lists` response — storyboard bootstraps three property lists via `create_property_list`, then asserts page 1 is non-terminal and page 2 is terminal with no stale cursor. |
| `deterministic-testing` | `comply_test_controller` state-machine verification — skipped if `capabilities.compliance_testing.supported: false`. |
| `comply-controller-mode-gate` | `comply_test_controller` live-mode denial gate — verifies sellers return `FORBIDDEN` when a live-mode account calls the controller; skipped for agents that do not expose `comply_test_controller`. |
| `signed-requests` | RFC 9421 transport-layer request-signing verification — skipped if `request_signing.supported: false`. |

Capability-gated rows (`deterministic-testing`, `signed-requests`) are skipped only when the agent advertises the capability as `false`; they cannot be claimed and partially implemented. Declaring `supported: true` and failing the storyboard is non-conformant — declare `false` rather than ship a partial implementation.
Expand Down
1 change: 1 addition & 0 deletions docs/building/conformance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Every agent MUST pass every storyboard below.
| [`pagination_integrity_collection_lists`](https://adcontextprotocol.org/compliance/latest/universal/collection-lists-pagination-integrity) | `cursor` ↔ `has_more` invariant on paginated `list_collection_lists` responses; storyboard bootstraps three collection lists via `create_collection_list` and walks from a continuation page to a terminal page |
| [`pagination_integrity_property_lists`](https://adcontextprotocol.org/compliance/latest/universal/property-lists-pagination-integrity) | `cursor` ↔ `has_more` invariant on paginated `list_property_lists` responses; storyboard bootstraps three property lists via `create_property_list` and walks from a continuation page to a terminal page |
| [`deterministic_testing`](https://adcontextprotocol.org/compliance/latest/universal/deterministic-testing) | `comply_test_controller` state machine — skipped if `capabilities.compliance_testing.supported: false` |
| [`comply_controller_mode_gate`](https://adcontextprotocol.org/compliance/latest/universal/comply-controller-mode-gate) | `comply_test_controller` live-mode denial gate — verifies sellers return `FORBIDDEN` when a live-mode account calls the controller; skipped for agents that do not expose `comply_test_controller` |
| [`signed_requests`](https://adcontextprotocol.org/compliance/latest/universal/signed-requests) | RFC 9421 transport-layer request-signing verification — skipped if `request_signing.supported: false`. |

Agents that declare `capabilities.compliance_testing.supported: true` MUST implement the full [test controller](/docs/building/implementation/comply-test-controller); a partial controller is non-conformant, so declare `false` rather than ship one.
Expand Down
78 changes: 78 additions & 0 deletions static/compliance/source/test-kits/acme-outdoor-live.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Acme Outdoor — Live-Mode Principal Test Kit
#
# Entity definition: fictional-entities.yaml → advertisers[acme_outdoor]
#
# Live-mode counterpart to acme-outdoor.yaml. Used exclusively by the
# comply-controller-mode-gate storyboard to supply a non-sandbox principal:
# the seller's gate must resolve the calling account, detect mode: live, and
# return ControllerError.error: FORBIDDEN before dispatching any scenario.
#
# IMPORTANT: sandbox: false here signals that this principal represents a
# live/production account for the purpose of controller gating tests. It does
# NOT mean this kit is used in production grading — the kit is only loaded by
# the comply_controller_mode_gate storyboard, which requires: [controller]
# and is dev/staging-only by design.

id: acme_outdoor_live
name: "Acme Outdoor (Live Mode)"
description: "Fictional outdoor gear brand — live-mode principal for comply_test_controller denial storyboard"
sandbox: false

# Authentication fixture for the comply-controller-mode-gate storyboard.
# The api_key here represents a live-mode (non-sandbox) credential. When the
# runner sends requests using this key, a conformant seller resolves the
# account, determines mode: live, and refuses comply_test_controller dispatch
# with FORBIDDEN.
#
# The demo-acme-outdoor-live- prefix signals this is a test harness key whose
# account resolves as live-mode on conformant implementations.
auth:
api_key: "demo-acme-outdoor-live-v1"
probe_task: list_creatives

brand:
house:
domain: "acmeoutdoor.example"
name: "Acme Outdoor"
brand_id: "acme_outdoor"
names:
- en: "Acme Outdoor"
description: "Premium outdoor gear for every adventure. From trail to summit, we make gear that performs."
industry: "retail"
keller_type: "master"
logos:
- url: "https://test-assets.adcontextprotocol.org/acme-outdoor/logo-primary.png"
orientation: "horizontal"
background: "light-bg"
variant: "primary"
width: 400
height: 100
colors:
primary: "#1B5E20"
secondary: "#FF6F00"
accent: "#FDD835"
background: "#FAFAFA"
text: "#212121"
fonts:
heading:
family: "Montserrat"
weight: 700
url: "https://fonts.googleapis.com/css2?family=Montserrat:wght@700"
body:
family: "Open Sans"
weight: 400
url: "https://fonts.googleapis.com/css2?family=Open+Sans"
tone:
voice: "Confident and adventurous, but never pretentious. We talk to people who do things, not people who buy things."
attributes:
- "active"
- "direct"
- "warm"
dos:
- "Use action verbs"
- "Reference real outdoor activities"
- "Keep it short"
donts:
- "Use superlatives without evidence"
- "Talk down to the reader"
- "Use corporate jargon"
135 changes: 135 additions & 0 deletions static/compliance/source/universal/comply-controller-mode-gate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
id: comply_controller_mode_gate
version: '1.0.0'
title: 'Comply test controller — live-account denial gate'
category: deterministic_testing
summary: 'Verifies that a seller exposing comply_test_controller refuses calls from live-mode (non-sandbox) accounts with FORBIDDEN.'
track: security
required_tools:
- comply_test_controller

requires:
- controller

narrative: |
Sellers that expose comply_test_controller MUST gate dispatch against the calling
account's mode. When a buyer agent authenticates as a live-mode (non-sandbox)
account and calls comply_test_controller, the seller MUST refuse with
error: FORBIDDEN before dispatching any scenario.

This storyboard exercises the denial path by sending a well-formed
force_creative_status request from a live-mode principal (test-kit:
acme-outdoor-live, sandbox: false) and asserting FORBIDDEN is returned.

The live_account_denial phase is optional for two-deployment sellers whose
sandbox endpoint admits all authenticated principals regardless of account mode.
Those sellers rely on their production endpoint simply not advertising
comply_test_controller. Single-endpoint sellers that expose the controller on
their main endpoint MUST implement per-account gating and MUST pass this phase.

This storyboard is the keystone for the (Sandbox) AAO Verified tier — a seller
claiming that badge must prove its compliance infrastructure correctly refuses
live-mode callers from the controller surface, confirming the sandbox/production
boundary is enforced in both directions.

agent:
interaction_model: media_buy_seller
capabilities:
- sells_media
- supports_test_controller
examples:
- 'Any single-endpoint seller with comply_test_controller'

caller:
role: buyer_agent
example: 'Comply test harness'

prerequisites:
description: |
The seller must expose comply_test_controller (verified by requires: [controller]
at storyboard load time). The live-mode test kit (acme-outdoor-live, sandbox: false)
provides an API key whose associated account resolves as live/production mode.
Conformant sellers resolve the account, detect mode: live, and return FORBIDDEN
without dispatching the scenario.
test_kit: 'test-kits/acme-outdoor-live.yaml'

phases:
- id: live_account_denial
title: 'Live-account caller is denied'
narrative: |
A buyer authenticates as the live-mode Acme Outdoor principal (sandbox: false)
and calls comply_test_controller with a well-formed force_creative_status request.

Sellers that implement per-account gating MUST detect mode: live and return
ControllerError with error: FORBIDDEN before any scenario dispatch occurs.

This phase is optional: two-deployment sellers whose sandbox endpoint
has no per-account gate will return a successful state transition rather than
FORBIDDEN, which would fail this phase. Those sellers correctly prevent
live-mode misuse by not advertising the tool on their production endpoint
at all, so an optional result here does not indicate non-conformance.

optional: true

steps:
- id: deny_live_caller
title: 'comply_test_controller returns FORBIDDEN for a live-mode account'
narrative: |
Send a valid force_creative_status request authenticated as the live-mode
principal. The creative_id comply-live-mode-probe-000 is a stable probe
value that will not exist on any real seller; the scenario is benign —
the seller's gate fires before entity lookup, so NOT_FOUND is not a
conformant response here.

The seller MUST resolve the calling account, detect that account.mode
is live (not sandbox), and return FORBIDDEN. Returning UNKNOWN_SCENARIO,
NOT_FOUND, or a success response are all non-conformant for a
single-endpoint seller that implements the gate.
task: comply_test_controller
schema_ref: 'compliance/comply-test-controller-request.json'
response_schema_ref: 'compliance/comply-test-controller-response.json'
doc_ref: '/building/implementation/comply-test-controller'
comply_scenario: comply_controller_mode_gate
stateful: false
auth:
type: api_key
from_test_kit: true
expect_error: true
negative_path: payload_well_formed
expected: |
Seller refuses with:
- success: false
- error: FORBIDDEN
- error_detail (optional): human-readable explanation such as
"comply_test_controller requires a sandbox account; resolved
account is in live mode"

sample_request:
scenario: 'force_creative_status'
params:
creative_id: 'comply-live-mode-probe-000'
status: 'approved'

context:
correlation_id: "comply_controller_mode_gate--deny_live_caller"

validations:
- check: response_schema
description: 'Response matches comply-test-controller-response.json schema (ControllerError branch)'
- check: field_value
path: 'success'
allowed_values:
- false
description: 'Request is rejected — seller refused live-mode controller dispatch'
- check: field_value
path: 'error'
allowed_values:
- 'FORBIDDEN'
description: 'Error code is FORBIDDEN — seller detected live-mode account and denied dispatch before scenario execution'

- check: field_present
path: "context"
description: "Response echoes back the context object"
- check: field_value
path: "context.correlation_id"
value: "comply_controller_mode_gate--deny_live_caller"
description: "Context correlation_id returned unchanged"
Loading