From 2a7440866ad615ba2dd793f42e38786f1477d17c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 14:24:52 +0000 Subject: [PATCH 1/2] feat(training-agent): add cursor pagination to governance list handlers (#3112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds cursor-based pagination to list_content_standards, list_collection_lists, and list_property_lists — the fifth batch in the rolling pagination conformance series (#3095, #3100, #3109, #3110). https://claude.ai/code/session_018AkoeaWmnrsnbXBtK8FLD2 --- .changeset/governance-list-pagination.md | 24 ++ docs/building/compliance-catalog.mdx | 3 + docs/building/conformance.mdx | 3 + .../content-standards-handlers.ts | 33 +- server/src/training-agent/framework-server.ts | 4 + .../inventory-governance-handlers.ts | 30 +- .../src/training-agent/property-handlers.ts | 31 +- ...collection-lists-pagination-integrity.yaml | 283 +++++++++++++++++ ...ontent-standards-pagination-integrity.yaml | 294 ++++++++++++++++++ .../property-lists-pagination-integrity.yaml | 287 +++++++++++++++++ 10 files changed, 987 insertions(+), 5 deletions(-) create mode 100644 .changeset/governance-list-pagination.md create mode 100644 static/compliance/source/universal/collection-lists-pagination-integrity.yaml create mode 100644 static/compliance/source/universal/content-standards-pagination-integrity.yaml create mode 100644 static/compliance/source/universal/property-lists-pagination-integrity.yaml diff --git a/.changeset/governance-list-pagination.md b/.changeset/governance-list-pagination.md new file mode 100644 index 0000000000..10f0b05b44 --- /dev/null +++ b/.changeset/governance-list-pagination.md @@ -0,0 +1,24 @@ +--- +--- + +feat(training-agent): add cursor-based pagination to list_content_standards, list_collection_lists, and list_property_lists handlers + +Fifth entry in the rolling pagination conformance series (#3095, #3100, #3109, #3110). Adds full cursor-based pagination to the three governance list handlers that previously emitted no `pagination` block or a stub `{ has_more: false }`. + +**Handler changes:** +- `handleListContentStandards` — reads `req.pagination?.max_results` (default 50, cap 100), decodes cursor via `decodeOffsetCursor('content_standards', ...)`, slices the filtered result set, and emits a full `pagination` envelope including `total_count` and a continuation `cursor` when `has_more: true`. +- `handleListCollectionLists` — replaces the `{ has_more: false }` stub with a real offset-cursor implementation using kind `'collection_lists'`. +- `handleListPropertyLists` — same pattern, kind `'property_lists'`. + +**inputSchema updates:** `pagination` property added to the `list_content_standards`, `list_collection_lists`, and `list_property_lists` MCP tool definitions, and to `LIST_COLLECTION_LISTS_SCHEMA` in `framework-server.ts` (Zod-validated path) so callers receive the field in tool descriptions. + +**Storyboards:** three new universal storyboards mirror the `pagination-integrity-list-accounts` pattern (create-then-list bootstrap, three items, `max_results=2` two-page walk): +- `static/compliance/source/universal/content-standards-pagination-integrity.yaml` +- `static/compliance/source/universal/collection-lists-pagination-integrity.yaml` +- `static/compliance/source/universal/property-lists-pagination-integrity.yaml` + +**Doc-parity:** both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx` updated with rows for the three new storyboards. + +Non-protocol change (training agent server-side behavior only, `--empty` changeset per playbook). + +Closes #3112. diff --git a/docs/building/compliance-catalog.mdx b/docs/building/compliance-catalog.mdx index 8646c05f00..7eb3183d3b 100644 --- a/docs/building/compliance-catalog.mdx +++ b/docs/building/compliance-catalog.mdx @@ -32,6 +32,9 @@ Every agent runs every storyboard in `/compliance/{version}/universal/` regardle | `pagination-integrity-list-accounts` | `cursor` ↔ `has_more` invariant verified by walking a paginated `list_accounts` response — storyboard bootstraps three accounts via `sync_accounts`, then asserts page 1 is non-terminal and page 2 is terminal with no stale cursor. | | `pagination-integrity-creative-formats` | `cursor` ↔ `has_more` invariant verified by walking a paginated `list_creative_formats` response — storyboard seeds two creative formats via `seed_creative_format`, then asserts page 1 is non-terminal and page 2 is terminal with no stale cursor. | | `get-media-buys-pagination-integrity` | `cursor` ↔ `has_more` invariant verified by walking a paginated `get_media_buys` response — storyboard seeds three media buys via `seed_media_buy`, then asserts page 1 is non-terminal and page 2 is terminal with no stale cursor. | +| `content-standards-pagination-integrity` | `cursor` ↔ `has_more` invariant verified by walking a paginated `list_content_standards` response — storyboard bootstraps three content standards configurations via `create_content_standards`, then asserts page 1 is non-terminal and page 2 is terminal with no stale cursor. | +| `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`. | | `signed-requests` | RFC 9421 transport-layer request-signing verification — skipped if `request_signing.supported: false`. | diff --git a/docs/building/conformance.mdx b/docs/building/conformance.mdx index ad9d03054e..fb20ebccbd 100644 --- a/docs/building/conformance.mdx +++ b/docs/building/conformance.mdx @@ -63,6 +63,9 @@ Every agent MUST pass every storyboard below. | [`pagination_integrity_list_accounts`](https://adcontextprotocol.org/compliance/latest/universal/pagination-integrity-list-accounts) | `cursor` ↔ `has_more` invariant on paginated `list_accounts` responses; storyboard bootstraps three accounts via `sync_accounts` and walks from a continuation page to a terminal page | | [`pagination_integrity_creative_formats`](https://adcontextprotocol.org/compliance/latest/universal/pagination-integrity-creative-formats) | `cursor` ↔ `has_more` invariant on paginated `list_creative_formats` responses; storyboard seeds two creative formats via `seed_creative_format` and walks from a continuation page to a terminal page | | [`get_media_buys_pagination_integrity`](https://adcontextprotocol.org/compliance/latest/universal/get-media-buys-pagination-integrity) | `cursor` ↔ `has_more` invariant on paginated `get_media_buys` responses; storyboard seeds three media buys via `seed_media_buy` and walks from a continuation page to a terminal page | +| [`pagination_integrity_content_standards`](https://adcontextprotocol.org/compliance/latest/universal/content-standards-pagination-integrity) | `cursor` ↔ `has_more` invariant on paginated `list_content_standards` responses; storyboard bootstraps three content standards configurations via `create_content_standards` and walks from a continuation page to a terminal page | +| [`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` | | [`signed_requests`](https://adcontextprotocol.org/compliance/latest/universal/signed-requests) | RFC 9421 transport-layer request-signing verification — skipped if `request_signing.supported: false`. | diff --git a/server/src/training-agent/content-standards-handlers.ts b/server/src/training-agent/content-standards-handlers.ts index ca4adf8cca..cbff8afd0e 100644 --- a/server/src/training-agent/content-standards-handlers.ts +++ b/server/src/training-agent/content-standards-handlers.ts @@ -9,6 +9,7 @@ import { randomUUID } from 'node:crypto'; import type { TrainingContext, ToolArgs, ContentStandardsState } from './types.js'; import { getSession, sessionKeyFromArgs, MAX_CONTENT_STANDARDS_PER_SESSION } from './state.js'; +import { encodeOffsetCursor, decodeOffsetCursor } from './pagination.js'; // ── Tool definitions ───────────────────────────────────────────── @@ -50,6 +51,13 @@ export const CONTENT_STANDARDS_TOOLS = [ channels: { type: 'array', items: { type: 'string' }, description: 'Filter by channel' }, languages: { type: 'array', items: { type: 'string' }, description: 'Filter by language' }, countries: { type: 'array', items: { type: 'string' }, description: 'Filter by country (ISO 3166-1 alpha-2)' }, + pagination: { + type: 'object', + properties: { + max_results: { type: 'integer', minimum: 1, maximum: 100, description: 'Max standards per page (default 50, cap 100).' }, + cursor: { type: 'string', description: 'Continuation token from a previous list_content_standards response.' }, + }, + }, }, }, }, @@ -221,7 +229,12 @@ export async function handleListContentStandards( args: ToolArgs, ctx: TrainingContext, ) { - const req = args as { channels?: string[]; languages?: string[]; countries?: string[] }; + const req = args as { + channels?: string[]; + languages?: string[]; + countries?: string[]; + pagination?: { max_results?: number; cursor?: string }; + }; const session = await getSession(sessionKeyFromArgs(args, ctx.mode, ctx.userId, ctx.moduleId)); let standards = [...session.contentStandards.values()]; @@ -244,8 +257,24 @@ export async function handleListContentStandards( ); } + const totalMatching = standards.length; + const requestedMax = req.pagination?.max_results; + const maxResults = Math.min(typeof requestedMax === 'number' ? requestedMax : 50, 100); + const offset = decodeOffsetCursor('content_standards', req.pagination?.cursor); + if (offset === null) { + return { errors: [{ code: 'INVALID_REQUEST', message: 'pagination.cursor is malformed' }] }; + } + const pageEnd = Math.min(offset + maxResults, totalMatching); + const pageStandards = standards.slice(offset, pageEnd); + const hasMore = pageEnd < totalMatching; + return { - standards: standards.map(toStandardsResponse), + standards: pageStandards.map(toStandardsResponse), + pagination: { + has_more: hasMore, + total_count: totalMatching, + ...(hasMore && { cursor: encodeOffsetCursor('content_standards', pageEnd) }), + }, }; } diff --git a/server/src/training-agent/framework-server.ts b/server/src/training-agent/framework-server.ts index bb157ab116..a9f836eaad 100644 --- a/server/src/training-agent/framework-server.ts +++ b/server/src/training-agent/framework-server.ts @@ -428,6 +428,10 @@ export function createFrameworkTrainingAgentServer(ctx: TrainingContext): AdcpSe account: ACCOUNT_REF, brand: BRAND_REF, context: CONTEXT_REF, + pagination: z.object({ + max_results: z.number().int().min(1).max(100).optional(), + cursor: z.string().optional(), + }).optional(), }; const DELETE_COLLECTION_LIST_SCHEMA = { diff --git a/server/src/training-agent/inventory-governance-handlers.ts b/server/src/training-agent/inventory-governance-handlers.ts index 936b8fd547..5080cf8971 100644 --- a/server/src/training-agent/inventory-governance-handlers.ts +++ b/server/src/training-agent/inventory-governance-handlers.ts @@ -10,6 +10,7 @@ import { randomUUID } from 'node:crypto'; import type { TrainingContext, ToolArgs, CollectionListState } from './types.js'; import { getSession, sessionKeyFromArgs } from './state.js'; import { ACCOUNT_REF_SCHEMA } from './account-handlers.js'; +import { encodeOffsetCursor, decodeOffsetCursor } from './pagination.js'; const MAX_ARRAY_INPUT = 100; @@ -79,6 +80,13 @@ export const COLLECTION_LIST_TOOLS = [ properties: { account: ACCOUNT_REF_SCHEMA, name_contains: { type: 'string' }, + pagination: { + type: 'object', + properties: { + max_results: { type: 'integer', minimum: 1, maximum: 100, description: 'Max lists per page (default 50, cap 100).' }, + cursor: { type: 'string', description: 'Continuation token from a previous list_collection_lists response.' }, + }, + }, }, }, }, @@ -125,6 +133,7 @@ interface UpdateCollectionListInput extends ToolArgs { interface ListInput extends ToolArgs { name_contains?: string; + pagination?: { max_results?: number; cursor?: string }; } interface DeleteInput extends ToolArgs { @@ -226,7 +235,26 @@ export async function handleListCollectionLists(args: ToolArgs, ctx: TrainingCon const filtered = input.name_contains ? lists.filter(l => l.name.toLowerCase().includes(input.name_contains!.toLowerCase())) : lists; - return { lists: filtered, pagination: { has_more: false } }; + + const totalMatching = filtered.length; + const requestedMax = input.pagination?.max_results; + const maxResults = Math.min(typeof requestedMax === 'number' ? requestedMax : 50, 100); + const offset = decodeOffsetCursor('collection_lists', input.pagination?.cursor); + if (offset === null) { + return { errors: [{ code: 'INVALID_REQUEST', message: 'pagination.cursor is malformed' }] }; + } + const pageEnd = Math.min(offset + maxResults, totalMatching); + const pageLists = filtered.slice(offset, pageEnd); + const hasMore = pageEnd < totalMatching; + + return { + lists: pageLists, + pagination: { + has_more: hasMore, + total_count: totalMatching, + ...(hasMore && { cursor: encodeOffsetCursor('collection_lists', pageEnd) }), + }, + }; } export async function handleDeleteCollectionList(args: ToolArgs, ctx: TrainingContext) { diff --git a/server/src/training-agent/property-handlers.ts b/server/src/training-agent/property-handlers.ts index 8739656c01..283749a850 100644 --- a/server/src/training-agent/property-handlers.ts +++ b/server/src/training-agent/property-handlers.ts @@ -10,6 +10,7 @@ import { randomUUID } from 'node:crypto'; import type { TrainingContext, ToolArgs, AccountRef, PropertyListState } from './types.js'; import { getSession, sessionKeyFromArgs, MAX_PROPERTY_LISTS_PER_SESSION } from './state.js'; import { ACCOUNT_REF_SCHEMA } from './account-handlers.js'; +import { encodeOffsetCursor, decodeOffsetCursor } from './pagination.js'; const MAX_PROPERTIES_PER_LIST = 10_000; @@ -46,6 +47,13 @@ export const PROPERTY_TOOLS = [ properties: { account: ACCOUNT_REF_SCHEMA, name_contains: { type: 'string', description: 'Filter to lists whose name contains this string' }, + pagination: { + type: 'object', + properties: { + max_results: { type: 'integer', minimum: 1, maximum: 100, description: 'Max lists per page (default 50, cap 100).' }, + cursor: { type: 'string', description: 'Continuation token from a previous list_property_lists response.' }, + }, + }, }, }, }, @@ -233,7 +241,10 @@ export async function handleListPropertyLists( args: ToolArgs, ctx: TrainingContext, ) { - const req = args as { name_contains?: string }; + const req = args as { + name_contains?: string; + pagination?: { max_results?: number; cursor?: string }; + }; const session = await getSession(sessionKeyFromArgs(args, ctx.mode, ctx.userId, ctx.moduleId)); let lists = [...session.propertyLists.values()]; @@ -243,8 +254,24 @@ export async function handleListPropertyLists( lists = lists.filter(l => l.name.toLowerCase().includes(lowerFilter)); } + const totalMatching = lists.length; + const requestedMax = req.pagination?.max_results; + const maxResults = Math.min(typeof requestedMax === 'number' ? requestedMax : 50, 100); + const offset = decodeOffsetCursor('property_lists', req.pagination?.cursor); + if (offset === null) { + return { errors: [{ code: 'INVALID_REQUEST', message: 'pagination.cursor is malformed' }] }; + } + const pageEnd = Math.min(offset + maxResults, totalMatching); + const pageLists = lists.slice(offset, pageEnd); + const hasMore = pageEnd < totalMatching; + return { - lists: lists.map(toListResponse), + lists: pageLists.map(toListResponse), + pagination: { + has_more: hasMore, + total_count: totalMatching, + ...(hasMore && { cursor: encodeOffsetCursor('property_lists', pageEnd) }), + }, }; } diff --git a/static/compliance/source/universal/collection-lists-pagination-integrity.yaml b/static/compliance/source/universal/collection-lists-pagination-integrity.yaml new file mode 100644 index 0000000000..7ebb61c839 --- /dev/null +++ b/static/compliance/source/universal/collection-lists-pagination-integrity.yaml @@ -0,0 +1,283 @@ +id: pagination_integrity_collection_lists +version: "1.0.0" +title: "Pagination cursor integrity — list_collection_lists" +category: schema_validation +summary: "Validates the cursor↔has_more invariant by walking a paginated list_collection_lists response from a continuation page to a terminal page." +track: core +required_tools: + - list_collection_lists + +narrative: | + Pagination is cursor-based across AdCP. Every response carrying a + `pagination` block satisfies a single invariant: when `has_more` is true the + `cursor` MUST be present (callers need it to fetch the next page); when + `has_more` is false the `cursor` MUST be absent (a stale token on a terminal + page invites callers to follow it into undefined behavior). + + The invariant is documented in prose on + `static/schemas/source/core/pagination-response.json` but JSON Schema does + not gate the two fields against each other. This storyboard exercises both + sides of the invariant on `list_collection_lists` in a single run. + + The runner bootstraps three collection lists via `create_collection_list`, + then lists them back with `max_results=2`. The first page MUST cap at two + lists and signal continuation with `has_more=true` plus a cursor. The runner + then follows that cursor; the second call MUST return the terminal page with + `has_more=false` and no cursor. An agent that hides the third list by + reporting `has_more=false` on the first page fails the first-page assertion. + An agent that carries a stale cursor onto the terminal page fails the + second-page assertion. + + The `total_count` field is allowed to be absent (some backends cannot + compute it cheaply per the pagination-response schema) but when an agent + volunteers it, the value MUST match the seeded count of three. + +agent: + interaction_model: stateful_preloaded + capabilities: [] + examples: + - "Any AdCP agent that exposes a paginated list_collection_lists" + +caller: + role: buyer_agent + example: "Compliance test harness" + +prerequisites: + description: | + The storyboard bootstraps three collection lists in the setup phase via + `create_collection_list`. The seed-DAG does not include collection lists, + so this storyboard uses the create-then-list pattern to keep itself + self-contained without extending the seed taxonomy. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: false + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + Confirm the agent responds to get_adcp_capabilities before + exercising list_collection_lists. + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares supported protocols before + driving paginated list_collection_lists. + task: get_adcp_capabilities + schema_ref: "protocol/get-adcp-capabilities-request.json" + response_schema_ref: "protocol/get-adcp-capabilities-response.json" + doc_ref: "/protocol/get_adcp_capabilities" + comply_scenario: pagination_integrity_collection_lists + stateful: false + expected: | + Return capabilities declaring supported protocols. + + sample_request: + context: + correlation_id: "pagination_integrity_collection_lists--get_capabilities" + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" + - check: field_present + path: "supported_protocols" + description: "Agent declares supported protocols" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_collection_lists--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: setup + title: "Bootstrap three collection lists" + narrative: | + Create three collection lists so the pagination walk has a deterministic + set of three items to page through with max_results=2. + steps: + - id: create_list_1 + title: "Create first collection list" + narrative: | + Create a collection list for primetime drama programs. The agent MUST + accept this and return a list_id. + task: create_collection_list + schema_ref: "collection/create-collection-list-request.json" + response_schema_ref: "collection/create-collection-list-response.json" + doc_ref: "/governance/task-reference/create_collection_list" + comply_scenario: pagination_integrity_collection_lists + stateful: true + expected: | + Accept the collection list and return a list.list_id. + + sample_request: + name: "Primetime Drama Allowlist" + description: "Collection of approved primetime drama programs." + context: + correlation_id: "pagination_integrity_collection_lists--setup_1" + validations: + - check: response_schema + description: "Response matches create-collection-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Response contains list.list_id" + + - id: create_list_2 + title: "Create second collection list" + narrative: | + Create a collection list for news programs. + task: create_collection_list + schema_ref: "collection/create-collection-list-request.json" + response_schema_ref: "collection/create-collection-list-response.json" + doc_ref: "/governance/task-reference/create_collection_list" + comply_scenario: pagination_integrity_collection_lists + stateful: true + expected: | + Accept the collection list and return a list.list_id. + + sample_request: + name: "News Programs Allowlist" + description: "Approved national news broadcast programs." + context: + correlation_id: "pagination_integrity_collection_lists--setup_2" + validations: + - check: response_schema + description: "Response matches create-collection-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Response contains list.list_id" + + - id: create_list_3 + title: "Create third collection list" + narrative: | + Create a collection list for sports programs. + task: create_collection_list + schema_ref: "collection/create-collection-list-request.json" + response_schema_ref: "collection/create-collection-list-response.json" + doc_ref: "/governance/task-reference/create_collection_list" + comply_scenario: pagination_integrity_collection_lists + stateful: true + expected: | + Accept the collection list and return a list.list_id. + + sample_request: + name: "Sports Programs Allowlist" + description: "Approved live sports broadcasts." + context: + correlation_id: "pagination_integrity_collection_lists--setup_3" + validations: + - check: response_schema + description: "Response matches create-collection-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Response contains list.list_id" + + - id: pagination_walk + title: "Walk pages with a small max_results" + narrative: | + With three collection lists seeded and `max_results: 2`, the first call + MUST return a continuation page (has_more=true with a cursor). The runner + then follows the cursor; the second call MUST return the terminal page + (has_more=false with no cursor). + + steps: + - id: first_page + title: "Request the first page with max_results=2" + narrative: | + The buyer asks for up to two collection lists. The agent has three + matching lists, so the request is non-terminal: callers expect + `has_more=true` and a `cursor` they can follow to retrieve the + remaining list. + + The runner captures `pagination.cursor` into `$context.next_cursor` + for the follow-up step. If the agent reports `has_more=false` on + this page (hiding the seeded third list behind a dishonest terminal + signal), the field_value check on `pagination.has_more` fails. + task: list_collection_lists + schema_ref: "collection/list-collection-lists-request.json" + response_schema_ref: "collection/list-collection-lists-response.json" + doc_ref: "/governance/task-reference/list_collection_lists" + comply_scenario: pagination_integrity_collection_lists + stateful: true + context_outputs: + - name: next_cursor + path: "pagination.cursor" + expected: | + Return up to two collection lists with: + - pagination.has_more = true + - pagination.cursor present (an opaque continuation token) + + sample_request: + pagination: + max_results: 2 + + context: + correlation_id: "pagination_integrity_collection_lists--first_page" + validations: + - check: response_schema + description: "Response matches list-collection-lists-response.json schema" + - check: field_value + path: "pagination.has_more" + value: true + description: "Three lists seeded with max_results=2 → first page is non-terminal" + - check: field_present + path: "pagination.cursor" + description: "has_more=true requires a cursor — without one the caller cannot continue" + - check: field_value_or_absent + path: "pagination.total_count" + allowed_values: [3] + description: "If volunteered, total_count MUST equal the seeded set size" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_collection_lists--first_page" + description: "Context correlation_id returned unchanged" + + - id: terminal_page + title: "Follow the cursor to the terminal page" + narrative: | + The buyer follows the captured cursor. With at most one remaining + list the agent MUST return a terminal page: `has_more=false` with no + `cursor` field. A cursor on `has_more=false` is a stale token that + invites callers to follow it past the end of results. + task: list_collection_lists + schema_ref: "collection/list-collection-lists-request.json" + response_schema_ref: "collection/list-collection-lists-response.json" + doc_ref: "/governance/task-reference/list_collection_lists" + comply_scenario: pagination_integrity_collection_lists + stateful: true + expected: | + Return the remaining list(s) with: + - pagination.has_more = false + - pagination.cursor absent + + sample_request: + pagination: + cursor: "$context.next_cursor" + max_results: 2 + + context: + correlation_id: "pagination_integrity_collection_lists--terminal_page" + validations: + - check: response_schema + description: "Response matches list-collection-lists-response.json schema" + - check: field_value + path: "pagination.has_more" + value: false + description: "After two pages of two items the seeded set is exhausted" + - check: field_value_or_absent + path: "pagination.cursor" + allowed_values: [null] + description: "Terminal page MUST omit cursor (null also accepted for clients that explicitly clear the field)" + - check: field_value_or_absent + path: "pagination.total_count" + allowed_values: [3] + description: "total_count (when volunteered) MUST stay stable across pages and equal the seeded set size" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_collection_lists--terminal_page" + description: "Context correlation_id returned unchanged" diff --git a/static/compliance/source/universal/content-standards-pagination-integrity.yaml b/static/compliance/source/universal/content-standards-pagination-integrity.yaml new file mode 100644 index 0000000000..dc6b0b7463 --- /dev/null +++ b/static/compliance/source/universal/content-standards-pagination-integrity.yaml @@ -0,0 +1,294 @@ +id: pagination_integrity_content_standards +version: "1.0.0" +title: "Pagination cursor integrity — list_content_standards" +category: schema_validation +summary: "Validates the cursor↔has_more invariant by walking a paginated list_content_standards response from a continuation page to a terminal page." +track: core +required_tools: + - list_content_standards + +narrative: | + Pagination is cursor-based across AdCP. Every response carrying a + `pagination` block satisfies a single invariant: when `has_more` is true the + `cursor` MUST be present (callers need it to fetch the next page); when + `has_more` is false the `cursor` MUST be absent (a stale token on a terminal + page invites callers to follow it into undefined behavior). + + The invariant is documented in prose on + `static/schemas/source/core/pagination-response.json` but JSON Schema does + not gate the two fields against each other. This storyboard exercises both + sides of the invariant on `list_content_standards` in a single run. + + The runner bootstraps three content standards configurations via + `create_content_standards`, then lists them back with `max_results=2`. The + first page MUST cap at two standards and signal continuation with + `has_more=true` plus a cursor. The runner then follows that cursor; the + second call MUST return the terminal page with `has_more=false` and no + cursor. An agent that hides the third standard by reporting `has_more=false` + on the first page fails the first-page assertion. An agent that carries a + stale cursor onto the terminal page fails the second-page assertion. + + The `total_count` field is allowed to be absent (some backends cannot + compute it cheaply per the pagination-response schema) but when an agent + volunteers it, the value MUST match the seeded count of three. + +agent: + interaction_model: stateful_preloaded + capabilities: [] + examples: + - "Any AdCP agent that exposes a paginated list_content_standards" + +caller: + role: buyer_agent + example: "Compliance test harness" + +prerequisites: + description: | + The storyboard bootstraps three content standards configurations in the + setup phase via `create_content_standards`. The seed-DAG does not include + content standards, so this storyboard uses the create-then-list pattern to + keep itself self-contained without extending the seed taxonomy. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: false + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + Confirm the agent responds to get_adcp_capabilities before + exercising list_content_standards. + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares supported protocols before + driving paginated list_content_standards. + task: get_adcp_capabilities + schema_ref: "protocol/get-adcp-capabilities-request.json" + response_schema_ref: "protocol/get-adcp-capabilities-response.json" + doc_ref: "/protocol/get_adcp_capabilities" + comply_scenario: pagination_integrity_content_standards + stateful: false + expected: | + Return capabilities declaring supported protocols. + + sample_request: + context: + correlation_id: "pagination_integrity_content_standards--get_capabilities" + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" + - check: field_present + path: "supported_protocols" + description: "Agent declares supported protocols" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_content_standards--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: setup + title: "Bootstrap three content standards configurations" + narrative: | + Create three content standards configurations so the pagination walk has + a deterministic set of three items to page through with max_results=2. + steps: + - id: create_standards_1 + title: "Create first content standards configuration" + narrative: | + Create a content standards configuration scoped to English-language + display ads. The agent MUST accept this and return a standards_id. + task: create_content_standards + schema_ref: "content-standards/create-content-standards-request.json" + response_schema_ref: "content-standards/create-content-standards-response.json" + doc_ref: "/governance/task-reference/create_content_standards" + comply_scenario: pagination_integrity_content_standards + stateful: true + expected: | + Accept the configuration and return a standards_id. + + sample_request: + scope: + languages_any: ["en"] + channels_any: ["display"] + policy: "No alcohol, gambling, or tobacco content." + context: + correlation_id: "pagination_integrity_content_standards--setup_1" + validations: + - check: response_schema + description: "Response matches create-content-standards-response.json schema" + - check: field_present + path: "standards_id" + description: "Response contains standards_id" + + - id: create_standards_2 + title: "Create second content standards configuration" + narrative: | + Create a second content standards configuration scoped to Spanish- + language video ads. + task: create_content_standards + schema_ref: "content-standards/create-content-standards-request.json" + response_schema_ref: "content-standards/create-content-standards-response.json" + doc_ref: "/governance/task-reference/create_content_standards" + comply_scenario: pagination_integrity_content_standards + stateful: true + expected: | + Accept the configuration and return a standards_id. + + sample_request: + scope: + languages_any: ["es"] + channels_any: ["video"] + policy: "Family-friendly content only. No political advertising." + context: + correlation_id: "pagination_integrity_content_standards--setup_2" + validations: + - check: response_schema + description: "Response matches create-content-standards-response.json schema" + - check: field_present + path: "standards_id" + description: "Response contains standards_id" + + - id: create_standards_3 + title: "Create third content standards configuration" + narrative: | + Create a third content standards configuration for French-language + connected-TV placements. + task: create_content_standards + schema_ref: "content-standards/create-content-standards-request.json" + response_schema_ref: "content-standards/create-content-standards-response.json" + doc_ref: "/governance/task-reference/create_content_standards" + comply_scenario: pagination_integrity_content_standards + stateful: true + expected: | + Accept the configuration and return a standards_id. + + sample_request: + scope: + languages_any: ["fr"] + channels_any: ["ctv"] + policy: "No pharmaceutical or financial product advertising." + context: + correlation_id: "pagination_integrity_content_standards--setup_3" + validations: + - check: response_schema + description: "Response matches create-content-standards-response.json schema" + - check: field_present + path: "standards_id" + description: "Response contains standards_id" + + - id: pagination_walk + title: "Walk pages with a small max_results" + narrative: | + With three standards seeded and `max_results: 2`, the first call MUST + return a continuation page (has_more=true with a cursor). The runner + then follows the cursor; the second call MUST return the terminal + page (has_more=false with no cursor). + + steps: + - id: first_page + title: "Request the first page with max_results=2" + narrative: | + The buyer asks for up to two content standards. The agent has three + matching configurations, so the request is non-terminal: callers + expect `has_more=true` and a `cursor` they can follow to retrieve + the remaining standard. + + The runner captures `pagination.cursor` into `$context.next_cursor` + for the follow-up step. If the agent reports `has_more=false` on + this page (hiding the seeded third standard behind a dishonest + terminal signal), the field_value check on `pagination.has_more` + fails — surfacing exactly the bug class this storyboard exists to + catch. + task: list_content_standards + schema_ref: "content-standards/list-content-standards-request.json" + response_schema_ref: "content-standards/list-content-standards-response.json" + doc_ref: "/governance/task-reference/list_content_standards" + comply_scenario: pagination_integrity_content_standards + stateful: true + context_outputs: + - name: next_cursor + path: "pagination.cursor" + expected: | + Return up to two standards with: + - pagination.has_more = true + - pagination.cursor present (an opaque continuation token) + + sample_request: + pagination: + max_results: 2 + + context: + correlation_id: "pagination_integrity_content_standards--first_page" + validations: + - check: response_schema + description: "Response matches list-content-standards-response.json schema" + - check: field_value + path: "pagination.has_more" + value: true + description: "Three standards seeded with max_results=2 → first page is non-terminal" + - check: field_present + path: "pagination.cursor" + description: "has_more=true requires a cursor — without one the caller cannot continue" + - check: field_value_or_absent + path: "pagination.total_count" + allowed_values: [3] + description: "If volunteered, total_count MUST equal the seeded set size" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_content_standards--first_page" + description: "Context correlation_id returned unchanged" + + - id: terminal_page + title: "Follow the cursor to the terminal page" + narrative: | + The buyer follows the captured cursor. With at most one remaining + standard the agent MUST return a terminal page: `has_more=false` + with no `cursor` field. A cursor on `has_more=false` is a stale + token that invites callers to follow it past the end of results. + task: list_content_standards + schema_ref: "content-standards/list-content-standards-request.json" + response_schema_ref: "content-standards/list-content-standards-response.json" + doc_ref: "/governance/task-reference/list_content_standards" + comply_scenario: pagination_integrity_content_standards + stateful: true + expected: | + Return the remaining standard(s) with: + - pagination.has_more = false + - pagination.cursor absent (a stale cursor on a terminal page is a + dishonest pagination signal) + + sample_request: + pagination: + cursor: "$context.next_cursor" + max_results: 2 + + context: + correlation_id: "pagination_integrity_content_standards--terminal_page" + validations: + - check: response_schema + description: "Response matches list-content-standards-response.json schema" + - check: field_value + path: "pagination.has_more" + value: false + description: "After two pages of two items the seeded set is exhausted" + - check: field_value_or_absent + path: "pagination.cursor" + allowed_values: [null] + description: "Terminal page MUST omit cursor (null also accepted for clients that explicitly clear the field)" + - check: field_value_or_absent + path: "pagination.total_count" + allowed_values: [3] + description: "total_count (when volunteered) MUST stay stable across pages and equal the seeded set size" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_content_standards--terminal_page" + description: "Context correlation_id returned unchanged" diff --git a/static/compliance/source/universal/property-lists-pagination-integrity.yaml b/static/compliance/source/universal/property-lists-pagination-integrity.yaml new file mode 100644 index 0000000000..c688c14726 --- /dev/null +++ b/static/compliance/source/universal/property-lists-pagination-integrity.yaml @@ -0,0 +1,287 @@ +id: pagination_integrity_property_lists +version: "1.0.0" +title: "Pagination cursor integrity — list_property_lists" +category: schema_validation +summary: "Validates the cursor↔has_more invariant by walking a paginated list_property_lists response from a continuation page to a terminal page." +track: core +required_tools: + - list_property_lists + +narrative: | + Pagination is cursor-based across AdCP. Every response carrying a + `pagination` block satisfies a single invariant: when `has_more` is true the + `cursor` MUST be present (callers need it to fetch the next page); when + `has_more` is false the `cursor` MUST be absent (a stale token on a terminal + page invites callers to follow it into undefined behavior). + + The invariant is documented in prose on + `static/schemas/source/core/pagination-response.json` but JSON Schema does + not gate the two fields against each other. This storyboard exercises both + sides of the invariant on `list_property_lists` in a single run. + + The runner bootstraps three property lists via `create_property_list`, then + lists them back with `max_results=2`. The first page MUST cap at two lists + and signal continuation with `has_more=true` plus a cursor. The runner then + follows that cursor; the second call MUST return the terminal page with + `has_more=false` and no cursor. An agent that hides the third list by + reporting `has_more=false` on the first page fails the first-page assertion. + An agent that carries a stale cursor onto the terminal page fails the + second-page assertion. + + The `total_count` field is allowed to be absent (some backends cannot + compute it cheaply per the pagination-response schema) but when an agent + volunteers it, the value MUST match the seeded count of three. + +agent: + interaction_model: stateful_preloaded + capabilities: [] + examples: + - "Any AdCP agent that exposes a paginated list_property_lists" + +caller: + role: buyer_agent + example: "Compliance test harness" + +prerequisites: + description: | + The storyboard bootstraps three property lists in the setup phase via + `create_property_list`. The seed-DAG does not include property lists, so + this storyboard uses the create-then-list pattern to keep itself + self-contained without extending the seed taxonomy. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: false + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + Confirm the agent responds to get_adcp_capabilities before + exercising list_property_lists. + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares supported protocols before + driving paginated list_property_lists. + task: get_adcp_capabilities + schema_ref: "protocol/get-adcp-capabilities-request.json" + response_schema_ref: "protocol/get-adcp-capabilities-response.json" + doc_ref: "/protocol/get_adcp_capabilities" + comply_scenario: pagination_integrity_property_lists + stateful: false + expected: | + Return capabilities declaring supported protocols. + + sample_request: + context: + correlation_id: "pagination_integrity_property_lists--get_capabilities" + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" + - check: field_present + path: "supported_protocols" + description: "Agent declares supported protocols" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_property_lists--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: setup + title: "Bootstrap three property lists" + narrative: | + Create three property lists so the pagination walk has a deterministic + set of three items to page through with max_results=2. + steps: + - id: create_list_1 + title: "Create first property list" + narrative: | + Create an inclusion property list for approved news publisher + domains. The agent MUST accept this and return a list.list_id. + task: create_property_list + schema_ref: "property/create-property-list-request.json" + response_schema_ref: "property/create-property-list-response.json" + doc_ref: "/governance/task-reference/create_property_list" + comply_scenario: pagination_integrity_property_lists + stateful: true + expected: | + Accept the property list and return a list.list_id. + + sample_request: + name: "Approved News Publishers" + description: "Inclusion list of brand-safe news publisher domains." + list_type: "inclusion" + context: + correlation_id: "pagination_integrity_property_lists--setup_1" + validations: + - check: response_schema + description: "Response matches create-property-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Response contains list.list_id" + + - id: create_list_2 + title: "Create second property list" + narrative: | + Create an exclusion property list for blocked gambling sites. + task: create_property_list + schema_ref: "property/create-property-list-request.json" + response_schema_ref: "property/create-property-list-response.json" + doc_ref: "/governance/task-reference/create_property_list" + comply_scenario: pagination_integrity_property_lists + stateful: true + expected: | + Accept the property list and return a list.list_id. + + sample_request: + name: "Gambling Sites Blocklist" + description: "Exclusion list of gambling and wagering domains." + list_type: "exclusion" + context: + correlation_id: "pagination_integrity_property_lists--setup_2" + validations: + - check: response_schema + description: "Response matches create-property-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Response contains list.list_id" + + - id: create_list_3 + title: "Create third property list" + narrative: | + Create an inclusion property list for premium entertainment + properties. + task: create_property_list + schema_ref: "property/create-property-list-request.json" + response_schema_ref: "property/create-property-list-response.json" + doc_ref: "/governance/task-reference/create_property_list" + comply_scenario: pagination_integrity_property_lists + stateful: true + expected: | + Accept the property list and return a list.list_id. + + sample_request: + name: "Premium Entertainment Properties" + description: "Inclusion list of tier-one entertainment publisher domains." + list_type: "inclusion" + context: + correlation_id: "pagination_integrity_property_lists--setup_3" + validations: + - check: response_schema + description: "Response matches create-property-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Response contains list.list_id" + + - id: pagination_walk + title: "Walk pages with a small max_results" + narrative: | + With three property lists seeded and `max_results: 2`, the first call + MUST return a continuation page (has_more=true with a cursor). The runner + then follows the cursor; the second call MUST return the terminal page + (has_more=false with no cursor). + + steps: + - id: first_page + title: "Request the first page with max_results=2" + narrative: | + The buyer asks for up to two property lists. The agent has three + matching lists, so the request is non-terminal: callers expect + `has_more=true` and a `cursor` they can follow to retrieve the + remaining list. + + The runner captures `pagination.cursor` into `$context.next_cursor` + for the follow-up step. If the agent reports `has_more=false` on + this page (hiding the seeded third list behind a dishonest terminal + signal), the field_value check on `pagination.has_more` fails. + task: list_property_lists + schema_ref: "property/list-property-lists-request.json" + response_schema_ref: "property/list-property-lists-response.json" + doc_ref: "/governance/task-reference/list_property_lists" + comply_scenario: pagination_integrity_property_lists + stateful: true + context_outputs: + - name: next_cursor + path: "pagination.cursor" + expected: | + Return up to two property lists with: + - pagination.has_more = true + - pagination.cursor present (an opaque continuation token) + + sample_request: + pagination: + max_results: 2 + + context: + correlation_id: "pagination_integrity_property_lists--first_page" + validations: + - check: response_schema + description: "Response matches list-property-lists-response.json schema" + - check: field_value + path: "pagination.has_more" + value: true + description: "Three lists seeded with max_results=2 → first page is non-terminal" + - check: field_present + path: "pagination.cursor" + description: "has_more=true requires a cursor — without one the caller cannot continue" + - check: field_value_or_absent + path: "pagination.total_count" + allowed_values: [3] + description: "If volunteered, total_count MUST equal the seeded set size" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_property_lists--first_page" + description: "Context correlation_id returned unchanged" + + - id: terminal_page + title: "Follow the cursor to the terminal page" + narrative: | + The buyer follows the captured cursor. With at most one remaining + list the agent MUST return a terminal page: `has_more=false` with no + `cursor` field. A cursor on `has_more=false` is a stale token that + invites callers to follow it past the end of results. + task: list_property_lists + schema_ref: "property/list-property-lists-request.json" + response_schema_ref: "property/list-property-lists-response.json" + doc_ref: "/governance/task-reference/list_property_lists" + comply_scenario: pagination_integrity_property_lists + stateful: true + expected: | + Return the remaining list(s) with: + - pagination.has_more = false + - pagination.cursor absent + + sample_request: + pagination: + cursor: "$context.next_cursor" + max_results: 2 + + context: + correlation_id: "pagination_integrity_property_lists--terminal_page" + validations: + - check: response_schema + description: "Response matches list-property-lists-response.json schema" + - check: field_value + path: "pagination.has_more" + value: false + description: "After two pages of two items the seeded set is exhausted" + - check: field_value_or_absent + path: "pagination.cursor" + allowed_values: [null] + description: "Terminal page MUST omit cursor (null also accepted for clients that explicitly clear the field)" + - check: field_value_or_absent + path: "pagination.total_count" + allowed_values: [3] + description: "total_count (when volunteered) MUST stay stable across pages and equal the seeded set size" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pagination_integrity_property_lists--terminal_page" + description: "Context correlation_id returned unchanged" From adfaa17b9535912d16da08201d997fdde03860f6 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 25 Apr 2026 11:34:05 -0400 Subject: [PATCH 2/2] fix(compliance): scope identity + idempotency_key + valid channel on governance pagination storyboards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Triage's #3147 hit two build-time lints CI surfaced: 1. **Storyboard scoping** — 15 sample_request blocks (3 storyboards × 5 steps each, minus capability_discovery) omitted brand/account identity. The create_* and list_* tasks for governance lists are tenant-scoped per `lint-storyboard-scoping`. Adds the canonical `account: { brand: { domain: 'acmeoutdoor.example' }, operator: 'pinnacle-agency.example' }` to all 15. 2. **Idempotency_key on mutating setup steps** — 9 create_* sample_requests (3 per storyboard) on `create_collection_list`/`create_content_standards`/ `create_property_list` omitted `idempotency_key`, which their request schemas mark as required. Adds `$generate:uuid_v4#_setup_` per the established convention. 3. **Invalid channel value** — `create_standards_2` used `channels_any: ["video"]` which isn't in the channels enum. Replace with `["olv"]` (the standardized value for online video advertising outside CTV per `static/schemas/source/enums/channels.json`). Verified: 8/8 pagination storyboards pass against the training agent (list_creatives, get_signals, list_creative_formats, list_accounts, get_media_buys, content_standards, collection_lists, property_lists) — 41/41 steps clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...collection-lists-pagination-integrity.yaml | 23 +++++++++++++++++ ...ontent-standards-pagination-integrity.yaml | 25 ++++++++++++++++++- .../property-lists-pagination-integrity.yaml | 23 +++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/static/compliance/source/universal/collection-lists-pagination-integrity.yaml b/static/compliance/source/universal/collection-lists-pagination-integrity.yaml index 7ebb61c839..0fba066e06 100644 --- a/static/compliance/source/universal/collection-lists-pagination-integrity.yaml +++ b/static/compliance/source/universal/collection-lists-pagination-integrity.yaml @@ -110,8 +110,13 @@ phases: Accept the collection list and return a list.list_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" name: "Primetime Drama Allowlist" description: "Collection of approved primetime drama programs." + idempotency_key: "$generate:uuid_v4#pagination_integrity_collection_lists_setup_create_list_1" context: correlation_id: "pagination_integrity_collection_lists--setup_1" validations: @@ -135,8 +140,13 @@ phases: Accept the collection list and return a list.list_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" name: "News Programs Allowlist" description: "Approved national news broadcast programs." + idempotency_key: "$generate:uuid_v4#pagination_integrity_collection_lists_setup_create_list_2" context: correlation_id: "pagination_integrity_collection_lists--setup_2" validations: @@ -160,8 +170,13 @@ phases: Accept the collection list and return a list.list_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" name: "Sports Programs Allowlist" description: "Approved live sports broadcasts." + idempotency_key: "$generate:uuid_v4#pagination_integrity_collection_lists_setup_create_list_3" context: correlation_id: "pagination_integrity_collection_lists--setup_3" validations: @@ -207,6 +222,10 @@ phases: - pagination.cursor present (an opaque continuation token) sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" pagination: max_results: 2 @@ -253,6 +272,10 @@ phases: - pagination.cursor absent sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" pagination: cursor: "$context.next_cursor" max_results: 2 diff --git a/static/compliance/source/universal/content-standards-pagination-integrity.yaml b/static/compliance/source/universal/content-standards-pagination-integrity.yaml index dc6b0b7463..ea950a6558 100644 --- a/static/compliance/source/universal/content-standards-pagination-integrity.yaml +++ b/static/compliance/source/universal/content-standards-pagination-integrity.yaml @@ -110,10 +110,15 @@ phases: Accept the configuration and return a standards_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" scope: languages_any: ["en"] channels_any: ["display"] policy: "No alcohol, gambling, or tobacco content." + idempotency_key: "$generate:uuid_v4#pagination_integrity_content_standards_setup_create_standards_1" context: correlation_id: "pagination_integrity_content_standards--setup_1" validations: @@ -138,10 +143,15 @@ phases: Accept the configuration and return a standards_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" scope: languages_any: ["es"] - channels_any: ["video"] + channels_any: ["olv"] policy: "Family-friendly content only. No political advertising." + idempotency_key: "$generate:uuid_v4#pagination_integrity_content_standards_setup_create_standards_2" context: correlation_id: "pagination_integrity_content_standards--setup_2" validations: @@ -166,10 +176,15 @@ phases: Accept the configuration and return a standards_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" scope: languages_any: ["fr"] channels_any: ["ctv"] policy: "No pharmaceutical or financial product advertising." + idempotency_key: "$generate:uuid_v4#pagination_integrity_content_standards_setup_create_standards_3" context: correlation_id: "pagination_integrity_content_standards--setup_3" validations: @@ -217,6 +232,10 @@ phases: - pagination.cursor present (an opaque continuation token) sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" pagination: max_results: 2 @@ -264,6 +283,10 @@ phases: dishonest pagination signal) sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" pagination: cursor: "$context.next_cursor" max_results: 2 diff --git a/static/compliance/source/universal/property-lists-pagination-integrity.yaml b/static/compliance/source/universal/property-lists-pagination-integrity.yaml index c688c14726..2d44b150ac 100644 --- a/static/compliance/source/universal/property-lists-pagination-integrity.yaml +++ b/static/compliance/source/universal/property-lists-pagination-integrity.yaml @@ -110,9 +110,14 @@ phases: Accept the property list and return a list.list_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" name: "Approved News Publishers" description: "Inclusion list of brand-safe news publisher domains." list_type: "inclusion" + idempotency_key: "$generate:uuid_v4#pagination_integrity_property_lists_setup_create_list_1" context: correlation_id: "pagination_integrity_property_lists--setup_1" validations: @@ -136,9 +141,14 @@ phases: Accept the property list and return a list.list_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" name: "Gambling Sites Blocklist" description: "Exclusion list of gambling and wagering domains." list_type: "exclusion" + idempotency_key: "$generate:uuid_v4#pagination_integrity_property_lists_setup_create_list_2" context: correlation_id: "pagination_integrity_property_lists--setup_2" validations: @@ -163,9 +173,14 @@ phases: Accept the property list and return a list.list_id. sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" name: "Premium Entertainment Properties" description: "Inclusion list of tier-one entertainment publisher domains." list_type: "inclusion" + idempotency_key: "$generate:uuid_v4#pagination_integrity_property_lists_setup_create_list_3" context: correlation_id: "pagination_integrity_property_lists--setup_3" validations: @@ -211,6 +226,10 @@ phases: - pagination.cursor present (an opaque continuation token) sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" pagination: max_results: 2 @@ -257,6 +276,10 @@ phases: - pagination.cursor absent sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" pagination: cursor: "$context.next_cursor" max_results: 2