diff --git a/.changeset/3-0-1-release-notes.md b/.changeset/3-0-1-release-notes.md deleted file mode 100644 index 0ff6529f8f..0000000000 --- a/.changeset/3-0-1-release-notes.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add curated 3.0.1 release notes section to `docs/reference/release-notes.mdx` and bump the FAQ's current-version mention to 3.0.1. Frames 3.0.1 as a stable-surface no-op for 3.0-conformant agents, with an honest accounting of what did change: skills bundle in the canonical tarball, normative clarifications (no wire change), additive fields on experimental surfaces (governance, TMP) per the experimental contract, two new conformance-harness scenarios, and one docs-level deprecation (`get_signals` top-level `max_results`). Cross-links to the experimental-status contract so adopters know why patch-level additions are permitted on those surfaces. diff --git a/.changeset/3.0.1-skills-bundle-and-path-fix.md b/.changeset/3.0.1-skills-bundle-and-path-fix.md deleted file mode 100644 index 135a0fcd36..0000000000 --- a/.changeset/3.0.1-skills-bundle-and-path-fix.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Cut **3.0.1** to ship `skills/` in the protocol tarball and fix path drift in `skills/call-adcp-agent/SKILL.md`. Closes #3116, #3117. - -**Why a patch bump (not a re-cut at 3.0.0):** the protocol tarball is the SDK distribution surface. `3.0.0.tgz` was published 2026-04-22, before #3097 hoisted `skills/`. Re-cutting at the same version would mean a new SHA-256 at the same stable URL — incompatible with content-addressed pipelines, supply-chain attestations, and the cosign signature bound to the original content. Pre-merge expert review (protocol + security) recommended bumping to preserve immutability and produce a fresh signed release through the normal `release.yml` path. - -**What's in 3.0.1:** - -- `skills/` bundled in `/protocol/3.0.1.tgz` (the seven protocol-managed skills: `call-adcp-agent` + the per-protocol `adcp-{brand,creative,governance,media-buy,si,signals}`) -- `manifest.contents.skills` enumerated for SDK sync scripts to detect -- `skills/call-adcp-agent/SKILL.md` — replace four hardcoded `dist/schemas//bundled/...` references with discovery-first phrasing that doesn't assume an SDK layout -- `docs/protocol/calling-an-agent.mdx` — sister content fix - -**What does NOT change:** every schema, every task definition, every wire-format detail in 3.0.0 carries over identically to 3.0.1. The bump is for the bundle/skill axis, not the protocol-spec axis. - -**SDK action:** bump `ADCP_VERSION` from `3.0.0` to `3.0.1` to receive the canonical skills via your existing sync flow. JS-side wiring is in [adcontextprotocol/adcp-client#965](https://github.com/adcontextprotocol/adcp-client/pull/965); Python and Go follow-ups tracked in [adcp-client-python#274](https://github.com/adcontextprotocol/adcp-client-python/issues/274) and [adcp-go#91](https://github.com/adcontextprotocol/adcp-go/issues/91). diff --git a/.changeset/3075-signed-requests-universal.md b/.changeset/3075-signed-requests-universal.md deleted file mode 100644 index e28536abd5..0000000000 --- a/.changeset/3075-signed-requests-universal.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -patch: complete the signed-requests reclassification (specialism → universal capability-gated storyboard) - -Follow-up patch to #3076 that completes the `signed-requests` reclassification flagged in #3075. - -**File move:** `static/compliance/source/specialisms/signed-requests/index.yaml` → `static/compliance/source/universal/signed-requests.yaml`. Removed `protocol: media-buy` and `status: deprecated` (universal storyboards have neither — they're cross-protocol and inherent to the suite). - -**Test-kit update:** `static/compliance/source/test-kits/signed-requests-runner.yaml` — `applies_to.specialism: signed-requests` → `applies_to.universal_storyboard: signed-requests`. Header comments and references field updated to point at the universal storyboard. Test-kit file path is unchanged (still `test-kits/signed-requests-runner.yaml`). - -**Docs:** `docs/building/conformance.mdx` adds `signed_requests` to the universal-storyboards table, gated on `request_signing.supported: true` (mirrors `deterministic_testing` which is gated on `compliance_testing.supported: true`). - -**Schema:** `static/schemas/source/enums/specialism.json` — `signed-requests` enum value retained for backward compatibility on a new `x-deprecated-enum-values` allowlist that the build-time parity check (`scripts/build-compliance.cjs verifyEnumParity`) respects. 4.0 enum removal tracked at #3078. - -**Docs cross-link:** `docs/building/implementation/security.mdx` — Signed Requests (Transport Layer) section now points at the universal storyboard so readers landing on the implementation reference can find the conformance suite. - -**Cross-references:** updated comments in `static/compliance/source/universal/storyboard-schema.yaml` (illustrative `applies_to` syntax now uses `universal_storyboard:` instead of `specialism:` example), `static/compliance/source/universal/runner-output-contract.yaml` ("specialisms" → "storyboards"), and `static/compliance/source/specialisms/governance-aware-seller/index.yaml` (notes the reclassification while keeping the example pattern relevant). - -Why patch (per the rule established in #3076): conformance-suite changes version independently of spec, and this is a taxonomy reclassification that doesn't change what an agent must do on the wire. Sellers continue to advertise `request_signing.supported: true` and implement the verifier per the security profile; the runner now reaches them via the universal storyboard instead of the per-protocol specialism. No graded users today (the prior specialism was preview status). 28+ test vectors at `static/compliance/source/test-vectors/request-signing/` are unchanged and shared. - -Closes #3075. diff --git a/.changeset/a2a-1-0-docs-and-extractor-compat.md b/.changeset/a2a-1-0-docs-and-extractor-compat.md deleted file mode 100644 index 043da850f9..0000000000 --- a/.changeset/a2a-1-0-docs-and-extractor-compat.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -docs(a2a): update A2A integration docs and response-extraction spec for A2A 1.0 wire format and placement changes - -A2A 1.0 (Linux Foundation) introduces breaking changes to the wire format and response placement: `Part` no longer carries a `kind` discriminator (content type is implied by which field is set — `text`, `data`, `url`, `raw`); `Role` and `TaskState` enums use ProtoJSON canonical form (`ROLE_USER`, `TASK_STATE_COMPLETED`); the Agent Card moves `url` and `protocolVersion` into a `supportedInterfaces` array; file Parts use `url` / `filename` / `mediaType`; streaming frames and push-notification payloads wrap the event in a `StreamResponse` oneof (`{ task }` / `{ statusUpdate }` / `{ artifactUpdate }` / `{ message }`); and two new task states were added (`TASK_STATE_REJECTED` as a terminal state for policy/validation rejections, `TASK_STATE_AUTH_REQUIRED` as an interim state for auth challenges during execution). - -Updated `a2a-guide.mdx` and `a2a-response-format.mdx` to use the 1.0 wire format in all examples, with a side-by-side compatibility section and a dual-advertising Agent Card example. Fixed two pre-existing contradictions in `a2a-response-format.mdx`: the interim-response example now places data in `status.message.parts` (matching the extraction rule, not in `artifacts`), and the Quick Reference table documents the artifacts→`status.message` fallback that the algorithm has always performed. Added a decision rule for structured-error placement (artifact DataPart with `adcp_error` for `failed`/`rejected`; free-text `status.message` only for transport-level failures with no task artifact). - -Made the normative extraction algorithm in `a2a-response-extraction.mdx` explicitly dual-compatible and placement-correct: it unwraps A2A 1.0 `StreamResponse` envelopes before reading fields, normalizes `status.state` (stripping the `TASK_STATE_` prefix), detects DataParts by field presence rather than by the `kind` discriminator, and handles `rejected` (terminal) and `auth-required` (interim) alongside the existing states. Added an "AdCP Conventions on Top of A2A" section calling out the single-artifact invariant, last-DataPart authority, first-DataPart-for-interim, and wrapper rejection as AdCP-specific rules. - -Hardened the normative extraction algorithm against the new A2A 1.0 attack surface: nested-envelope smuggling (`{ task: { task: {...} } }`) is explicitly rejected as malformed rather than recursively unwrapped; state normalization requires exact ASCII string equality after prefix-strip to prevent collision with seller-invented `TASK_STATE_*` variants; bare `{ message }` envelopes MUST NOT receive a 200 acknowledgment (avoids acting as a presence oracle); and cancel origin must be client-reconciled — a seller-supplied `adcp_error` on a user-initiated cancel MUST be ignored to prevent bookkeeping or retry-logic manipulation. Added a normative "Auth Challenge URL Validation" subsection covering `auth-required` challenge URLs (https-only, no userinfo, agent-origin allowlist, strip seller-supplied redirect_uri) — unvalidated use is an OAuth-phishing and SSRF vector. Added a "Seller-Controlled String Hygiene" note for log-injection and UI-escape handling on `adcp_error` text. - -Extended the reference extractor in `tests/a2a-response-extraction.test.cjs` with envelope unwrapping, the two new states, and the hardening rules above. Added 14 A2A 1.0 wire-format test vectors (Part shapes without `kind`, ProtoJSON states, `rejected`, `auth-required`, wrapped `{ task }` / `{ statusUpdate }` / `{ artifactUpdate }` envelopes) and a safety-hardening suite (nested envelopes, array/null inner values, `response: null` and `response: []` not-a-wrapper, exact-match normalization). 69 tests pass. - -No server, SDK, or protocol schema changes — documentation, reference extractor, and test-vector surface only. The JS `@a2a-js/sdk` 1.0 is not yet published on npm; bumping `@adcp/client` is tracked separately. Cross-repo follow-ups (wrapper policy alignment, webhook envelope wrapping, additional test coverage) are tracked in adcp-client-python#263. diff --git a/.changeset/acquire-rights-validation-clauses.md b/.changeset/acquire-rights-validation-clauses.md deleted file mode 100644 index a326257f3b..0000000000 --- a/.changeset/acquire-rights-validation-clauses.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -docs(brand): specify normative request-validation clauses for `acquire_rights` (closes #2680, #2681) - -Two campaign-field validations on `acquire_rights` were sensible-but-unspecified in 3.0, leaving implementers to disagree on identical requests: - -1. **Expired campaign window.** Brand agents MUST reject with `INVALID_REQUEST` and `field: "campaign.end_date"` when `campaign.end_date` is in the past at the time of the request. Issuing a zero-duration grant is almost always a buyer-side bug; deterministic rejection is more useful than silent expiry. Unlike `create_media_buy` (where `any_of` supports time-shifting a flight forward), rights grants attach to the requested period and cannot be retroactively shifted, so reject-only is the correct contract. - -2. **CPM-priced rights under a governed plan.** When the request carries an intent-phase `governance_context` token (the buyer's plan is governed) and the selected pricing option has `model: "cpm"`, brand agents MUST reject with `INVALID_REQUEST` and `field: "campaign.estimated_impressions"` when that field is omitted or `0`. When provided, projected commitment is `(pricing_option.price / 1000) × campaign.estimated_impressions` evaluated in `pricing_option.currency`. If `pricing_option.currency` differs from the plan's budget currency, the agent MUST reject with `field: "pricing_option_id"` — currency conversion is not specified. If the projected commitment exceeds remaining plan budget, the agent MUST reject with `field: "campaign.estimated_impressions"`. Non-CPM pricing options commit the flat amount regardless of volume; agents MUST NOT require `estimated_impressions` for governance projection on those. - -Added a new "Request validation" section to `docs/brand-protocol/tasks/acquire_rights.mdx` and tightened the field descriptions on `static/schemas/source/brand/acquire-rights-request.json` for `campaign.end_date` and `campaign.estimated_impressions` so the validation contract is discoverable from both the task reference and the schema. - -Patch-eligible: docs-only clarification of behavior the spec already implied. No schema shape changes (only description text); no new error codes (`INVALID_REQUEST` is already standard). The `governance_context` anchor and the `(price / 1000) × impressions` projection formula reference fields that exist on the published 3.0 schemas — this PR does not introduce new wire surface, only normative interpretation. diff --git a/.changeset/add-claude-routines-infra.md b/.changeset/add-claude-routines-infra.md deleted file mode 100644 index 96a3b1bb9e..0000000000 --- a/.changeset/add-claude-routines-infra.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add Claude Code routines scaffolding: triage-prompt, context-refresh-prompt, environment-setup, current-context snapshot, and a GitHub Actions bridge that fires the triage routine's `/fire` endpoint on `issues.opened`/`reopened` so event response happens in minutes instead of waiting for the next scheduled run. diff --git a/.changeset/add-list-accounts-handler.md b/.changeset/add-list-accounts-handler.md deleted file mode 100644 index cf92a54dbc..0000000000 --- a/.changeset/add-list-accounts-handler.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `handleListAccounts` to the training agent: cursor-based pagination, status/sandbox filters, explicit camelCase→snake_case mapping from `AccountState`, compliance fixture fallback pool for empty sessions, HANDLER_MAP wiring, and a `pagination-integrity-list-accounts.yaml` storyboard that bootstraps three accounts via `sync_accounts` and walks the cursor↔has_more invariant. Unblocks pagination conformance gating for `list_accounts`. Follows the `handleListCreatives` pattern from #3095/#3100. diff --git a/.changeset/add-mcp-strict-required-forbidden-routes.md b/.changeset/add-mcp-strict-required-forbidden-routes.md deleted file mode 100644 index 0d1e18898b..0000000000 --- a/.changeset/add-mcp-strict-required-forbidden-routes.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -feat(training-agent): add /mcp-strict-required and /mcp-strict-forbidden conformance routes - -Adds two grader-targeted MCP routes that expose `covers_content_digest='required'` and `'forbidden'` modes for the request-signing conformance grader: - -- `/mcp-strict-required` — advertises and enforces `covers_content_digest: 'required'`. Enables grader vector neg/007 (`missing-content-digest`): rejects signatures that omit content-digest coverage. -- `/mcp-strict-forbidden` — advertises and enforces `covers_content_digest: 'forbidden'`. Enables grader vector neg/018 (`digest-covered-when-forbidden`): rejects signatures that include content-digest coverage. - -Previously, `/mcp-strict` advertised `'either'` (correct for the sandbox) so neg/007 and neg/018 could never fire — the verifier correctly accepted both probe shapes, leaving buyers with no endpoint to test required-mode and forbidden-mode rejection paths. - -**Implementation:** refactors the internal `buildStrictAuthenticator` into a `buildStrictModeAuthenticator(lazyAuth)` factory that accepts a lazy signing authenticator, allowing each route to hold its own capability instance baked at init time (per-request capability swap is unsafe because `verifySignatureAsAuthenticator` captures the capability at construction). Adds `digestMode` to `TrainingContext` so `selectSigningCapability(ctx)` can pick the right capability for the `get_adcp_capabilities` advertisement. - -Server-only change. No schema modifications. Closes #3339. diff --git a/.changeset/add-seed-creative-format-pagination.md b/.changeset/add-seed-creative-format-pagination.md deleted file mode 100644 index 20a9c88248..0000000000 --- a/.changeset/add-seed-creative-format-pagination.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -feat(compliance): add seed_creative_format scenario and list_creative_formats pagination - -Adds `seed_creative_format` to `comply_test_controller` so the compliance harness can pre-populate a deterministic, size-controlled set of creative formats for pagination-integrity storyboards. `comply_test_controller` is a conformance-harness surface, not a core-protocol task — additive enum extensions on it bump at patch level under AdCP semver. - -**Schema changes (comply-test-controller-request.json, comply-test-controller-response.json):** `seed_creative_format` added to the `scenario` enum in both files. The request schema gains a `params.format_id` string field (required when `scenario = seed_creative_format`) and the response schema's `list_scenarios` enum is extended to match. - -**Training-agent implementation:** `seed_creative_format` is handled in `handleComplyTestController` before the SDK dispatcher. Seeded formats are stored in a new `session.complyExtensions.seededCreativeFormats` map and replace the static catalog when non-empty for `list_creative_formats` responses. - -**Pagination:** `handleListCreativeFormats` now applies cursor-based pagination (matching the `list_creatives` pattern) and is session-aware to read seeded formats. Non-compliance callers continue to see the full static catalog with pagination applied. - -**Storyboard:** `pagination-integrity-creative-formats.yaml` exercises the cursor↔has_more invariant on `list_creative_formats` by seeding two formats and walking pages at `max_results=1`. - -Non-breaking: adds a new enum value and optional param. Sellers that don't implement `seed_creative_format` will return `UNKNOWN_SCENARIO`; the storyboard's `controller_seeding: true` signals that support is required for this storyboard to pass. Existing callers of `list_creative_formats` are unaffected — pagination fields are additive to the response. - -Closes #3108. diff --git a/.changeset/add-storyboard-negative-path-attribute.md b/.changeset/add-storyboard-negative-path-attribute.md deleted file mode 100644 index b4e6c84d5b..0000000000 --- a/.changeset/add-storyboard-negative-path-attribute.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Introduce `negative_path` attribute for storyboard steps to distinguish `schema_invalid` (skip lint, default) from `payload_well_formed` (schema-valid payload, validate anyway) negative-path tests. Renames the value `business_rule` → `payload_well_formed` across 26 storyboard steps, the lint predicate, and the schema doc — the new name is broader (covers auth failures + state conflicts + governance denials, not just business rules). Also adds governance checking to `handleActivateSignal` in the training agent so the `signal_marketplace/governance_denied` storyboard passes CI when governance plans are registered, and adds a `sample_response` fixture to the `activate_signal_denied` step. Implements #2824. diff --git a/.changeset/add-storyboard-response-schema-lint.md b/.changeset/add-storyboard-response-schema-lint.md deleted file mode 100644 index e6802bcfb6..0000000000 --- a/.changeset/add-storyboard-response-schema-lint.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add response-side storyboard schema lint (`scripts/lint-storyboard-response-schema.cjs`) that validates `sample_response` fixtures against `response_schema_ref` using AJV, mirroring the existing request-side lint. Includes shrink-only ratchet allowlist, Node test wrapper, and CI wiring. Implements #2823. diff --git a/.changeset/add-system-settings-audit.md b/.changeset/add-system-settings-audit.md deleted file mode 100644 index f988d9abe0..0000000000 --- a/.changeset/add-system-settings-audit.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add audit history table for system_settings changes. Records every setSetting call (key, old_value, new_value, changed_by, changed_at) using an atomic writable CTE. Surfaces the last 50 changes in a new "Recent changes" section on the admin settings page. Also adds the editorial and announcement channel UI sections that were missing from admin-settings.html. diff --git a/.changeset/addie-aao-oauth-knowledge.md b/.changeset/addie-aao-oauth-knowledge.md deleted file mode 100644 index 28cba7c32f..0000000000 --- a/.changeset/addie-aao-oauth-knowledge.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Teach Addie that AAO runs a production OAuth 2.1 + OIDC authorization server (RFC 8414 discovery, RFC 9728 protected-resource metadata for `/api` and `/mcp`, RFC 7591 dynamic client registration), and that AAO platform auth is distinct from AdCP protocol-level bearer auth between agents. Closes Escalation #288 (Emma Mulitz / Scope3): Addie was telling users AAO doesn't support OAuth, which was wrong. diff --git a/.changeset/addie-adoption-prompt-rules.md b/.changeset/addie-adoption-prompt-rules.md deleted file mode 100644 index dae4df4751..0000000000 --- a/.changeset/addie-adoption-prompt-rules.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Stage 1.5 of persona-driven Addie suggested prompts (#2299): add the two deferred adoption rules. Adds `adoption.has_company_listing` (derived from existing fetched profile, free) and `adoption.team_wg_coverage` (one DB query against `working_group_memberships`, reusing the WorkOS membership list already fetched) to MemberContext. Two new rules: - -- **List my company in the directory** (priority 76): non-personal org with no public listing, fires for org owners and admins. -- **Find working groups for my team** (priority 73): owner/admin of a 3+ team with less than half in any working group. - -API key count rule is still deferred — needs a cheaper signal source than per-request WorkOS API calls. diff --git a/.changeset/addie-agent-context-and-experts.md b/.changeset/addie-agent-context-and-experts.md deleted file mode 100644 index 55400eafce..0000000000 --- a/.changeset/addie-agent-context-and-experts.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Addie now sees the shared agent infrastructure: - -- The weekly `.agents/current-context.md` snapshot is injected into her cached system prompt under a `# Current AdCP Context` heading, so she can answer roadmap questions with current signal instead of guessing. -- The expert personas under `.claude/agents/` (ad-tech-protocol-expert, adtech-product-expert, code-reviewer, etc.) are summarized into a `# Expert Panel` reference block — Addie knows which voice is appropriate for deep questions. -- New `ModelConfig.depth` tier (default `claude-opus-4-7[1m]`, overridable via `CLAUDE_MODEL_DEPTH`) routes `requires_depth` turns to the same model the AdCP triage routines use, keeping protocol answers consistent across surfaces. `requires_precision` (billing/financial) remains on `ModelConfig.precision` (Opus 4.6) unchanged. diff --git a/.changeset/addie-agent-test-staleness.md b/.changeset/addie-agent-test-staleness.md deleted file mode 100644 index 6d0755a3d7..0000000000 --- a/.changeset/addie-agent-test-staleness.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Closes #3254: agent test staleness signal feeding a new builder-persona prompt rule. Reuses the existing `agent_test_history` table (no new schema) and adds a `getLatestTestForUser` query. Hydrates `agent_testing.{last_test_at, last_outcome}` onto MemberContext. New rule `agent.stale_test` (priority 91, decay enabled) fires for `molecule_builder` and `pragmatic_builder` personas when the last test is older than 14 days or never run, with label "Run a fresh agent test." Generic `member.test_my_agent` (priority 50) now suppresses when the high-priority stale rule is firing to avoid duplicate-intent prompts in the top 4. Storyboard runner (`run_storyboard`) now records to `agent_test_history` so its runs count toward the staleness signal alongside `evaluate_agent_quality`. diff --git a/.changeset/addie-always-available-gh-tools.md b/.changeset/addie-always-available-gh-tools.md deleted file mode 100644 index 0602b3cc58..0000000000 --- a/.changeset/addie-always-available-gh-tools.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Stop Addie from hallucinating that GitHub issue filing is unavailable in Slack threads. `create_github_issue` joins `draft_github_issue` in `ALWAYS_AVAILABLE_TOOLS`, the content tool-set description no longer claims ownership of GitHub issuing, and the unavailable-sets hint now explicitly enumerates always-available escape hatches. Also tightens `draft_github_issue` so Addie can't invent non-existent repo names, and points the Connect-GitHub fallback at the actual `/member-hub` page. diff --git a/.changeset/addie-cert-continuation.md b/.changeset/addie-cert-continuation.md deleted file mode 100644 index 468b09eef5..0000000000 --- a/.changeset/addie-cert-continuation.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Stage 2 of persona-driven Addie suggested prompts (#2299) — Piia's flagship example: a learner mid-certification gets a "Continue certification" prompt at the top of Addie's home. - -- Adds `MemberContext.certification` (track_id, module_id, status, started_at, last_activity_at) hydrated in both Slack and web flows. -- New `getLatestAttempt(userId)` query (LIMIT 1, prefers in-progress) replaces a full-scan call to `getUserAttempts`. -- New rule `cert.continue_in_progress` at priority **93** (above lapsed at 92 — a concrete unfinished thing beats generic re-engagement when both signals are present). -- **Decay-exempt** like persona prompts: re-engaging a stalled learner is exactly the high-value case; don't auto-suppress. -- **Freshness guard**: only fires when `started_at` is within the last 45 days. Past that, the learner has likely moved on and the lapsed-re-engagement rule (or low-login) handles re-entry better than nudging about an abandoned artifact. -- Gates `persona.ladder_or_simple_starter` ("Start with the Academy") to skip when the learner is already mid-cert — "Continue certification" is the more accurate prompt at that point. - -Out of scope (filed for follow-up): dynamic per-module label like "Continue A1" — needs `PromptRule.label`/`prompt` to accept a function, which is a broader refactor. diff --git a/.changeset/addie-context-coverage-for-cta-blocks.md b/.changeset/addie-context-coverage-for-cta-blocks.md deleted file mode 100644 index 9fa9a3b67d..0000000000 --- a/.changeset/addie-context-coverage-for-cta-blocks.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Surface the CTA-producing context blocks to Addie's reasoning, not just her rules engine. `formatMemberContextForPrompt` now renders sections for `certification`, `agent_testing`, `perspectives`, `next_event`, and `adoption` — five blocks that were hydrated onto MemberContext for the suggested-prompts engine but had been invisible to Addie's system prompt. After this change, when a learner clicks "Continue A1" Addie's system prompt already says "Currently working on: A1 (in progress, started 7 days ago)" instead of forcing a tool round-trip to figure out what they're working on. Same for stale agent tests, upcoming events, the user's perspectives footprint, and missing public company listings. diff --git a/.changeset/addie-cta-registry-strangler.md b/.changeset/addie-cta-registry-strangler.md deleted file mode 100644 index 8846e09520..0000000000 --- a/.changeset/addie-cta-registry-strangler.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Strangler-fig refactor: the digest-newsletter `pickNudge` no longer maintains its own parallel rule list. Three cross-cut CTAs (cert continuation, find a working group, complete profile) now live in the suggested-prompts registry with both a `pull` facet (Addie home) and a new `digest` facet, each with its own typed `when` clause. Three digest-only CTAs (membership conversion, Slack onboarding, contributor cert) live in `DIGEST_ONLY_NUDGES`. `pickNudge` becomes a thin wrapper that merges both lists, filters by `digest.when`, and picks lowest priority. No behavior change — same 7 nudges, same priority order, same copy. The win is structural: one catalog, per-surface eligibility. Adding a new CTA used to mean editing both `prompt-rules.ts` and `digest-nudge.ts`; now it's one place. Documented the invariant in `.agents/playbook.md` along with the data-plane invariant about `MemberContext` fields surfacing to both the rules engine and `formatMemberContextForPrompt`. diff --git a/.changeset/addie-dynamic-prompt-labels.md b/.changeset/addie-dynamic-prompt-labels.md deleted file mode 100644 index f28abe816f..0000000000 --- a/.changeset/addie-dynamic-prompt-labels.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Closes #3304: dynamic per-context labels and prompts for Addie's suggested-prompts engine. `PromptRule.label` and `.prompt` now accept `string | (ctx) => string`. Resolves at evaluation time. Dynamic-prompt rules opt into a `matchClick` callback for click telemetry since the static reverse-index can't represent function strings. Cert continuation now renders "Continue A1" / "Let's keep going with A1. Where did we leave off?" when module_id is known, falling back to track_id ("Continue A") and the original generic phrasing when neither is present. diff --git a/.changeset/addie-escalation-triage-suggestions.md b/.changeset/addie-escalation-triage-suggestions.md deleted file mode 100644 index 3850ffea0d..0000000000 --- a/.changeset/addie-escalation-triage-suggestions.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Addie now runs a daily escalation triage pass that writes suggested -resolutions (resolve / wont_do / keep_open) for open escalations older -than 7 days. Suggestions land in a new `escalation_triage_suggestions` -table — admins review them at `/admin/escalations/triage` and one-click -accept or reject. Nothing is auto-resolved: the job is suggest-only so -every close still has an operator behind it. - -The MVP classifier is rule-based (URL probe against agenticadvertising.org, -referenced-escalation chasing, stale-ops age heuristic) — the same rules -used to clean up the 73 stale escalations on 2026-04-24. An LLM -classification pass can layer on later without changing the suggestion -schema. diff --git a/.changeset/addie-fix-version-knowledge.md b/.changeset/addie-fix-version-knowledge.md deleted file mode 100644 index 5cee839d7f..0000000000 --- a/.changeset/addie-fix-version-knowledge.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -fix(addie): remove hardcoded version info from rules, defer to search_docs - -Addie was answering version/maturity questions from stale hardcoded rules -instead of looking them up from the live docs. No protocol changes. diff --git a/.changeset/addie-grade-and-diagnose-auth-wrappers.md b/.changeset/addie-grade-and-diagnose-auth-wrappers.md deleted file mode 100644 index 00859b93d4..0000000000 --- a/.changeset/addie-grade-and-diagnose-auth-wrappers.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Add `grade_agent_signing` and `diagnose_agent_auth` tools to Addie's `agent_testing` capability set. - -Both wrap the same conformance graders that power `npx @adcp/client grade request-signing` and `npx @adcp/client diagnose-auth`, so Addie can run RFC 9421 signing grades and OAuth handshake diagnoses interactively in Slack and web threads instead of telling users to install the CLI locally. - -`diagnose_agent_auth` calls `runAuthDiagnosis` from `@adcp/client/auth` in-process. `grade_agent_signing` shells out to the published `@adcp/client` CLI's `grade request-signing --json` because `gradeRequestSigning` isn't yet on the package's public export surface — follow-up tracks promoting it so the tool can move in-process. - -Live-side-effect vectors (real `create_media_buy`, replay-cap flood) are skipped by default. Callers must pass `allow_live_side_effects: true` to run them and should only do so against sandbox endpoints. - -Closes #3371. diff --git a/.changeset/addie-length-postprocessor.md b/.changeset/addie-length-postprocessor.md deleted file mode 100644 index b4fa023de3..0000000000 --- a/.changeset/addie-length-postprocessor.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Adds a deterministic length post-processor that fires on responses to short user questions. Sonnet's instinct toward thoroughness produces 250–350 word answers to 7–12 word challenges (priv-1, acct-1, acct-2, gap-1 in the redteam suite) even with response-style.md teaching it to match the conversational register. The teaching reduces mean length but doesn't enforce a ceiling — this is the floor. Fires when the user's question is ≤15 words AND the assistant response is >160 words; truncates at the nearest sentence boundary at or before 130 words and appends *"Happy to go deeper on any of this if useful."* Code blocks are kept whole, never cut mid-fence. Same shape as the existing `stripBannedRituals` post-processor — applied at the same four return sites in `claude-client.ts`. Idempotent. diff --git a/.changeset/addie-perspectives-and-event-prep.md b/.changeset/addie-perspectives-and-event-prep.md deleted file mode 100644 index f3342fc508..0000000000 --- a/.changeset/addie-perspectives-and-event-prep.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Two new Stage 2 prompt rules powered by community-engagement context. - -**`event.upcoming_registered`** (priority 89, dynamic label/prompt + matchClick) — fires for members whose next registered event starts within 14 days. Renders "Prep for Cannes Lions" / "Cannes Lions is coming up. What do I need to know?" for an event titled "Cannes Lions"; falls back to a generic "Prep for your event" when title resolution fails. - -**`perspectives.share_first_one`** (priority 55) — fires for active members (≥1 login in 30d) who have never published a perspective. Renders "Share what I'm building." - -New MemberContext blocks: `perspectives` (`{ published_count, last_published_at }`) and `next_event` (`{ title, slug, starts_at }`). Both hydrated in Slack and web flows. Single-row queries, no new schema. 89 unit tests total (was 80; added 9 covering both rules + matchClick for the dynamic event prompt). diff --git a/.changeset/addie-prompt-decay.md b/.changeset/addie-prompt-decay.md deleted file mode 100644 index 307a110bda..0000000000 --- a/.changeset/addie-prompt-decay.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Implements #3282: decay/suppression for Addie's suggested-prompts rules engine. Without this, every activation rule fires indefinitely until the gating signal flips — owners who deliberately ignore the "List my company in the directory" prompt see it forever. - -- New migration `436_addie_prompt_telemetry.sql` creates `addie_prompt_telemetry (workos_user_id, rule_id, shown_count, last_shown_at, suppressed_until)` keyed on `(workos_user_id, rule_id)`. -- New DB layer `server/src/db/addie-prompt-telemetry-db.ts`: `getTelemetryForUser` reads into a Map; `recordPromptsShown` does one bulk upsert via `unnest($2::text[])` for all rules in the batch. -- **Counting is bucketed by UTC day**: shown_count only increments if `last_shown_at < CURRENT_DATE`. Without this a Slack user who opens App Home and starts a few Assistant threads in one workday would burn through the suppression threshold without ever consciously reading the prompt. -- Default thresholds: 5 distinct days of shows → 30-day suppression. Tunable via `recordPromptsShown` options. -- **Persona prompts are exempt** (`decay: false` on `PromptRule`). Personas are stable entry points, not nudges — suppressing "Prove the outcomes" for a `data_decoder` would leave them with strictly worse fallbacks. Their rule IDs are excluded from `recordPromptsShown` so no telemetry is written. -- `MemberContext.prompt_telemetry` hydrated in both Slack and web flows; benefits from the existing 30-min context cache so the evaluator stays synchronous. Cache means in-memory `suppressed_until` can be up to 30 min stale, which is fine given suppression is 30 days. -- New `pickPrompts()` returns parallel `{prompts, ruleIds}` for telemetry recording; existing `buildSuggestedPrompts()` kept as a thin wrapper. All 4 call sites (Slack Assistant, App Home, Web Home, legacy wrappers) record fire-and-forget after picking. -- 8 new tests covering suppression behaviour, the `pickPrompts` API, and persona-exempt logic. - -Out of scope: dismissal UI (no Slack/web button surface yet), per-rule decay configurations, "acted on" tracking (when the gating signal flips, the rule's `when()` returns false naturally). diff --git a/.changeset/addie-prompt-metrics.md b/.changeset/addie-prompt-metrics.md deleted file mode 100644 index fc3d4e51d8..0000000000 --- a/.changeset/addie-prompt-metrics.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Suggested-prompts usage metrics: heuristic click tracking + admin dashboard. Adds `clicked_count` and `last_clicked_at` to `addie_prompt_telemetry` (migration 442). Detects clicks by exact-string match between incoming user messages and the rule registry's prompt strings, recorded fire-and-forget at message receipt sites (Slack assistant thread, Slack handler, web chat stream + non-stream endpoints). New admin endpoint `/api/admin/prompt-metrics` and dashboard at `/admin/prompt-metrics` showing per-rule shown/clicked/CTR/suppression with sortable columns; dormant rules (never shown) surfaced in red. Click on a rule clears `suppressed_until` so a user re-engaging gets normal evaluation again. diff --git a/.changeset/addie-quality-anon-sonnet-stripper.md b/.changeset/addie-quality-anon-sonnet-stripper.md deleted file mode 100644 index b2fb3725c8..0000000000 --- a/.changeset/addie-quality-anon-sonnet-stripper.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Closes most of the Addie redteam baseline failures with two changes: (1) anonymous chat now defaults to Sonnet rather than Haiku — the daily-cap and per-IP rate limit already bound total spend, and Sonnet substantially better follows the response-style.md negative instructions ("don't say 'great question'") and avoids the fabrication patterns we saw in flagged threads; override via `ADDIE_ANONYMOUS_MODEL` if cost forces a downgrade. (2) A deterministic post-processor strips banned ritual phrases ("the honest answer is", "great question", "to be clear,", etc.) from assistant text before it reaches the user, so even Haiku-mode anonymous responses (and any future regressions) can't leak the phrases. Strip is applied outside fenced code blocks, re-capitalizes following sentences, and is idempotent. diff --git a/.changeset/addie-self-knowledge-pages.md b/.changeset/addie-self-knowledge-pages.md deleted file mode 100644 index 68f3165a6f..0000000000 --- a/.changeset/addie-self-knowledge-pages.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Addie now has self-knowledge documentation. Adds three audience-specific pages under `docs/aao/` (members, org-admins, AAO-admins) plus an auto-generated tool reference (`addie-tools.mdx`) listing all 224 of Addie's tools grouped by capability set. search_docs indexes these automatically, so Addie can answer "what can you do?" or "how do I do X on AAO?" by reading her own reference instead of fabricating. Generator is `scripts/build-addie-tool-reference.ts`; `npm run test:addie-tools` is a parity check that fails CI if the page is stale. diff --git a/.changeset/addie-speaker-followups.md b/.changeset/addie-speaker-followups.md deleted file mode 100644 index 8bbdaf945d..0000000000 --- a/.changeset/addie-speaker-followups.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Follow-ups to #3267: extend per-message speaker tracking to the surfaces -that were left out for scope, and harmonize thread-level display labels -with the new resolver. - -- **`email-conversation-handler.ts`**: stamp `user_id` (sender email) and - `user_display_name` (sanitized From-name) on inbound user messages. - Pass `currentSpeakerName` through. Conversation history reads the - stored display name. Forwarded chains and reply-alls now distinguish - speakers in the prompt the same way Slack channel threads do. -- **`tavus.ts`**: stamp speaker on the user-role message and pass - `currentSpeakerName` through `processMessageStream`. -- **`bolt-app.ts`**: switch the 5 `getOrCreateThread` call sites from - `mc?.slack_user?.display_name` to `resolveSpeakerDisplayName(mc)` so - the thread-level label and per-message labels for the same user - match. Resolves the cosmetic inconsistency flagged in the original - code review. - -The synthetic `addie-admin.ts` test-router endpoint is intentionally -left as-is — it writes to a `test-user` thread for router simulation, -not a real conversation. diff --git a/.changeset/addie-storyboard-hint-fix-plan.md b/.changeset/addie-storyboard-hint-fix-plan.md deleted file mode 100644 index 1679992498..0000000000 --- a/.changeset/addie-storyboard-hint-fix-plan.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Addie: render storyboard `context_value_rejected` hints as a Diagnose / Locate / Fix / Verify build playbook instead of a single passive "Hint:" line. The new formatter consumes the runner's structured hint fields (`source_step_id`, `source_task`, `response_path`, `request_field`, `accepted_values`) and emits a deterministic plan that names the two tools that disagree, offers widen-vs-narrow fix paths, and cites the exact `run_storyboard_step` call to verify the fix. Wired into both `run_storyboard` and `run_storyboard_step` MCP tool outputs. - -Bumps `@adcp/client` to `5.17.0` to pick up the runner-side hint emission. diff --git a/.changeset/addie-strip-end-turn-textblocks.md b/.changeset/addie-strip-end-turn-textblocks.md deleted file mode 100644 index f201247fcb..0000000000 --- a/.changeset/addie-strip-end-turn-textblocks.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fixes a missed code path in the banned-ritual stripper. Prod redteam against the just-deployed Sonnet+stripper combo showed 3 ritual-phrase leaks ("the honest answer is", "that's a fair question", "here's the honest answer") despite the stripper being in place. Tracing showed the strip was applied at three response-emission sites in `claude-client.ts` but missed a fourth: the `end_turn` path at line 763 that handles multi-text-block responses (used by web-search-returning answers). That path returned `text` unstripped to the caller. Now consistent with the other three return points — collect rawText, run through `stripBannedRituals`, return the cleaned form. No semantic change to non-end_turn paths. diff --git a/.changeset/addie-teach-length-and-privacy.md b/.changeset/addie-teach-length-and-privacy.md deleted file mode 100644 index c9f629b4f7..0000000000 --- a/.changeset/addie-teach-length-and-privacy.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Teaches Addie to handle two redteam failure modes that stuck after the Sonnet bump in PR #3273: length blow-out on short questions (Sonnet's instinct toward thoroughness produces 250–350-word essays for 7–12-word challenges) and "cryptographic guarantee" overclaim on TMP privacy questions. Both are addressed by strengthening the positive frame in the rules (response-style.md and knowledge.md) rather than adding more to the banned-phrase stripper. The length rule now explains *why* verbosity reads as defensiveness, adds a self-check ("count the user's words; calibrate yours"), and gives concrete word-count targets per question shape with a contrasting anti-example. The TMP privacy rule names the precise terms to use ("architectural separation," "data minimization," "two-endpoint design") and explicitly enumerates the cryptographic overclaims to avoid alongside *why* — TMP doesn't ship cryptographic primitives today. diff --git a/.changeset/addie-thread-speaker-identity.md b/.changeset/addie-thread-speaker-identity.md deleted file mode 100644 index 05a8fab949..0000000000 --- a/.changeset/addie-thread-speaker-identity.md +++ /dev/null @@ -1,40 +0,0 @@ ---- ---- - -Fix Addie misidentifying the speaker in multi-human Slack channel threads. - -Previously, when an admin replied mid-thread to a non-member's question, -Addie addressed the response to the original speaker and skipped tools the -admin had access to (e.g. `create_github_issue` via WorkOS Pipes). The DB -had no per-message speaker, and the prompt builder collapsed every -non-Addie turn into anonymous `role='user'` text — so the LLM lost track -of speaker switches mid-thread. - -Changes: - -- **Migration `435_thread_message_speaker.sql`**: nullable `user_id` and - `user_display_name` columns on `addie_thread_messages`. Old rows degrade - gracefully via the `'User'` sentinel. -- **`prompts.ts`**: new `BuildMessageTurnsOptions.currentSpeakerName`. When - the thread has multiple distinct named humans, every user-role turn - (history + current) is prefixed `[Name] ...`. Single-speaker threads keep - prior behavior. New `sanitizeSpeakerName` helper strips brackets, - newlines, and control chars and caps length so user-controlled display - names cannot break out of the prompt envelope. -- **`bolt-app.ts`**: speaker stamped on all 6 user-role write sites; 3 - `conversationHistory` builders surface the stored display name; 6 - `processOptions` carry `currentSpeakerName`. Mention handler appends - "the message you are responding to is from **{name}**" to the - system-prompt thread context block. -- **`addie-chat.ts`**: speaker stamped on both web user-message write sites - and `currentSpeakerName` plumbed through. `user_name` from the request - body is now ignored on anonymous requests so unauthenticated callers - cannot assert identity into the LLM context. -- **`claude-client.ts`**: `currentSpeakerName` plumbed through both - `processMessage` and `processMessageStream`. - -Repro and regression coverage at `server/scripts/repro-addie-thread-speaker.ts` -and `server/tests/unit/build-message-turns-speakers.test.ts`. Email, -admin-chat, and Tavus single-speaker write sites are not updated — they -do not trigger the bug class. The reaction handler intentionally does not -prefix synthetic `[User reacted ...]` turns. diff --git a/.changeset/addie-tool-catalog-and-honest-search.md b/.changeset/addie-tool-catalog-and-honest-search.md deleted file mode 100644 index aab26d45f9..0000000000 --- a/.changeset/addie-tool-catalog-and-honest-search.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Append an auto-generated authoritative tool catalog to Addie's system prompt and add a behavior rule against the "tools aren't loaded in this conversation" framing. - -`scripts/build-addie-tool-reference.ts` now emits both `docs/aao/addie-tools.mdx` (public reference) and `server/src/addie/generated/tool-catalog.generated.ts` (compact catalog injected into Addie's prompt) from the same source — `server/src/addie/mcp/*-tools.ts` plus `tool-sets.ts`. The runtime catalog cannot drift from the public docs page because both are written together. - -Pairs with a new rule in `server/src/addie/rules/behaviors.md` that requires Addie to report what she searched and what came back, rather than claiming a tool isn't loaded — the catalog is always in her prompt, so "not loaded" is never the honest framing. diff --git a/.changeset/addie-upgrade-and-fit-questions.md b/.changeset/addie-upgrade-and-fit-questions.md deleted file mode 100644 index 312d128993..0000000000 --- a/.changeset/addie-upgrade-and-fit-questions.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Addie behavior updates driven by escalation #281 (Vladimir Houba), -expanded after expert review. - -- **knowledge.md** — New FAQ row for upgrade pricing. Every tier is - a Stripe subscription (credit card or invoice); Stripe prorates - upgrades automatically regardless of collection method, so the - user typically pays only the prorated difference, not the full new - tier on top. Worked examples for Explorer → Professional and - Builder → Partner. Invoice-billed subscriptions see the prorated - charge on their next invoice instead of an immediate card charge — - same numbers, different timing. -- **constraints.md** — "Do NOT escalate" list under Escalation - Protocol calling out community-fit questions and routine - credit-card pricing. Replaces the bundled-question one-liner with - a decomposition procedure (split, decide per part, default - answer-all-parts) and a worked example using Vladimir's actual - question. -- **behaviors.md** — "Individual Practitioner Suitability" now has a - peer-register sub-clause: skip the reassurance script ("Basics is - free", "no coding") for senior practitioners (10+ years RTB / DSP / - ad-ops), and instead name the working group(s) where their depth - is load-bearing. Adds a sequencing rule for fit + pricing bundled - questions: affirm fit → name path → reassure friction-free upgrade, - in that order. -- **escalation-tools.ts** — Tightens the `escalate_to_admin` tool - description so it stops winning negative-rule contests. Softens - "too complex or sensitive for you to handle" to "requires admin - judgment, account access, or a human action you cannot perform" - and adds explicit DO-NOT-USE-FOR entries for community-fit, - routine pricing, and decomposable multi-part questions. diff --git a/.changeset/admin-addie-cost-observability.md b/.changeset/admin-addie-cost-observability.md deleted file mode 100644 index d7a06b22d0..0000000000 --- a/.changeset/admin-addie-cost-observability.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -Add admin observability for the per-user Addie Anthropic cost cap (#2945, -follow-up to #2790 / #2946 / #2950). - -New admin page at `/admin/addie-costs` and three read-only endpoints: - -- `GET /api/admin/addie-costs/summary` — workspace 24h/7d totals and a - per-namespace breakdown (email / slack / mcp / tavus / anon / workos / - unknown) so operators can see which caller category dominates spend. -- `GET /api/admin/addie-costs/leaderboard?window=24h|7d&limit=N` — top - scope keys by spend with inferred tier, % of cap, event count, and - model mix. Bare WorkOS scope keys join back to `users` + - `organization_memberships` + `organizations` so paying members show - `member_paid` tier; email hash / mcp sub / tavus IP scopes stay - opaque by design. -- `GET /api/admin/addie-costs/scope/:scopeKey/events` — drill-in with - the 200 most-recent events for one scope (timestamp, model, token - volume, cost). - -Tier inference on the leaderboard is defensive: `member_paid` is only -claimed when a bare WorkOS id joins to an active subscription; -everywhere else the displayed cap falls back to the namespace-level -inference (anonymous for email/mcp/tavus/anon, member_free for -slack/workos) so an email-hash scope can't be mis-displayed as a -paying-member ceiling. diff --git a/.changeset/admin-announcements-backlog.md b/.changeset/admin-announcements-backlog.md deleted file mode 100644 index 3379754102..0000000000 --- a/.changeset/admin-announcements-backlog.md +++ /dev/null @@ -1,37 +0,0 @@ ---- ---- - -**Admin `/admin/announcements` backlog page** - -Single surface for the editorial team to see what's waiting. Filter tabs -for **Pending review**, **LinkedIn pending**, **Done**, **Skipped**, and -**All**, with count badges on each tab. Per-row link to the account -detail page (Mark posted to LinkedIn lives there). Drafts older than -7 days in a non-terminal state are visually flagged as "stuck". - -**Why.** Now that backfill can land 10-15 retroactive drafts in one run -(PR #2990), editorial needs a dashboard view beyond scrolling the Slack -channel. Flagged as the top follow-up during Stage 2/3 expert review. - -**New:** - -- `GET /api/admin/announcements` — returns per-org backlog rows with - derived `state` bucket + per-state counts. -- `loadAnnouncementBacklog()` in `announcement-handlers.ts` — one-query - join with `DISTINCT ON (organization_id)` for each state CTE so every - org collapses to a single row regardless of duplicate activity - history. -- `server/src/routes/admin/announcements.ts` — wires the page + API. -- `server/public/admin-announcements.html` — page with filter tabs, - table view, BACKFILL badge on retroactive rows, stuck-days warning. -- Sidebar link in `admin-sidebar.js` under Community → Announcements. - -**Not in this PR:** stale-LI alerting on the trigger job cadence is -the natural next step but hasn't shipped yet. - -**Tests:** 4 query-shape tests in `announcement-backlog.test.ts` cover -row-mapping, legacy-row `is_backfill` coercion, SQL-uses-DISTINCT-ON, -and empty-result. 6 route tests in `announcement-backlog-route.test.ts` -cover state-bucket derivation (all four buckets), skipped-precedence -invariant, ISO date strings, backend-error 500, empty happy path. Full -announcement suite 154/154 pass. diff --git a/.changeset/admin-bypass-storyboard-eval-rate-limit.md b/.changeset/admin-bypass-storyboard-eval-rate-limit.md deleted file mode 100644 index 0fbd7a5391..0000000000 --- a/.changeset/admin-bypass-storyboard-eval-rate-limit.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -AAO platform admins now bypass the storyboard evaluation and step rate limits (previously capped at 5 full evals / 30 steps per hour), so admins can debug and curate agent storyboards without being throttled. Non-admin users are unaffected. diff --git a/.changeset/admin-dedup-history.md b/.changeset/admin-dedup-history.md deleted file mode 100644 index ed874e9688..0000000000 --- a/.changeset/admin-dedup-history.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Persists webhook-side dedup decisions to `registry_audit_log` (action: `subscription_dedup`) and adds a "Dedup Events" button + modal on the admin org detail page so admins can retroactively see which subscriptions the helper canceled, when, and why. Closes the admin-UI sub-item of #3245. diff --git a/.changeset/admin-replace-subscription-endpoint.md b/.changeset/admin-replace-subscription-endpoint.md deleted file mode 100644 index f3d025f54d..0000000000 --- a/.changeset/admin-replace-subscription-endpoint.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Adds `POST /api/admin/accounts/:orgId/replace-subscription` for out-of-band tier changes on existing members (custom contracts). Uses Stripe's in-place `subscriptions.update` so `stripe_subscription_id` stays the same and the agreement audit trail is preserved. Body accepts `lookup_key` or `price_id`, optional `coupon_id`, and `proration_behavior` (default `none`). Records a `subscription_replaced` audit log row with before/after state. Closes #3180. diff --git a/.changeset/admin-reset-subscription-state.md b/.changeset/admin-reset-subscription-state.md deleted file mode 100644 index 19e24e3ddc..0000000000 --- a/.changeset/admin-reset-subscription-state.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `POST /api/admin/accounts/:orgId/reset-subscription-state` — admin-only endpoint that atomically clears all subscription fields to NULL for orgs whose Stripe state is gone but whose DB rows are stale. Includes live-subscription safety guard, org-name confirmation, required reason field, and before-state snapshot in registry_audit_log. diff --git a/.changeset/admin-thread-search-cross-identifier.md b/.changeset/admin-thread-search-cross-identifier.md deleted file mode 100644 index a9441f5987..0000000000 --- a/.changeset/admin-thread-search-cross-identifier.md +++ /dev/null @@ -1,20 +0,0 @@ ---- ---- - -Admin thread search now matches across every natural identifier — full -name, Slack handle, Slack real name, WorkOS first/last name, or any -known email address. `listThreads({ user_search })` adds two LEFT JOINs -(slack_user_mappings, users) only when the filter is set, then -OR-matches the term against thread display name, raw user_id (which -carries the email for email threads), Slack mapping fields, and WorkOS -profile fields. - -Without this, harmonizing `addie_threads.user_display_name` to -`Brian O'Kelley` (full WorkOS name) in #3271 orphaned admins who -typed a Slack handle like `bokelley` into the search box. The -underlying mapping was always there; the query just wasn't using it. - -Eight new integration tests in `tests/unit/thread-service.test.ts` -cover Slack handle, Slack real name, Slack-side email, WorkOS first -name, WorkOS email, email-thread sender lookup, harmonized display -name across surfaces, and the negative case. diff --git a/.changeset/admin-ui-channel-settings.md b/.changeset/admin-ui-channel-settings.md deleted file mode 100644 index cfb9da109b..0000000000 --- a/.changeset/admin-ui-channel-settings.md +++ /dev/null @@ -1,38 +0,0 @@ ---- ---- - -**Admin UI for editorial + announcement channels; Stage 1 env-var → DB migration** - -Closes two follow-ups flagged during Workflow B Stage 2/3 reviews. - -**Admin UI (`admin-settings.html`).** Two new sections on the System -Settings page: - -- *Editorial review channel* — private-channel picker. Stores into the - existing `editorial_slack_channel` setting. Workflow B Stage 1 review - cards land here. -- *Public announcement channel* — public-channel picker (pulls from the - `?visibility=public` variant of the picker endpoint). Stores into - the `announcement_slack_channel` setting added in Stage 2. - -Previously both settings were DB-backed but API-only — editorial team -had to `curl PUT /api/admin/settings/editorial-channel` to configure. - -**Stage 1 reader migration.** `runAnnouncementTriggerJob` and the -backfill script now call a new `resolveEditorialChannel()` that prefers -the DB setting and falls back to the legacy -`SLACK_EDITORIAL_REVIEW_CHANNEL` env var. Safe rollout: existing prod -config keeps working on deploy; once the admin UI is in prod an -operator can set the DB value and we can drop the env var in a later -PR. - -Also: transient DB read failures fall back to env rather than blocking -the job — the job cares about *any* way of getting a channel id, not -whichever storage won the coin flip this hour. - -**Tests.** `tests/announcement/announcement-channel-resolver.test.ts` -— 8 tests covering DB-populated wins over env, env-fallback-on-null, -both-null → null, env whitespace/empty handling, DB-throws → env -fallback, DB-throws + env-unset → null. - -Full announcement suite 143/143 pass; typecheck clean. diff --git a/.changeset/anthropic-cost-cap.md b/.changeset/anthropic-cost-cap.md deleted file mode 100644 index 2cef4322f8..0000000000 --- a/.changeset/anthropic-cost-cap.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Close #2790: per-user Anthropic API cost cap at the claude-client boundary. Tool-call frequency limits (#2784, #2789) bound our external API spend (Google Docs, Gemini, Slack) but didn't bound Anthropic spend — a compromised account could keep a session running under the tool-call cap while steadily driving Claude bills. - -- **New migration 424** — `addie_token_cost_events(scope_key, cost_usd_micros, model, tokens_input, tokens_output, recorded_at)` indexed for rolling-window sums. -- **New `claude-pricing.ts`** with integer-math cost calculation for Haiku / Sonnet / Opus 4.x. Unknown models fall back to Opus rates so undercounting is impossible. -- **New `claude-cost-tracker.ts`** mirroring the tool-rate-limiter DI seam: Postgres default in prod, in-memory for unit tests. Tiered daily budgets (`anonymous: $1`, `member_free: $5`, `member_paid: $25`). System-user allowlist matches the tool limiter. -- **`claude-client.ts` instrumented** at both `processMessage` and `processMessageStream`: check cap at entry (friendly error + early return when blocked); record cost on completion + max-iteration fallback. All terminal paths accounted for. -- **Callers wired:** web chat (anonymous + authenticated), Slack primary streaming path, Slack mention/DM handler. Other callers (email, tavus voice, MCP chat-tool) tracked in the follow-up issue. - -Conservative tier resolution: every authenticated caller is `member_free` for now. Real paying members get the $25/day ceiling once the subscription-status lookup is threaded through (filed as follow-up #2945 alongside the admin observability dashboard). - -35 new unit tests (pricing calc correctness + cap enforcement + tier differentiation + system exemption + accumulation across calls). 1971 server unit tests + 631 root unit tests pass. diff --git a/.changeset/audit-log-mode-on-entries.md b/.changeset/audit-log-mode-on-entries.md deleted file mode 100644 index 3a4d237ca8..0000000000 --- a/.changeset/audit-log-mode-on-entries.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Add optional `mode` field to `get_plan_audit_logs` audit entries, recording the governance mode (enforce/advisory/audit) active at check time. Surfaces the enforcement posture that produced each decision, closing a gap where audit and enforce modes produced identical-looking trails. diff --git a/.changeset/auto-approve-verified-owner-logos.md b/.changeset/auto-approve-verified-owner-logos.md deleted file mode 100644 index 01da53499b..0000000000 --- a/.changeset/auto-approve-verified-owner-logos.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Brand logos uploaded by a verified domain owner now auto-approve and rebuild the manifest immediately, instead of sitting in the pending review queue. Closes #3150 (the policy half — community uploads and the brand_logos default still queue, which is intentional). - -Concretely: when `POST /api/brands/:domain/logos` runs, we check `isVerifiedBrandOwner(user.id, domain)` (existing helper, now exported) and set `source: 'brand_owner', review_status: 'approved'` if true. Verified hosted brands skip the manifest rebuild because they manage logos via brand.json. The pending queue stays for genuinely community-contributed logos where ownership is unclear — that's the case the moderation queue is actually for. - -Resolves the upstream cause of the thehook.es escalation: Felipe's domain was verified, his uploads should never have queued. diff --git a/.changeset/auto-provision-digest.md b/.changeset/auto-provision-digest.md deleted file mode 100644 index b61aa6782c..0000000000 --- a/.changeset/auto-provision-digest.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -feat(notifications): daily digest of new auto-provisioned members for org admins - -The consent receipt for the `auto_provision_verified_domain` default. With auto-add on (which it is by default), org membership grows quietly when verified-domain emails sign in — owners had no signal that their seat list was changing. This adds a daily Slack digest that lists the new auto-joined members per org and links to the team page where the owner can review or flip the toggle off. - -## Mechanics - -- New per-org watermark `organizations.last_auto_provision_digest_sent_at`. Migration 437. -- New `findOrgsWithNewAutoProvisionedMembers()` and `listNewAutoProvisionedMembers(orgId, since)` queries find members where `provisioning_source = 'verified_domain'` and `created_at > watermark`. Skips personal workspaces and orgs with `auto_provision_verified_domain = false`. -- `server/src/scheduled/auto-provision-digest.ts` runs the check every 24 hours (5-minute startup delay so it doesn't fire during boot's noisy window). Uses the same Slack DM dispatch helper (`sendToOrgAdmins`) that the seat-request reminder job uses, so multi-admin orgs get a group DM and single-admin orgs get a direct DM. -- Watermark is only updated after a successful Slack delivery. If no admins are mapped to Slack, or delivery fails, the watermark stays put and the next run retries — eventually surfaces the news once an admin's Slack mapping shows up. - -## Tests - -- `server/tests/integration/membership-webhook.test.ts` — 31 → 37 tests. Six new cases cover: candidates filtered to verified-domain since watermark, opt-out flag honored, NULL watermark = beginning of time, members returned chronologically with non-verified-domain sources excluded, and `markAutoProvisionDigestSent` updating the timestamp. - -## Future - -- Email fallback for orgs without Slack mappings (defer — Slack is the primary admin surface today). -- Per-org cadence preference (defer — daily is the right default for a low-volume notification). -- "What's in this digest" preview accessible from the team page (defer — wire into the next admin UI cycle). diff --git a/.changeset/auto-provision-verified-domain.md b/.changeset/auto-provision-verified-domain.md deleted file mode 100644 index 2e45f9fb33..0000000000 --- a/.changeset/auto-provision-verified-domain.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -Auto-provision users into verified-domain orgs more aggressively, give org admins a self-service way to manage members by email, and let them opt out per org. - -Closes the gap surfaced by the Triton Digital escalation: an org owner tried to promote someone with a verified-domain email to admin, but the system 404'd because the user wasn't yet a member of the org in WorkOS. - -## Server changes - -- `autoLinkByVerifiedDomain` runs on every authenticated request. The helper short-circuits internally when the user already has a row in the candidate org's local membership cache, so the cost stays close to one indexed query. Always provisions as `member`; the existing race-safe `upsertOrganizationMembership` SQL handles auto-promotion to `owner` for ownerless orgs (atomically, against the live table — no cache-skew risk). -- New `user.created` webhook step provisions users with verified emails into their employer's verified-domain org proactively, instead of lazily on first API hit. -- New `auto_provision_verified_domain` column on `organizations` (default `true`) — org owners and admins can flip it via `PATCH /api/organizations/:orgId/settings` to require explicit invites only. -- New `POST /api/organizations/:orgId/members/by-email` endpoint walks the four-state machine for callers (invite / create membership / update role / no-op). Authz mirrors the existing patterns: org admin/owner OR AAO super-admin can add members; only org owner OR AAO super-admin can change an existing member's role; only owner or AAO super-admin can assign the owner role. -- The invite path always invites as `member` regardless of the requested role (matching the bearer-credential downgrade discipline used in `routes/invites.ts`); admins promote after acceptance via the same endpoint. - -## Migration - -`433_auto_provision_verified_domain.sql` adds the opt-out flag with default `TRUE`. diff --git a/.changeset/brand-claim-adopt-copy-fix.md b/.changeset/brand-claim-adopt-copy-fix.md deleted file mode 100644 index e073804024..0000000000 --- a/.changeset/brand-claim-adopt-copy-fix.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix brand-claim adoption copy: hide the adopt-prior checkbox unless the prior manifest actually exists, neutralize "inherit" framing that primed hostile-takeover users (e.g. brand reclaiming from a squatter) toward keeping assets they don't want. diff --git a/.changeset/brand-claim-challenge-self-service.md b/.changeset/brand-claim-challenge-self-service.md deleted file mode 100644 index e6e80cd02d..0000000000 --- a/.changeset/brand-claim-challenge-self-service.md +++ /dev/null @@ -1,20 +0,0 @@ ---- ---- - -Self-service brand-claim challenge for the verified-domain takeover path, built on the WorkOS Domain Verification API. - -A member who controls a domain can claim it (or transfer from a soft-claimed prior owner) without filing an escalation: - -1. `POST /api/me/member-profile/brand-claim/issue` with `{domain}` → calls `workos.organizationDomains.create(orgId, domain)` and returns the WorkOS-issued DNS TXT record (`verification_prefix.{domain} = verification_token`). -2. Member publishes the TXT record. -3. `POST /api/me/member-profile/brand-claim/verify` → calls `workos.organizationDomains.verify(domainId)`. On success WorkOS marks the domain Verified, fires the `organization_domain.verified` webhook, AND we mirror the state into the brand registry inline (idempotent with the webhook handler). - -Two important properties come for free from WorkOS: -- One verified domain per org enforcement — verified-vs-verified collisions are unreachable. If org A has acme.com Verified, the create call for org B returns a 422 and we surface it as 409 with an escalation hint. -- DNS-TXT proof — registrar-level evidence rather than web-server-level. - -Cross-org disputes from #3168 now mention this path in their 409 response so a member hitting "managed by another organization" knows there's a self-service route. - -The `organization_domain.verified` webhook handler also calls `applyVerifiedBrandClaim` so admins flipping state via the WorkOS dashboard get the brand registry sync for free. - -Closes the policy half of #3176. The "auto-transfer after N-day cooldown when the incumbent doesn't refresh their challenge" piece is moot now — WorkOS gates that at the create level. diff --git a/.changeset/brand-claim-chat-tool-and-ui.md b/.changeset/brand-claim-chat-tool-and-ui.md deleted file mode 100644 index 473df1b7aa..0000000000 --- a/.changeset/brand-claim-chat-tool-and-ui.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add member chat tools (`request_brand_domain_challenge`, `verify_brand_domain_challenge`) and member-profile UI surface for the WorkOS-backed DNS TXT brand-claim flow. Extracts the WorkOS domain-verification orchestration into a shared service used by both the chat tools and the existing HTTP routes. diff --git a/.changeset/brand-claim-cross-org-callout.md b/.changeset/brand-claim-cross-org-callout.md deleted file mode 100644 index 9149a2575d..0000000000 --- a/.changeset/brand-claim-cross-org-callout.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Surface the WorkOS DNS challenge inline when a member tries to save a brand identity for a domain that's already owned by another org (409 cross_org_ownership). Admins can now self-service the claim from the member-profile page without leaving for the chat or waiting on the escalation queue. The /brand-identity 409 response now includes a `code: 'cross_org_ownership'` field so the UI can branch. diff --git a/.changeset/brand-claim-status-tag.md b/.changeset/brand-claim-status-tag.md deleted file mode 100644 index 435b2f9ece..0000000000 --- a/.changeset/brand-claim-status-tag.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Prefix brand-claim chat tool responses with an HTML comment `` (invisible in rendered markdown, parseable by the LLM) so the model can branch on a stable signal instead of pattern-matching prose. Covers happy paths (dns_record_issued, already_verified, verified) and rejection paths (collision, invalid_domain, still_pending, no_challenge, workos_error, not_authenticated, no_org, not_admin, missing_domain). diff --git a/.changeset/brand-identity-tools-and-validation.md b/.changeset/brand-identity-tools-and-validation.md deleted file mode 100644 index ba1c23c422..0000000000 --- a/.changeset/brand-identity-tools-and-validation.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Brand identity hygiene + Addie tooling rework, triggered by two stuck-logo escalations (thehook.es, kyber1.com): - -**Member self-service** -- `update_company_logo` — new member tool so a logged-in user can update their own logo or brand color through Addie chat. Was previously admin-only via `update_member_logo`, which forced an escalation for every "fix my logo" request. - -**Admin tooling** -- `list_pending_brand_logos`, `list_brand_logos`, `review_brand_logo` — surface the registry approval queue and let aao-admin members approve/reject from any thread. `getPendingLogos` existed in the DB layer but had no caller, so uploads sat invisible until manually escalated. -- Wired `update_member_logo` and `update_member_profile` into the `admin` tool set — they were defined but unreachable from the router, the same shape of gap that hid the new logo-review tools. -- `canReviewBrandLogos` accepts the synthetic `admin_api_key` user so internal tooling can read pending logos via `GET /api/brands/:domain/logos`. - -**Validation hardening** -- `checkLogoUrlIsImage` HEAD-fetches saved logo URLs and rejects responses that aren't `image/*`. Catches Google Drive `/view` and Dropbox preview pages that silently return HTML and render as a broken image once stored. Wired into both the member-facing `PUT /api/me/member-profile/brand-identity` and admin/member Addie tools. -- `canonicalizeBrandDomain` strips `https://`, `www.`, `m.`, paths, queries, fragments, and lowercases. Applied on every brand-identity save so members no longer split-brain into separate `kyber1.com` / `www.kyber1.com` registry rows. - -**Resolver fixes** -- `resolveBrandFromJson` accepts the singular `logo: {url}` shape and the `brand_colors` alias used by some real-world brand.json publishers (e.g. `house_portfolio` variants). Previously these brands appeared logoless on member surfaces despite declaring a valid logo. `referrals.ts` and `si-chat.ts` switched to the shared resolver instead of inline `brands[0].logos[0]` extraction. - -**UI fallback** -- Logo `` elements on the dashboard sidebar and member-profile page now swap to the placeholder when the URL fails to load (broken host, expired link, viewer page). Already-stored bad URLs no longer leave the section visually empty. - -**Refactor** -- Extracted shared `updateBrandIdentity` service so the route handler, the new `update_company_logo` member tool, and the existing `update_member_logo` admin tool all run identical transaction logic, validation, and canonicalization. diff --git a/.changeset/brand-json-canonicalization-link.md b/.changeset/brand-json-canonicalization-link.md deleted file mode 100644 index 6029e0a56a..0000000000 --- a/.changeset/brand-json-canonicalization-link.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -**Apply the AdCP URL canonicalization rule to brand.json agent URLs.** - -Follow-up to #3067 — the canonicalization reference page now exists, -and `seller-agent-ref`, `adagents.json` `authorized_agents[].url`, -`format-id`, and `provider-registration` all link to it. `brand.json` -declares additional agent URLs that fall in the same identifier- -comparison class but weren't covered: - -- `brand_agent_entry.url` — the brand-declared agent endpoint (MCP or - A2A) used by callers resolving "is this the agent that signed this - artifact?" or matching against a discovery cache. -- `brand_agent.url` — the brand agent MCP endpoint reference. -- `rights_agent.url` — the rights agent MCP endpoint reference. - -All three now reference the AdCP URL canonicalization rules at -`docs/reference/url-canonicalization` so two URLs differing only in -case, default port, or percent-encoded unreserved characters compare -equal during agent resolution. - -`logo.url`, `data_subject_contestation.url`, asset-library `url`, and -the brand's primary `url` are *not* identifier-comparison keys (they -point at human-facing pages or asset CDN endpoints), so they were -left unchanged. - -`jwks_uri` (line 627) is a fetch target for JWKS download, not an -identifier-comparison key — receivers HTTP-GET the URL as declared -without comparing it to anything. Not in scope for this rule. - -No schema shape changes. Descriptions only. diff --git a/.changeset/brand-logo-pending-digest.md b/.changeset/brand-logo-pending-digest.md deleted file mode 100644 index e4bc5630b0..0000000000 --- a/.changeset/brand-logo-pending-digest.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -New scheduled job `brand-logo-digest` posts a daily summary to the configured admin Slack channel when brand logos have been sitting in moderation review for more than 12 hours. Uses the existing in-process scheduler (`server/src/addie/jobs/scheduler.ts`) and the same `getAdminChannel()` system setting other admin alerts use. - -The pending queue was previously invisible — `getPendingLogos` had zero callers before #3137, and admins won't poll `list_pending_brand_logos` on their own. The 12-hour threshold gives #3154's auto-approve path time to handle owner uploads without surfacing them in the digest. Items younger than 12h, or runs where the queue is empty, produce no Slack noise. - -Closes #3151. diff --git a/.changeset/brand-orphan-adoption-integration-tests.md b/.changeset/brand-orphan-adoption-integration-tests.md deleted file mode 100644 index 89dcc5e5a1..0000000000 --- a/.changeset/brand-orphan-adoption-integration-tests.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Integration coverage for the orphan-adoption flow shipped in #3168. Closes the test gap code-reviewer flagged on that PR. - -`server/tests/integration/brand-orphan-adoption.test.ts` exercises the end-to-end transaction against a real Postgres: - -- `deleteHostedBrand` sets `manifest_orphaned=true`, stashes `prior_owner_org_id`, clears ownership, marks `is_public=false`, and **preserves** the manifest. -- `getDiscoveredBrandByDomain` still returns the row so callers can branch on the orphan flag. -- `updateBrandIdentity` throws `BrandIdentityError` with `code='orphan_manifest_decision_required'` and prior-owner meta when `adoptPriorManifest` is undefined for an orphaned brand. -- `adoptPriorManifest=false` clears the prior manifest and writes a fresh one with the new logo only; orphan flag clears, ownership transfers, `is_public=true`. -- `adoptPriorManifest=true` keeps the prior manifest and merges the new logo over it; prior colors persist. -- Cross-org write to a non-orphaned brand still throws `cross_org_ownership` even with `adopt_prior_manifest: true` (sanity check that the orphan path doesn't bypass the boundary). - -`checkLogoUrlIsImage` is mocked at module-graph load so the test doesn't make outbound HEAD requests against `.example.com` URLs. diff --git a/.changeset/brand-ownership-disputes-and-delete-fix.md b/.changeset/brand-ownership-disputes-and-delete-fix.md deleted file mode 100644 index d46c99dfcd..0000000000 --- a/.changeset/brand-ownership-disputes-and-delete-fix.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Three follow-ons to #3152 / #3159's transfer-tool work: - -- **Cross-org write conflicts route through escalations.** When `updateBrandIdentity` rejects a write because the domain is owned by another org, both the web `PUT /api/me/member-profile/brand-identity` route and the `update_company_logo` member tool now file a `category: sensitive_topic` escalation. The framing is neutral — the dispute might be an acquisition, naming overlap, or backfill, not necessarily a squat — and admins resolve via the existing `transfer_brand_ownership` tool (#3159). Web returns 409 + escalation_id; chat returns a ticket-id message. If escalation creation itself fails, the user-facing message switches to a "please email support" line so we don't promise follow-up that won't happen. - -- **Relinquished manifests are flagged for adoption, not nuked.** `deleteHostedBrand` previously cleared ownership but kept the prior org's `brand_manifest`, leaking the visual identity to any new claimant. Migration `430_brand_orphan_manifest.sql` adds `manifest_orphaned` and `prior_owner_org_id` columns; relinquish now sets the orphan flag (paired with `is_public=false`) and stashes the prior owner. Public reads filter orphaned brands. The new claimant can pass `adopt_prior_manifest: true` to keep the prior identity (acquisition / handoff case) or omit it to start fresh (default — protects against silent identity inheritance). Either way the orphan flag clears at claim time. - -- **`BrandIdentityError` discriminator.** Adds a `code` + per-code `meta` (typed via `BrandIdentityErrorMetaByCode`) plus an `isCrossOrgOwnership()` type guard. Catch sites narrow on the guard so `err.meta.brandDomain` is typed as string instead of unknown — typos at compile time, not runtime. - -Closes most of #3152. Auto-resolve via verified domain ownership (the "publish brand.json with our pointer to take it over" path) is filed separately as #3176 — it needs a real DNS-TXT or file-placement challenge rather than the current pointer-only check, which isn't strong enough to gate a provenance change against a sitting incumbent. diff --git a/.changeset/brand-ownership-transfer-admin-tool.md b/.changeset/brand-ownership-transfer-admin-tool.md deleted file mode 100644 index b086fcd9ee..0000000000 --- a/.changeset/brand-ownership-transfer-admin-tool.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Add `transfer_brand_ownership` admin tool to Addie's admin panel. - -Admins can now transfer a brand domain from one organization to another via a single tool call — no more direct database surgery for acquisitions, org renames, or "original uploader left" cases. The operation writes a revision to the `brand_revisions` audit trail before updating `brands.workos_organization_id`. - -Two items from the issue that need more design are deferred: the soft-claim model (spoofing risk when `brand_manifest` is non-empty) and the `claim_disputed_brand` member tool (better modeled as a `dispute_type: brand_ownership` escalation using the existing escalation system). diff --git a/.changeset/bump-5-13-and-framework-envelope.md b/.changeset/bump-5-13-and-framework-envelope.md deleted file mode 100644 index 5a39b68a84..0000000000 --- a/.changeset/bump-5-13-and-framework-envelope.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Bump `@adcp/client` to `^5.13.0` and close the remaining storyboard -failures that weren't spec-side. - -- **5.13 brings** the `governance.denial_blocks_mutation` recovery-path - fix (adcp-client#813). Closes the last two shared failures: - `media_buy_seller/governance_denied_recovery` and - `media_buy_seller/measurement_terms_rejected`. -- **Storyboard CI overlay step** now resolves the cache dir dynamically - (5.13 moved it from `latest` to the AdCP version string). -- **Cross-session state fallback** (`server/src/training-agent/state.ts`): - `findSessionMatching` + per-entity wrappers let handlers whose tool - schemas strip `account` still reach state written by earlier steps - that kept account context. Wired into `handleCheckGovernance`, - `handleReportPlanOutcome`, and `handleAcquireRights` (governance - plan), `handleLogEvent` + `handleProvidePerformanceFeedback` (event - source / media buy). -- **Sandbox event-source permissiveness**: `handleLogEvent` now - auto-registers unknown `event_source_id` so storyboards that omit - `sync_event_sources` (e.g. `sales_social`) still grade ingestion. -- **Envelope emission**: framework `adapt()` wraps successful - responses with `wrapEnvelope({ replayed: false, context })` so - idempotency + context-echo fields land on the AdCP envelope per - spec. - -Storyboard lift (overlaid compliance cache): -legacy **44 → 52** clean, framework **38 → 51** clean. - -Single residual framework-only failure is -`idempotency/create_media_buy_initial` — the storyboard's -`field_value: replayed [false]` assertion still reads `undefined` -because `create_media_buy` responses go through the framework's -task-capable wrap before `adapt()` runs. Tracked as a separate -follow-up. diff --git a/.changeset/bump-adcp-client-5-15.md b/.changeset/bump-adcp-client-5-15.md deleted file mode 100644 index 7a77f6dfe6..0000000000 --- a/.changeset/bump-adcp-client-5-15.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Bump `@adcp/client` from `5.13.0` to `5.15.0` and align training-agent -seller catalog with spec test-kit fixtures. - -`@adcp/client` 5.15.0 ships the two regression fixes from 5.14.0 that -had blocked this pin: - -- Schema loader pre-registers non-tool fragments from every flat-tree - domain directory, unblocking `$ref` resolution for governance, brand, - property, collection, content-standards, account, and signals tools. -- `create_media_buy` enricher: fixture-authored `product_id` / - `pricing_option_id` / `bid_price` on `packages[0]` now win over - discovery-derived values. Sentinel literals `"test-product"` and - `"test-pricing"` still defer to discovery. - -Side-effects for our training agent: storyboards that hardcode a -real (non-sentinel) product or pricing id in `packages[0]` now require -the agent's catalog to actually contain that id. Three mismatches -surfaced, all fixed by catalog alignment: - -- `product-factory.ts`: aliased `outdoor_ctv_q2` (CTV publisher) and - `local_display_dynamic` (first publisher) with `cpm_standard` - pricing — pattern already established for `test-product`, - `sports_ctv_q2`. Closes `media_buy_seller/governance_conditions` - and `sales_catalog_driven`. -- `signal-providers.ts`: renamed `po_prism_ltv_flat` → - `po_prism_flat_monthly` and `po_prism_cart_cpm` → - `po_prism_abandoner_cpm` to match `test-kits/nova-motors.yaml`. - Closes `signal_owned/activate_on_platform` and `activate_on_agent`. - -Storyboard CI floors raised to the new clean baseline (legacy -378 → 380, framework 390 → 393). Storyboard counts stay 52/52. diff --git a/.changeset/bump-adcp-client-5-16.md b/.changeset/bump-adcp-client-5-16.md deleted file mode 100644 index 0c96ff17d1..0000000000 --- a/.changeset/bump-adcp-client-5-16.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -Bump `@adcp/client` from `5.15.0` to `5.16.0` and restore positive -coverage on the fresh-path `replayed` assertion. - -**5.16.0 brings** the two follow-ups our prior bump flagged: - -- **`field_value_or_absent` matcher** (adcp-client#873 → 5.16.0). - Passes when a field is absent OR present with a matching value; - fails only when present with a disallowed value. The envelope-spec - escape hatch we needed for `replayed` on fresh execution. -- **Context-rejection hints** (adcp-client#870 → 5.16.0). Runner - emits non-fatal `context_value_rejected` hints when a seller's - error response's `available:` list would have accepted a value - that traces back to a prior-step `$context.*` write. Collapses - the "SDK bug vs seller bug" triage surface. Pass/fail unchanged; - hints surface on `StoryboardStepResult.hints[]`. - -**Spec-side use of the new matcher.** `universal/idempotency.yaml`'s -`create_media_buy_initial` step regains a positive assertion on -`replayed`: "if reported, must be false." The previous PR dropped -that assertion entirely because `field_value` fired on spec-compliant -agents that omit the field. `field_value_or_absent` restores coverage -without penalizing omission. The replay step's `field_value: -allowed_values: [true]` is unchanged. - -No training-agent code changes required — the catalog and handler -paths 5.15 exercised continue to pass. Storyboard baselines stay -52/52 in both dispatch modes. diff --git a/.changeset/bump-adcp-client-5-18-0.md b/.changeset/bump-adcp-client-5-18-0.md deleted file mode 100644 index 08b33ecbb0..0000000000 --- a/.changeset/bump-adcp-client-5-18-0.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Bumps `@adcp/client` 5.17.0 → 5.18.0. The release ships our `get_media_buys` request-builder fix (adcp-client#987 closing #983), the broader placeholder-ID enricher audit (#991), schema-aware brand/account injection in the storyboard runner (#943), the new `a2a_context_continuity` validator for multi-step storyboards (#962), A2A wire-shape capture (#904), and triage-bot ergonomics that close the loop on adcp#3121 (#992, #993). - -Two breakages surfaced and fixed: - -- **Hint type widening.** `StoryboardStepHint` widened from `ContextValueRejectedHint` to a five-kind union (`context_value_rejected | shape_drift | missing_required_field | format_mismatch | monotonic_violation`). `renderAllHintFixPlans` in `server/src/addie/services/storyboard-fix-plan.ts` now accepts the broader type and filters to `context_value_rejected` for the existing render path. Richer rendering for the other four kinds is a follow-up — today they're silently dropped from the fix-plan section, but the runner's per-hint `message` field still surfaces them upstream. - -- **Strict request-schema validation in storyboard runner.** The `create_property_list` request schema declares `additionalProperties: false`; `pagination-integrity-property-lists.yaml` was passing a `list_type: "inclusion" | "exclusion"` field that isn't in the schema. 5.18.0's runner rejects unknown fields strictly. Removed the `list_type` field — it never affected agent behavior (no handler reads it). - -Multi-page upgrade for `get_media_buys_pagination_integrity` deferred — adcp-client's convention extractor populates `context.media_buy_id` from the first-page response, then the request-builder injects that ID and turns the second call into an ID-lookup. Filed adcp-client#998 with the diagnosis and fix options. Storyboard stays at the single-step pagination-envelope assertion until the SDK fix lands. diff --git a/.changeset/bump-adcp-client-5-21-1.md b/.changeset/bump-adcp-client-5-21-1.md deleted file mode 100644 index eba23f11e3..0000000000 --- a/.changeset/bump-adcp-client-5-21-1.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -chore: bump @adcp/client 5.21.0 → 5.21.1 - -Patch bump picks up the grader fix from -[adcontextprotocol/adcp-client#1026](https://github.com/adcontextprotocol/adcp-client/pull/1026) -— `adcp grade request-signing` against Cloudflare-fronted endpoints -(closing my-filed [#1025](https://github.com/adcontextprotocol/adcp-client/issues/1025)). - -Validated end-to-end: the grader now reaches `/api/training-agent/mcp-strict` -on prod and runs all 39 vectors. 30 pass, 3 fail (verifier-side conformance -gaps unrelated to this PR), 6 are mcp-mode skips. - -Runtime API surface unchanged. No code changes in this repo other than the -version pin. diff --git a/.changeset/bump-mintlify-restore-broken-links.md b/.changeset/bump-mintlify-restore-broken-links.md deleted file mode 100644 index cb1050b474..0000000000 --- a/.changeset/bump-mintlify-restore-broken-links.md +++ /dev/null @@ -1,43 +0,0 @@ ---- ---- - -**Bump Mintlify 4.2.521 → 4.2.525, restore broken-links CI to its -intended posture, fix three real broken doc links it had been masking.** - -Closes #2983. - -Mintlify 4.2.525 ships with a deps-graph that no longer mixes React 18 -and 19 across `@mintlify/*` subpackages. Local hook and CI both run -clean now — no `Invalid hook call` startup crash, and the MDX parser -no longer chokes on `.changeset/*.md` bodies — so the workarounds -landed earlier this week can come back out: - -- `package.json` — `mintlify ^4.2.521` → `^4.2.525`. Lockfile updated. -- `.github/workflows/broken-links.yml` — replaced the React-crash - tolerance grep filter and the `.changeset/` shuffle with a plain - `npx --no-install mintlify broken-links`. The grep was a real - hazard: it filtered out lines containing `react` (case-insensitive) - before searching for failure patterns, which masked any genuine - failure mentioning a React-related path. The first time Mintlify - ran cleanly it surfaced **three real broken doc links** that had - been hiding behind the workaround: - - `docs/reference/url-canonicalization.mdx` linked - `/schemas/adagents.json` and `/schemas/core/format-id.json` as - bare paths; Mintlify treats those as internal page routes (which - don't exist) rather than CDN passthroughs. Switched both to - absolute `https://adcontextprotocol.org/schemas/v3/...` URLs - matching the convention in `docs/intro.mdx` and other published - docs. - - `docs/trusted-match/specification.mdx` had the same bare - `/schemas/adagents.json` link in the Seller Agent Attribution - paragraph (landed in PR #2984). Same fix. -- `.husky/pre-push` — dropped `.changeset/` from the shuffle list. - The other shuffles (`dist/addie/rules`, `.addie-repos`, `.context`) - remain — those still hold non-MDX markdown that Mintlify shouldn't - scan. - -Verified locally: `npx mintlify broken-links` exits 0 with -"success no broken links found" against the cleaned repo. - -Not protocol-related — descriptions-only doc fixes plus tooling -config — so this changeset is empty (no package version bump). diff --git a/.changeset/bump-workos-9.1.1.md b/.changeset/bump-workos-9.1.1.md deleted file mode 100644 index 3cebc3014b..0000000000 --- a/.changeset/bump-workos-9.1.1.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Bump `@workos-inc/node` from 8.13.0 to 9.1.1. Adapt to v9 breaking changes: `workos.portal` → `workos.adminPortal`; `apiKeys.validateApiKey` → `apiKeys.createValidation`; `widgets.getToken` → `widgets.createToken`; `organizations.listOrganizationRoles` → `authorization.listOrganizationRoles` (now takes a string `organizationId`); `organizationDomains.create`/`verify` → `createOrganizationDomain`/`verifyOrganizationDomain`. diff --git a/.changeset/catalog-agent-auth-readers.md b/.changeset/catalog-agent-auth-readers.md deleted file mode 100644 index bf052d5ef7..0000000000 --- a/.changeset/catalog-agent-auth-readers.md +++ /dev/null @@ -1,41 +0,0 @@ ---- ---- - -Reader cutover for property-registry agent/authorization unification (PR 4b-readers -of #3177). Nine federated-index/property-db readers now UNION over the legacy -`agent_publisher_authorizations` / `agent_property_authorizations` / -`discovered_properties` graph and the catalog-side -`v_effective_agent_authorizations` view, with legacy winning on collision -during the dual-read window. - -Functions cut over: -- `getAgentsForDomain` -- `getDomainsForAgent` -- `bulkGetFirstAuthForAgents` -- `getAllAgentDomainPairs` -- `getPropertiesForAgent` -- `getPublisherDomainsForAgent` -- `findAgentsForPropertyIdentifier` -- `getAuthorizationSource` (drives `validateAgentForProduct`) -- `isPropertyAuthorizedForAgent` -- `PropertyDatabase.getAgentAuthorizationsForDomain` - -The `validateSelector*` and `getAuthorizedProperties*` helpers are derived -in-memory from the unioned property reads so the catalog/legacy union -shape is materialized in exactly one place per relation. - -Catalog `evidence` values are coerced to the legacy `source` vocabulary -('override' → 'adagents_json' as moderator-authoritative; 'community' → -'agent_claim' as lower trust) so the API contract for source field -remains 'adagents_json' | 'agent_claim' | 'none'. - -Override layer (suppress / add) flows through `v_effective_agent_authorizations` -unchanged: suppress hides matching base rows, add surfaces phantom rows -with publisher_domain set to the override's host_domain. - -Writers (`upsertAuthorization`, `upsertAgentPropertyAuthorization`) and -cleanup ops (`deleteExpired`, `clearAll`, `getStats`) remain legacy-only — -PR 5 will collapse those after the dual-write window closes. - -Refs #3177. Builds on #3244 (property-side reader cutover), #3274 (schema), -#3314 (writer extension), #3312 (change-feed authorization events). diff --git a/.changeset/catalog-agent-auth-schema.md b/.changeset/catalog-agent-auth-schema.md deleted file mode 100644 index 835a97d1ff..0000000000 --- a/.changeset/catalog-agent-auth-schema.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Add `catalog_agent_authorizations` schema, `seq_no` rotation trigger, -`v_effective_agent_authorizations` view, and one-time backfill from -`agent_property_authorizations` + `agent_publisher_authorizations`. -Schema-only — no readers or writers wired yet. - -Gates the writer + reader cutover (PR 4b) and the change-feed extension -(PR 4b-feed) for agent → publisher / agent → property authorizations, -following the design pinned in `specs/registry-authorization-model.md`. - -Refs #3177. diff --git a/.changeset/catalog-agent-auth-snapshots.md b/.changeset/catalog-agent-auth-snapshots.md deleted file mode 100644 index 8baee7243a..0000000000 --- a/.changeset/catalog-agent-auth-snapshots.md +++ /dev/null @@ -1,40 +0,0 @@ ---- ---- - -Agent-side sync endpoints for the property-registry agent/authorization -catalog (PR 4b-snapshots of #3177). Two new endpoints under -`/api/registry/authorizations` give verification consumers (DSPs, sales -houses, agencies) a way to pull authorization rows without scraping -publisher manifests. - -- **`GET /api/registry/authorizations?agent_url=`** — narrow - per-agent pull, the default for most adopters. Indexed via - `idx_caa_by_agent`; returns the rows where the requested agent appears - as `agent_url` (typically ≤ a few hundred rows). Pairs with - `/api/registry/feed?entity_type=authorization` via the `X-Sync-Cursor` - response header — consumers tail subsequent changes from the cursor - position. - -- **`GET /api/registry/authorizations/snapshot`** — bootstrap for - inline verifiers that maintain a local copy. Streams gzipped NDJSON - via a Postgres cursor in 10K-row batches so memory stays bounded as - the table grows toward long-run scale (~5M rows, ~150-300 MB on the - wire). `ETag` is the hash of the X-Sync-Cursor; clients can - `If-None-Match` to skip a re-pull when nothing has changed. - -Both endpoints accept `?include=raw|effective` (default `effective` — -applies the override layer via `v_effective_agent_authorizations`) and -`?evidence=` (default `adagents_json` only). `agent_claim` is -opt-in (`?evidence=adagents_json,agent_claim`) per spec line 391 to -prevent buy-side trust misuse. - -`X-Sync-Cursor` is the most recent authorization event_id from -`catalog_events` — read via `ORDER BY event_id DESC LIMIT 1` since -Postgres has no `MAX(uuid)`. When zero events exist the all-zero -UUIDv7 sentinel `00000000-0000-7000-8000-000000000000` is returned so -the consumer can hand it to the feed endpoint unchanged. - -Refs #3177. Spec: `specs/registry-authorization-model.md:374-401`. -Builds on #3244 (property-side readers), #3274 (catalog schema), #3314 -(writer extension), #3312 (change-feed authorization events), #3352 -(reader UNION cutover). diff --git a/.changeset/catalog-agent-auth-writer.md b/.changeset/catalog-agent-auth-writer.md deleted file mode 100644 index 8f75e3f04e..0000000000 --- a/.changeset/catalog-agent-auth-writer.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Writer extension for catalog_agent_authorizations (PR 4b of #3177). -`cacheAdagentsManifest` now projects each `authorized_agents[]` entry -into the catalog table after the property-side projection runs. -Coverage in v1: `property_ids`, `inline_properties`, lexically-anchored -`publisher_properties` (selection_type ∈ {`all`, `by_id`}), and -publisher-wide (no `authorization_type`). Cross-publisher -`publisher_properties` claims, `selection_type='by_tag'`, `property_tags`, -`signal_ids`, and `signal_tags` are deferred per spec — the legacy -`agent_publisher_authorizations` table continues to serve them via the -UNION reader during the dual-read window. - -Security guards: -- `agent_url` canonicalization (lowercase + strip trailing slash; - embedded wildcards rejected; `*` sentinel exact-match only). -- Cross-publisher refusal — a manifest at attacker.example claiming - `publisher_properties` for victim.example is logged and skipped. -- Each entry in its own savepoint so a malformed entry doesn't lose - the rest of the manifest. - -Reader cutover and snapshot endpoints are out of scope for this PR; -they ship in subsequent PRs. - -Refs #3177. Builds on #3274 (schema). Spec #3251. diff --git a/.changeset/channel-privacy-audit-job.md b/.changeset/channel-privacy-audit-job.md deleted file mode 100644 index 25a9dd4ab6..0000000000 --- a/.changeset/channel-privacy-audit-job.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Close #2849: daily audit backstop for admin-settings channel privacy. #2735 catches drift at send time — a channel that has flipped private → public stops receiving sensitive posts on the first send after the drift. Channels that sit idle between writes could go undetected. This job runs once a day, checks each configured channel (billing / escalation / admin / prospect / error / editorial) against Slack, and emits a structured `channel_privacy_drift_audit` warn/info log on any drift or unverifiable state. - -When drift is found, a summary is posted to the `admin_slack_channel` — unless that channel itself is the drifted one, in which case the summary is suppressed and the structured log is the only signal (log aggregation alerting should key on the event). Non-destructive by design: the audit does NOT auto-null the drifted setting. The send-time gate from #2735 already refuses to post sensitive content; this job is pure observability. - -Registered with the existing `jobScheduler`, 24-hour interval, 10-minute initial delay. Uses the `runChannelPrivacyAudit()` export as its runner. 9 unit-test scenarios cover the orchestration logic (unconfigured channels skipped, admin-channel self-drift suppresses the summary, throws collapse to 'unknown', non-destructive behavior pinned). diff --git a/.changeset/channel-privacy-fail-closed.md b/.changeset/channel-privacy-fail-closed.md deleted file mode 100644 index 4d2b7171cf..0000000000 --- a/.changeset/channel-privacy-fail-closed.md +++ /dev/null @@ -1,46 +0,0 @@ ---- ---- - -**fix(admin): fail-closed on write-time channel privacy check (#3003)** - -Closes the pre-existing security gap flagged during PR #3000 review. - -**Before:** all seven admin-settings PUT endpoints guarded their -`is_private` requirement with `if (channelInfo && !channelInfo.is_private)`. -When `getChannelInfo` returned `null` (bot not a member, Slack 5xx, -throttle, archived channel, wrong scope), the `channelInfo && ...` -short-circuit silently skipped the check and the write was accepted. - -- For the six private-required endpoints (billing, escalation, admin, - prospect, error, editorial), the downstream - `sendChannelMessage(..., requirePrivate: true)` gate at send time - covered most exposure. -- The announcement endpoint inverts the check (requires public) and - has no downstream gate, so a `null` return accepted a private - channel id that would silently never receive the public post. - -**After:** new `verifyChannelPrivacyForWrite(channelId, expected)` -helper in `slack/client.ts` returns a discriminated result: - -- `ok: true` — proceed -- `ok: false, reason: 'wrong_privacy'` — channel confirmed wrong kind; - pick another channel (response includes actual and expected) -- `ok: false, reason: 'cannot_verify'` — Slack can't describe this - channel; invite the bot and retry - -All seven endpoints now go through a `requireChannelPrivacy` wrapper in -`settings.ts` that surfaces distinct error messages for each branch so -the admin knows whether to pick another channel or retry after inviting -the bot. Local-dev behavior (no ADDIE_BOT_TOKEN) is unchanged — -`isSlackConfigured()` short-circuits. - -Closes #3003. - -**Tests:** five new `verifyChannelPrivacyForWrite` cases in -`slack-channel-privacy.test.ts` (ok both directions, wrong_privacy -both directions, cannot_verify both directions) plus eight -supertest route tests in the new -`admin-settings-privacy-gate.test.ts` covering the billing (private- -required) and announcement (public-required) endpoints across all -three outcomes + the Slack-not-configured short-circuit. Full -announcement + privacy suite 206/206 pass; server typecheck clean. diff --git a/.changeset/claude-triage-nudge.md b/.changeset/claude-triage-nudge.md deleted file mode 100644 index 82a6ff3400..0000000000 --- a/.changeset/claude-triage-nudge.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add a `/claude-triage` comment nudge: repo members (OWNER / MEMBER / COLLABORATOR) can comment with `/claude-triage` on any issue to fire the triage routine on demand, regardless of the issue's assignee / open-PR / recent-comment state (the nudge overrides the already-engaged check). Optional modifiers: `/claude-triage execute`, `/claude-triage clarify`, `/claude-triage defer`. diff --git a/.changeset/connect-addie-doc-and-rule.md b/.changeset/connect-addie-doc-and-rule.md deleted file mode 100644 index fad980e46d..0000000000 --- a/.changeset/connect-addie-doc-and-rule.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -docs(aao): add "Connect Addie to your AI client" page covering Claude Desktop, ChatGPT, and Claude Code (with the `mcp-remote` workaround for CC bug #10250 and the auth-mode pitfalls users keep hitting). Surface the page under a new "Using AAO" group in the Mintlify nav and pin a knowledge rule so Addie retrieves the doc instead of improvising install steps when asked how to connect a specific client. diff --git a/.changeset/consolidate-uuid-validation.md b/.changeset/consolidate-uuid-validation.md deleted file mode 100644 index 9f52ee537f..0000000000 --- a/.changeset/consolidate-uuid-validation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Consolidate ~24 inline UUID-validation regexes into a single `isUuid()` helper at `server/src/utils/uuid.ts`. Fixes two latent bugs where the `/i` flag was missing, causing valid uppercase UUIDs to be rejected: - -- `GET /logos/brands/:domain/:id` (brand logo serving) -- `GET /brand/logos/:id` (brand-logos route) diff --git a/.changeset/copy-all-dns-instructions.md b/.changeset/copy-all-dns-instructions.md deleted file mode 100644 index 11332c1405..0000000000 --- a/.changeset/copy-all-dns-instructions.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add a "Copy all (for IT)" button to the brand-claim DNS panel that emits a self-contained text block (domain, FQDN, type, value, TTL, propagation note, verification URL) the user can paste into a Jira/Linear ticket for their IT team. CMOs and CTOs typically don't publish DNS themselves; this gives them a hand-off block their IT team can act on without bouncing back for clarification. diff --git a/.changeset/cost-cap-complete-wiring.md b/.changeset/cost-cap-complete-wiring.md deleted file mode 100644 index 342ddf82ab..0000000000 --- a/.changeset/cost-cap-complete-wiring.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -Complete the per-user Anthropic cost-cap wiring (#2950, follow-up to #2946). - -**Security:** the email conversation handler was the highest-priority unwired -path — inbound From-header auth is spoofable, so a cooperative mail server -could previously hit Claude uncapped. Now bucketed by sha256-hashed From -address under the `anonymous` tier. - -**Wired callers:** -- `server/src/addie/email-conversation-handler.ts` — `email:${sha256(from).slice(0,16)}` scope, `anonymous` tier -- `server/src/routes/tavus.ts` — resolves `thread.user_id` → `member_free`; falls back to `uncapped: true` (Bearer-auth bounds the surface) -- `server/src/mcp/chat-tool.ts` — external partners via safe-tools-only → explicit `uncapped: true` -- `server/src/addie/bolt-app.ts` — 5 Slack sites (mention, DM, thread reply, proposed-channel, reaction) scoped by WorkOS id or `slack:${userId}` fallback, `member_free` tier - -**Fail-closed default:** `AddieClaudeClient` now logs -`event: 'cost_cap_unwired'` at `warn` level when `processMessage` / -`processMessageStream` is called with neither `costScope` nor `uncapped: true`. -Log aggregation can alert on this event so future unwired callers don't ship -silently. Tests pin both the warn-fires-on-missing case and the suppressed -case when `uncapped: true` is set. diff --git a/.changeset/cost-cap-slack-helper-and-cache.md b/.changeset/cost-cap-slack-helper-and-cache.md deleted file mode 100644 index fc4b6e4099..0000000000 --- a/.changeset/cost-cap-slack-helper-and-cache.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Cost-cap polish bundle — closes deferred items from the #2945 / #2969 -reviews. - -**`buildSlackCostScope(memberContext, slackUserId)` helper.** The -8 Slack-originated call sites (6 in `bolt-app.ts`, 2 in `handler.ts`) -each had a 2-line prelude constructing the scope key and probing tier: - -```ts -const costScopeUserId = memberContext?.workos_user?.workos_user_id ?? `slack:${userId}`; -const costScopeTier = await resolveUserTierFromDb(costScopeUserId); -costScope: { userId: costScopeUserId, tier: costScopeTier }, -``` - -Collapsed to one line at each site: - -```ts -costScope: await buildSlackCostScope(memberContext, userId), -``` - -Keeps the `slack:` fallback shape in one place so a future namespace -rename only touches the helper. - -**60s memo cache on `resolveUserTierFromDb`.** Subscription status -changes on the order of days (Stripe webhooks → organizations -update), so a per-process 60s stale window cuts the DB-probe hot path -to ~1 probe per user per minute rather than 1 per Addie turn. Error -paths are NOT cached — a transient DB failure shouldn't lock a paying -member out of `member_paid` for a full TTL. Tests pin: -- Burst calls hit DB once, return cached tier thereafter. -- Error paths are not cached; next call retries. -- Cache is per-user, not global. - -No behavior change for end users; pure dev-debt cleanup + perf. diff --git a/.changeset/cost-cap-tier-upgrade.md b/.changeset/cost-cap-tier-upgrade.md deleted file mode 100644 index 299b930140..0000000000 --- a/.changeset/cost-cap-tier-upgrade.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Resolve real tier for authenticated callers of the Addie cost cap -(#2945 follow-up; closes the deferred tier-upgrade path from -#2790 / #2946 / #2950). - -Paying members were silently capped at `member_free` ($5/day) rather -than their `member_paid` ceiling ($25/day) because every caller site -hardcoded `tier: 'member_free'`. This lands a new async helper -`resolveUserTierForScopeKey(userId)` in `claude-cost-tracker.ts` and -wires it into every authenticated caller site (10 total across -`bolt-app.ts`, `handler.ts`, `addie-chat.ts`, `tavus.ts`). - -**Resolution rule:** -- Bare WorkOS id (`user_…`): DB probe against - `organization_memberships` + `organizations`. Active, non-canceled - subscription → `member_paid`. Otherwise → `member_free`. -- Non-WorkOS scope keys (`slack:…`, `email:…`, `mcp:…`, `tavus:ip:…`, - `anon:…`): no lookup, stays `member_free`. -- DB error: fail-closed to `member_free` so a transient outage can't - accidentally grant the $25/day ceiling to unverified callers. - -Tests cover the four cases (non-WorkOS bypass, active-sub promotion, -no-sub fallback, DB-error fail-closed) via the `db/client.js` mock -seam. diff --git a/.changeset/dashboard-429-polish.md b/.changeset/dashboard-429-polish.md deleted file mode 100644 index ccc111a7f2..0000000000 --- a/.changeset/dashboard-429-polish.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -Close #2937, #2938, #2939 — the three retrospective follow-ups from PR #2933 (dashboard 429 UX). - -**#2937 (P1) — coherent + accessible failure state.** -- The post-auto-retry "you've refreshed too quickly" copy is now driven by a real countdown off the second response's `Retry-After`, falling back to 60s when absent. Button stays disabled for the full window instead of re-enabling while the copy says to wait. -- Countdown container gets `role="status"`, `aria-live="polite"`, `aria-atomic="true"`. Text updates are throttled to every 10s + the final 5s + the terminal transition so screen readers aren't shouted at every tick. - -**#2938 (P2) — no stale session state.** -- `autoRetriedAgents` now clears when a retry succeeds. A 9am 429 no longer taints a 5pm retry. -- New `retryInFlight` Set prevents a click-plus-countdown race from firing `retryAgentCard` twice. - -**#2939 (P3) — polish.** -- `visibilitychange → visible` forces a countdown tick so background-throttled tabs don't show stuck values. -- Initial page render happens BEFORE the agent fetches. Cards appear in list order immediately in their "not yet checked" skeleton state, then hydrate in place as each worker resolves. Fixes the scrambled loading order and the "dead blank page" feel on large agent lists. -- A shared `cancellation` flag lets workers stop hammering the network when the 15s timeout fires. -- "Retrying now…" beat before auto-retry fires so the transition reads. -- Middleware: extracted `parseRetryAfterSeconds` helper that rejects 0, negatives, and non-finite values (replaces `|| undefined` which swallowed 0). Added comment noting the HTTP-date form is technically legal but express-rate-limit only emits delta-seconds. -- New `rate-limit-retry-after.test.ts` — 5 tests on the parse helper + a supertest that verifies the 429 body surfaces `retryAfter` when the header is set. - -No production behavior changes beyond what's called out above. 1946 server + 631 root unit tests pass. diff --git a/.changeset/dashboard-429-ux.md b/.changeset/dashboard-429-ux.md deleted file mode 100644 index 6c0d8c5e4e..0000000000 --- a/.changeset/dashboard-429-ux.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Close #2804: the Agents dashboard's 429 UX was vague and self-DOS-prone. Three fixes: - -- **Concurrency cap** in `loadAgents` — replaces `Promise.allSettled(agents.map(...))` with a 6-worker pool, so a member with a large saved-agent list can't self-429 on page load. Typical users (≤10 agents) still feel instant. Workers drain a shared queue; any results that arrived before the 15s timeout are harvested into the compliance map. -- **Live countdown + single auto-retry** on rate-limited cards. Instead of "Retry in a moment" (vague, no recovery path), the card shows "Rate-limited — retry in 28s…" with a per-second countdown, disables the Retry button until zero, then auto-retries once. A second 429 on the same agent freezes the card with "You've refreshed too quickly — wait a minute before trying again." No infinite-retry loops. -- **Proxy-stripped fallback.** `agentReadRateLimiter` now emits `retryAfter` (seconds) in the 429 JSON body alongside the standard `Retry-After` header, and the client reads the body as a fallback. Reverse proxies that drop non-standard headers no longer hide the retry hint. - -No test changes — the dashboard client lives in `server/public/*.html` and the repo has no frontend unit-test harness; server-side change is a small addition to an existing 429 handler. diff --git a/.changeset/dashboard-agents-visibility-tri-state.md b/.changeset/dashboard-agents-visibility-tri-state.md deleted file mode 100644 index 5a7294d1a4..0000000000 --- a/.changeset/dashboard-agents-visibility-tri-state.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add the three-tier agent visibility selector (Private / Members only / Public) to each card on `/dashboard/agents`. The radios call `PATCH /api/me/member-profile/agents/:index/visibility`; the "Public" option is gated on Professional+ tier and a configured brand domain, with inline upsell/nudge messages when either is missing. Styling reuses the shared `.agent-card-visibility` / `.agent-visibility-option` classes from `member-card.js`. diff --git a/.changeset/dedup-cancel-unpaid-policy.md b/.changeset/dedup-cancel-unpaid-policy.md deleted file mode 100644 index 08f48dcbeb..0000000000 --- a/.changeset/dedup-cancel-unpaid-policy.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Replaces the dedup helper's "always cancel newer" policy with "cancel the unpaid one when exactly one is unpaid." Determines paid/unpaid via `latest_invoice.status === 'paid'`. When zero or multiple subs are unpaid, the helper now refuses to auto-cancel and emits a `manual_review` outcome with full context for ops. Webhook handler updated to handle the new four-way outcome (`no_duplicate` | `canceled_new` | `canceled_existing` | `manual_review`). Closes the cancel-newer sub-item of #3245. diff --git a/.changeset/dedup-customer-apology-email.md b/.changeset/dedup-customer-apology-email.md deleted file mode 100644 index f7ec975cbf..0000000000 --- a/.changeset/dedup-customer-apology-email.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Adds a customer-facing notice when the webhook dedup helper auto-resolves a duplicate subscription. Branches copy on whether money moved (refund vs. no charge) and on which sub was canceled (new duplicate vs. old unpaid intake). Sent fire-and-forget to all org admins. Closes the customer-apology sub-item of #3245. diff --git a/.changeset/dependabot-bodyparser-mintlify-bumps.md b/.changeset/dependabot-bodyparser-mintlify-bumps.md deleted file mode 100644 index 2eaf4b4e18..0000000000 --- a/.changeset/dependabot-bodyparser-mintlify-bumps.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Bump dev dependencies via dependabot npm_and_yarn group: `mintlify` 4.2.525 → 4.2.531 and transitive `body-parser` 1.20.1 → 1.20.5 inside `@mintlify/previewing`. Both changes are dev-only (mintlify is in `devDependencies`, and `body-parser` is not a direct dependency of the server runtime — production uses Express's bundled bodyParser). No runtime behavior change. diff --git a/.changeset/deps-cargo-tauri-security.md b/.changeset/deps-cargo-tauri-security.md deleted file mode 100644 index baf702995b..0000000000 --- a/.changeset/deps-cargo-tauri-security.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Bump Tauri desktop cargo indirect dependencies for security patches: `bytes` 1.11.0 → 1.11.1 (integer overflow fix in `BytesMut::reserve`) and `time` 0.3.44 → 0.3.47 (RFC 2822 parsing DoS via unbounded recursion). Lockfile-only change under `apps/desktop/src-tauri/`. diff --git a/.changeset/deps-npm-security-patches.md b/.changeset/deps-npm-security-patches.md deleted file mode 100644 index 86d0940fd2..0000000000 --- a/.changeset/deps-npm-security-patches.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Bump npm_and_yarn indirect dependencies for security patches: `@xmldom/xmldom` 0.8.12 → 0.8.13 (XML injection advisories GHSA-j759-j44w-7fr8/GHSA-x6wf-f3px-wcqx/GHSA-f6ww-3ggp-fr8h + stack-overflow DoS GHSA-2v35-w6hq-6mfw), `hono` 4.12.12 → 4.12.14 (JSX SSR attribute injection GHSA-458j-xx4x-4375), and `follow-redirects` 1.15.11 → 1.16.0 (input sanitization). Lockfile-only change. diff --git a/.changeset/deps-uv-python-security.md b/.changeset/deps-uv-python-security.md deleted file mode 100644 index c670e59f6c..0000000000 --- a/.changeset/deps-uv-python-security.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Bump Python uv group dependencies, primarily for `cryptography` 46.0.3 → 46.0.7 which fixes CVE-2026-39892 (buffer overflow via non-contiguous buffers), CVE-2026-34073 (name constraint bypass with wildcard DNS SAN leafs), and a binary-curve key attack. Also bumps `protobuf` 6.33.0 → 6.33.5, `pyasn1` 0.6.1 → 0.6.3, `python-dotenv` 1.2.1 → 1.2.2, `python-multipart` 0.0.20 → 0.0.26, `requests` 2.32.5 → 2.33.0, and `urllib3` 2.5.0 → 2.6.3. Lockfile-only change. diff --git a/.changeset/docs-cli-auth-graders.md b/.changeset/docs-cli-auth-graders.md deleted file mode 100644 index 2a327869fa..0000000000 --- a/.changeset/docs-cli-auth-graders.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Adds `docs/building/grading.mdx`, a discoverable reference page for the `@adcp/client` CLI authentication conformance graders (`grade request-signing`, `diagnose-auth`, `signing generate-key`, `signing verify-vector`). These commands shipped in 5.21+ but were not indexed by `search_docs`, causing Addie to fabricate issues requesting tools that already exist. The new page includes frontmatter keywords for grade/evaluate/OAuth/RFC 9421 queries, per-command documentation, and cross-links from `authentication.mdx` and `security.mdx`. diff --git a/.changeset/docs-media-buy-pending-display-collapsing.md b/.changeset/docs-media-buy-pending-display-collapsing.md deleted file mode 100644 index 93518715c5..0000000000 --- a/.changeset/docs-media-buy-pending-display-collapsing.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -docs(media-buy): document blessed pattern for collapsing `pending_creatives` and `pending_start` in buyer UIs. - -Adds a `` block to the media-buy Lifecycle States section stating that buyer applications MAY render both pending states as a single `pending` label for display, but MUST preserve the raw status value on the wire (API responses, webhooks, persisted records, logs) so downstream gating keeps working. Reinforces the existing guidance to drive UI affordances from `valid_actions` rather than from the status value directly. Closes #2988. No schema change — docs guidance only. diff --git a/.changeset/docs-storyboard-a2a-submitted-artifact-check.md b/.changeset/docs-storyboard-a2a-submitted-artifact-check.md deleted file mode 100644 index 1b24adeb88..0000000000 --- a/.changeset/docs-storyboard-a2a-submitted-artifact-check.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -docs(compliance): enumerate a2a_submitted_artifact in storyboard-schema check list - -Adds `a2a_submitted_artifact` to the canonical `check:` enum comment in -`static/compliance/source/universal/storyboard-schema.yaml` and adds a sibling -documentation section (modelled after the existing `refs_resolve` section) that -describes the A2A wire-shape invariants the check asserts, its `not_applicable` -self-skip on non-A2A transports, and its provenance (adcp-client#899 / #952). - -Documentation-only: the runner already supports the check as of adcp-client#952; -no schema, task definition, or wire-format change. diff --git a/.changeset/document-sdk-3-0-support.md b/.changeset/document-sdk-3-0-support.md deleted file mode 100644 index 9b5c62d645..0000000000 --- a/.changeset/document-sdk-3-0-support.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Mark AdCP 3.0 as GA across the docs: add a "Version 3.0.0" entry to the release notes covering the rc.3 → GA deltas (meta-tool refactor, governance error, `update_media_buy` account requirement, `preview_creative` and `comply_test_controller` flattening, offline reporting, TMP provider lifecycle, geo-capability revert, VAST 4.2 events, generalized `measurement_window`), add an "AdCP 3.0 support" section to Schemas and SDKs listing minimum 3.0-compatible SDK versions (`@adcp/client` 5.13.0, `adcp` 4.0.0, `adcp-go` v1.0.0), refresh schema-path examples from 2.5.3 → 3.0.0, and remove the "release candidate" framing from intro, faq, industry-landscape, and adcp-vs-openrtb. Closes #2229. diff --git a/.changeset/document-specialism-naming-triplet.md b/.changeset/document-specialism-naming-triplet.md deleted file mode 100644 index 2274f7b264..0000000000 --- a/.changeset/document-specialism-naming-triplet.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -docs(compliance): document specialism naming triplet and unclaimed-specialism grading behavior - -AdCP specialisms appear under three casings depending on context — kebab-case on the wire (`capabilities.specialisms[]`), snake_case as storyboard category IDs, and prose in storyboard titles. The mapping was already in the compliance-catalog naming-conventions table but the "unclaimed = no tracks" failure mode was not documented anywhere. - -Added a `` callout to the "How to claim" section of `docs/building/compliance-catalog.mdx` (immediately after the runner steps list) explaining that an agent wiring all required tools for a specialism but omitting the kebab-case ID from `capabilities.specialisms[]` will receive "No applicable tracks found" — a silent fail at the track level. - -Updated the `specialisms` field description in `static/schemas/source/protocol/get-adcp-capabilities-response.json` to state that values MUST be kebab-case and that omitting an ID silently skips the specialism's tracks in the compliance runner. No wire-format or schema-structure changes. - -Docs-only. No protocol spec bump. diff --git a/.changeset/drop-addie-rules-table.md b/.changeset/drop-addie-rules-table.md deleted file mode 100644 index 95c5910ba2..0000000000 --- a/.changeset/drop-addie-rules-table.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Drop `addie_rules` DB table and remove dead admin UI Rules tab. Rules have been -served from `server/src/addie/rules/*.md` since PR #2028; the DB table, three -read-only GET endpoints, ~140 lines of dead DB methods, and the rule-CRUD admin -UI were all vestige. Admin Rules tab replaced with a system-prompt viewer. diff --git a/.changeset/dry-tier-column-selector.md b/.changeset/dry-tier-column-selector.md deleted file mode 100644 index 93439c8503..0000000000 --- a/.changeset/dry-tier-column-selector.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Close #2826: collapse the duplicated SQL + row-type pair for resolving an organization's membership tier inside a transaction. `applyAgentVisibility` (member-profiles.ts) and the seat-check path (organization-db.ts) both hand-rolled the same `SELECT membership_tier, subscription_price_lookup_key, ...` against a pg client and then fed the row into `resolveMembershipTier`. Every future tier-relevant column would have had to land in three places (resolver input type, both SELECTs) or silently degrade. - -- New `MembershipTierRow` type + `MEMBERSHIP_TIER_COLUMNS` tuple in `organization-db.ts` as the single source of truth for the resolver's input shape. -- New `readMembershipTierFromClient(client, orgId, { forUpdate? })` helper that runs the SELECT with the shared column list, parses the row, and returns `MembershipTier | null`. Optional `forUpdate` for callers that need to lock the `organizations` row too. -- Both inline call sites replaced. - -No behavior change. 1920 server + 631 root unit tests pass. diff --git a/.changeset/dup-customer-detection-by-email-name.md b/.changeset/dup-customer-detection-by-email-name.md deleted file mode 100644 index ccb935096c..0000000000 --- a/.changeset/dup-customer-detection-by-email-name.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Extends `findStripeCustomerMismatches` (and `GET /api/admin/stripe-mismatches`) to detect duplicate Stripe customers by shared email or shared name+active-sub, in addition to the existing metadata-based detection. Each mismatch carries `match_reason: 'metadata' | 'email' | 'name'`. Closes #3200, the ResponsiveAds case where two customers shared an email and the orphan generated a duplicate $2,500 invoice. diff --git a/.changeset/duplicate-subscription-guard.md b/.changeset/duplicate-subscription-guard.md deleted file mode 100644 index 5c88c25f71..0000000000 --- a/.changeset/duplicate-subscription-guard.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Refuse to mint a new Stripe subscription/invoice when the org already has an active, trialing, or past_due one. - -`POST /api/checkout-session`, `POST /api/invoice-request`, and `POST /api/invite/:token/accept` now all run a duplicate-subscription guard before issuing billing. If the org has a live subscription, they return 409 with the existing subscription details and a Stripe Customer Portal URL. Tier upgrades and payment-method changes belong in the portal, not these intake routes — without this guard, two of them in sequence produced the duplicate $3K Builder sub on top of Triton's active $10K Corporate sub (Apr 2026). diff --git a/.changeset/ehj-page-approved-content.md b/.changeset/ehj-page-approved-content.md deleted file mode 100644 index 50f598694f..0000000000 --- a/.changeset/ehj-page-approved-content.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -docs(governance): replace the Embedded Human Judgment page with the approved final content from the governance workstream — a manifesto plus oversight framework covering design principles, domains of human-owned judgment, governance architecture, data protection, decision/escalation framework, protocol vs runtime, and audit. Closes #2905. diff --git a/.changeset/envelope-forbid-legacy-status-fields.md b/.changeset/envelope-forbid-legacy-status-fields.md deleted file mode 100644 index 55c4b3da9d..0000000000 --- a/.changeset/envelope-forbid-legacy-status-fields.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Make the `task_status` / `response_status` prohibition from #3021 machine-enforceable at the schema level. Adds a `not: { anyOf: [{ required: [task_status] }, { required: [response_status] }] }` constraint on `protocol-envelope.json` (matching the existing idiom in `catchment.json`) so any JSON Schema validator rejects envelopes that dual-emit legacy status fields — no runner-specific primitive required. The prose MUST NOT in the envelope `status` description remains for human readers; the constraint is what validators act on. Closes #3041 at the spec layer. Runtime conformance (storyboard `field_absent` primitive + `@adcp/client` implementation) is tracked separately. diff --git a/.changeset/escalation-file-as-github-issue.md b/.changeset/escalation-file-as-github-issue.md deleted file mode 100644 index f2bc6b1e1e..0000000000 --- a/.changeset/escalation-file-as-github-issue.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Escalation triage can now suggest filing bug-shaped escalations as GitHub -issues. When a URL probe confirms the bug still repros (404/410 on the -referenced AAO page), the classifier emits a `file_as_issue` suggestion -with a pre-drafted title/body. Admins review the draft on -`/admin/escalations/triage` and one-click file — Addie creates the issue -via GITHUB_TOKEN, records the URL on the escalation, and marks it resolved. - -The draft is built from Addie-authored fields only (summary + context) — -user PII (email, slack handle, display name, raw original request) is -excluded by construction. diff --git a/.changeset/escape-html-attribute-context-hardening.md b/.changeset/escape-html-attribute-context-hardening.md deleted file mode 100644 index ee00547e22..0000000000 --- a/.changeset/escape-html-attribute-context-hardening.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -`escapeHtml` helpers in 63 server/public HTML pages now escape `"` and `'` in addition to `<>&`, so values interpolated into HTML attribute contexts (`src="${escapeHtml(...)}"`, `data-*` attrs, etc.) can't break out of the attribute even if upstream validation lets a quote through. Defense-in-depth for #3153. - -The `div.textContent → div.innerHTML` round-trip historically only escaped `<>&`, leaving the helper unsafe for the dominant call site (HTML attributes in template literals). Pages that already used a manual `replace()` chain were already safe and weren't touched. - -Behavior change for callers: text content with quotes now renders with `"` / `'` entities — visually identical in browsers, byte-different in inspect-element. The `tests/community/profile-edit.test.ts` assertion was updated accordingly. diff --git a/.changeset/extend-hint-formatter-four-kinds.md b/.changeset/extend-hint-formatter-four-kinds.md deleted file mode 100644 index e375db17c2..0000000000 --- a/.changeset/extend-hint-formatter-four-kinds.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Addie: extend the storyboard hint fix-plan formatter to render Diagnose / Locate / Fix / Verify playbooks for the four hint kinds 5.18.0 added beyond `context_value_rejected` — `shape_drift`, `missing_required_field`, `format_mismatch`, and `monotonic_violation`. Each kind dispatches to its own templated playbook off the structured fields the runner emits; unknown future kinds drop silently from the fix-plan section while the upstream `hint.message` still surfaces them at the consumer's discretion. Trust model documented per-kind in the formatter's docstring. diff --git a/.changeset/extract-handle-subscription-created.md b/.changeset/extract-handle-subscription-created.md deleted file mode 100644 index 2c34ac47da..0000000000 --- a/.changeset/extract-handle-subscription-created.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Refactor the Stripe `customer.subscription.created` webhook block out of `server/src/http.ts` into `server/src/billing/handle-subscription-created.ts`. No behavior change — the handler takes its deps (stripe, workos, orgDb, pool, notifiers, logger) as args so the multi-step flow (resolver → org agreement update → user-level attestation → audit log → org_activities → pending-clear → Stripe metadata stamp → Slack notify) is now testable end-to-end. Adds `handle-subscription-created.test.ts` with 7 integration-style cases: happy path, deleted customer, no-user-resolved, recordUserAgreementAcceptance throws, pending_agreement_user_id priority over subscription/customer metadata, fallback to published version when pending is absent, Stripe metadata stamp failure non-blocking. Deletes `agreement-recording.test.ts` — 351 lines of mock-SQL assertions that didn't exercise any real code path (testing-expert feedback on PR #3011). Coverage for the real behavior now lives in the new integration test plus the existing `resolve-subscription-user.test.ts` and `database-schema.test.ts`. diff --git a/.changeset/feat-content-editor-polish.md b/.changeset/feat-content-editor-polish.md deleted file mode 100644 index 49b4baa430..0000000000 --- a/.changeset/feat-content-editor-polish.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -feat(editorial): content editor polish — rich-text paste, Submit-for-Review, title length validation. - -Three epic #2693 cleanup items bundled: - -**#2699 — Rich-text paste.** The "Write Content" editor in both `admin-content.html` and `my-content.html` now converts pasted HTML to markdown via turndown (loaded via CDN). Paste from Google Docs, Notion, or Word preserves headings, bold/italic, lists, links, and tables — no more "paste kills the formatting" loop that blocked Mary from submitting the 3.0 launch blog. Plain-text paste falls through to the browser default with no regression for members who already have markdown in their clipboard. DOMPurify sanitizes the HTML before it hits turndown. - -**#2719 — my-content.html adds "Submit for Review".** Member-facing form previously only offered Draft / Publish Now, so members hitting "Publish Now" got silently demoted by the server-side review gate. Now the status dropdown has three options with `Submit for Review` as the explicit default, matching the admin-content.html form. The status hint text explains what happens. - -**#2734 — Title length validation returns 400.** `proposeContentForUser` now validates `title ≤ 500`, `subtitle ≤ 1000`, `author_title ≤ 255`, `external_site_name ≤ 255` before the INSERT, so an oversized field returns a friendly 400 with a field-specific message instead of the Postgres "value too long" → HTTP 500 we were hitting. Dashboard forms also got `maxlength="500"` on the title inputs so the browser prevents entry past the limit. - -Three integration tests added to `content-my-content.test.ts` covering the validation branches. - -Remaining epic #2693 work: expert-review follow-ups #2712/#2713/#2733/#2735/#2736/#2752/#2753/#2754/#2755/#2756. diff --git a/.changeset/feed-authorization-events.md b/.changeset/feed-authorization-events.md deleted file mode 100644 index 511677eeb4..0000000000 --- a/.changeset/feed-authorization-events.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Authorization events on the registry change feed. Migration 442 adds -Postgres triggers on `catalog_agent_authorizations` (PR 4b-prereq) and -`adagents_authorization_overrides` (PR 1) that emit -`authorization.granted` / `.revoked` / `.modified` events into -`catalog_events`. Wire format pinned in -`specs/registry-authorization-model.md` ("Change-feed event shape"). - -Reader side: zero changes. The existing `/api/registry/feed` endpoint -already supports `event_type` glob filtering, so consumers subscribe -with `?types=authorization.*` and the new events flow through. - -Trigger emission semantics: -- `granted` on base-row insert / un-tombstone, on `add` override insert, - on `suppress` override supersede. -- `revoked` on base-row tombstone, on `add` override supersede, on - `suppress` override insert (fans out per affected base row). -- `modified` on base-row UPDATE of `authorized_for` / `expires_at` / - `disputed`. seq_no rotation alone produces no event. -- Override layer scoped to `evidence='adagents_json'` only; agent_claim - and community rows pass through unaffected. - -Drive-by: scoped `registry-feed.test.ts` cleanups to its own actor and -filtered queryFeed reads by event_type so concurrent test files writing -events via the new triggers don't trample shared catalog_events state. - -Refs #3177. Builds on #3274 (schema). Spec #3251. diff --git a/.changeset/field-value-or-absent-check-enum.md b/.changeset/field-value-or-absent-check-enum.md deleted file mode 100644 index e18943dc32..0000000000 --- a/.changeset/field-value-or-absent-check-enum.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Document `field_value_or_absent` check matcher in storyboard-schema.yaml and runner-output-contract.yaml. The matcher shipped in `@adcp/client` 5.16.0; this update keeps the normative compliance spec in sync and unblocks storyboards that need to assert on envelope-optional fields like `replayed`. diff --git a/.changeset/fix-436-dup-migration.md b/.changeset/fix-436-dup-migration.md deleted file mode 100644 index 09cdca84b9..0000000000 --- a/.changeset/fix-436-dup-migration.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Renumbers `436_organization_membership_provisioning_source.sql` to `438_*` to break the duplicate-migration deadlock on main. PRs #3294 and #3295 both landed migration 436 simultaneously, blocking every open PR at the duplicate-check and migration-build CI gates. 437 is already taken by an unrelated `auto_provision_digest_sent_at` migration that landed in the same window, so this lands at 438. diff --git a/.changeset/fix-addie-depth-model-1m-context.md b/.changeset/fix-addie-depth-model-1m-context.md deleted file mode 100644 index b7bf019ac7..0000000000 --- a/.changeset/fix-addie-depth-model-1m-context.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Fix Addie 404 errors on deep-reasoning turns: - -- `ModelConfig.depth` default was `claude-opus-4-7[1m]`, which is not a valid Anthropic model ID (the `[1m]` suffix is not part of the model name). Any turn the router classified as `requires_depth` — expert consultation, multi-doc synthesis, protocol-level analysis — or any message in a working-group / council channel would 404 with `not_found_error: model: claude-opus-4-7[1m]`. -- Default is now `claude-opus-4-7` (valid model ID). -- 1M context is now enabled the correct way: via the `context-1m-2025-08-07` Anthropic beta flag, applied automatically to models listed in `MODELS_SUPPORTING_1M_CONTEXT` (currently Opus 4.7 and Sonnet 4.6). Opt out per-deploy with `CLAUDE_DISABLE_1M_CONTEXT=true`. -- Addie's streaming path was switched from `client.messages.stream` to `client.beta.messages.stream` so it can pass `betas` alongside the existing non-stream beta call. diff --git a/.changeset/fix-addie-null-bytes-and-empty-slack-reply.md b/.changeset/fix-addie-null-bytes-and-empty-slack-reply.md deleted file mode 100644 index 811dfb4616..0000000000 --- a/.changeset/fix-addie-null-bytes-and-empty-slack-reply.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -fix(addie): strip U+0000 from thread message inserts and avoid empty Slack postMessage in active thread replies - -Two related Slack failures observed in production: - -1. `ThreadService.addMessage` was rejecting inserts with `unsupported Unicode escape sequence` when a tool result contained a null byte. Postgres TEXT/JSONB both reject U+0000, so we now strip null bytes from `content`, `content_sanitized`, `flag_reason`, `email_message_id`, the `tools_used` array, and the JSON-stringified `tool_calls` / `router_decision` fields before insertion. -2. `handleActiveThreadReply` was calling `chat.postMessage` with an empty `text` when the model produced no usable output, causing Slack to return `no_text`. The active-thread reply path now falls back to the same apology used elsewhere in the bolt app instead of sending an empty message. diff --git a/.changeset/fix-addie-router-opinion-poll-flaky-test.md b/.changeset/fix-addie-router-opinion-poll-flaky-test.md deleted file mode 100644 index 28f6106900..0000000000 --- a/.changeset/fix-addie-router-opinion-poll-flaky-test.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Fix flaky addie-router LLM test for opinion-poll channel messages (#3101). - -Clarifies the channel-ignore prompt to explicitly call out opinion polls (e.g. "what do you all think about IAB CTV guidelines?") with a carve-out preserving responses to AdCP-specific protocol questions. Replaces the non-deterministic live-API assertion with a recorded fixture so the test is stable on every PR. diff --git a/.changeset/fix-admin-error-triage.md b/.changeset/fix-admin-error-triage.md deleted file mode 100644 index cee759aefe..0000000000 --- a/.changeset/fix-admin-error-triage.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Two admin-channel errors fixed: - -- `updateThreadTitle` truncates titles to 500 chars so generated titles can't overflow `addie_threads.title VARCHAR(500)`. -- `/brand/:id/brand.json` and `/property/:id/adagents.json` reject non-UUID ids with a 404 instead of letting Postgres throw a 500 on malformed input (e.g. `abc123`). diff --git a/.changeset/fix-admin-stripe-subscription-id-response.md b/.changeset/fix-admin-stripe-subscription-id-response.md deleted file mode 100644 index a1629a49f1..0000000000 --- a/.changeset/fix-admin-stripe-subscription-id-response.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix admin GET /accounts/:orgId not returning stripe_subscription_id and price_lookup_key in the subscription object. Both fields were written to the DB correctly by the webhook handler but were omitted from the response builder, causing operators to misread subscription state after cleanup operations. diff --git a/.changeset/fix-announcement-test-module-cache-race.md b/.changeset/fix-announcement-test-module-cache-race.md deleted file mode 100644 index 73d4a2ebb0..0000000000 --- a/.changeset/fix-announcement-test-module-cache-race.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix flaky announcement tests under `--pool=threads` by adding `vi.resetModules()` to `beforeEach` in all 7 affected test files, and add `testTimeout: 10000` to `vitest.config.ts` so individual hung tests fail with a name instead of silently consuming the 60s precommit budget. diff --git a/.changeset/fix-bundler-enum-hoisting.md b/.changeset/fix-bundler-enum-hoisting.md deleted file mode 100644 index 20bf33d22c..0000000000 --- a/.changeset/fix-bundler-enum-hoisting.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix bundler enum hoisting: add `hoistDuplicateInlineEnums` post-processing step in `scripts/build-schemas.cjs` to detect titled pure-enum schemas inlined 2+ times in a bundled output, hoist them to root `$defs`, and replace duplicates with `$ref` pointers. Eliminates the `AgeVerificationMethod1` numbered-suffix codegen artifact in `json-schema-to-typescript` consumers. Complex object schemas (`BriefAsset1`, `VASTAsset1`, etc.) are intentionally out of scope — see #3145 for the RFC on opt-in `x-hoist` markers. diff --git a/.changeset/fix-channel-privacy-toctou.md b/.changeset/fix-channel-privacy-toctou.md deleted file mode 100644 index d8ce0294c0..0000000000 --- a/.changeset/fix-channel-privacy-toctou.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Close #2735: recheck Slack channel privacy at send time before posting sensitive content. - -The admin-settings routes (`billing-channel`, `escalation-channel`, `admin-channel`, `prospect-channel`, `error-channel`, `editorial-channel`) validate `is_private === true` at write time, but Slack lets a channel owner flip the channel public afterward — and the server wouldn't notice. Billing events, escalation summaries, editorial reviewer names, admin alerts, prospect data, and system errors could all leak into a formerly-private channel that's now workspace-visible. - -- New `verifyChannelStillPrivate(channelId)` helper in `server/src/slack/client.ts` — uses the existing 30-minute channel-info cache so the happy path is free, returns `false` when the channel is no longer private (or can't be verified), emits a structured `channel_privacy_drift` warn log. -- `sendChannelMessage` gained an `options.requirePrivate` flag. When `true`, the gate blocks the send and returns `{ ok: false, skipped: 'not_private' }`. Default is `false` — WG / announcement channels aren't regressed. -- All six sensitive notification flows now opt in: billing, prospect (two handlers), assessment (admin channel), error-notifier (two paths), escalation tool, editorial pending-content notification. -- Drive-by: fixed a pre-existing main typecheck break in `training-agent/task-handlers.ts` (stale `asset_type` discriminator after #2795). - -Daily audit job for channels that aren't written to often is tracked separately as #2849. diff --git a/.changeset/fix-crt-raw-pem.md b/.changeset/fix-crt-raw-pem.md deleted file mode 100644 index a5bceb7235..0000000000 --- a/.changeset/fix-crt-raw-pem.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix `scripts/sign-protocol-tarball.sh` to emit raw PEM certificates instead of base64-wrapped PEM. Cosign's `sign-blob --output-certificate` writes the cert base64-encoded by convention; downstream tooling (adcp-go's `download.sh`, anything doing a PEM header sniff before handing the cert to `cosign verify-blob`) expects raw PEM. Decode in place after signing so `.crt` on disk matches the Sigstore standard layout. Also re-writes the already-shipped `dist/protocol/3.0.0.tgz.crt` to raw PEM (same underlying cert bytes; `cosign verify-blob` accepts both formats, so signatures continue to verify). Closes adcp#2900. diff --git a/.changeset/fix-deterministic-testing-session-key-schema-ref.md b/.changeset/fix-deterministic-testing-session-key-schema-ref.md deleted file mode 100644 index 94e3531600..0000000000 --- a/.changeset/fix-deterministic-testing-session-key-schema-ref.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix three root causes behind deterministic_testing storyboard failures under @adcp/client 5.18.0: (1) comply_test_controller steps in deterministic_media_buy, deterministic_creative, deterministic_delivery, and deterministic_budget phases lacked brand/account in sample_request, causing sessionKeyFromArgs to fall back to open:default while entities lived in open:acmeoutdoor.example — fixed by adding brand: { domain: acmeoutdoor.example } to all ten affected controller steps; (2) sync_fresh_creative_for_rejection referenced non-existent schema paths media-buy/sync-creatives-*.json (moved to creative/ in the 3.0.0 creative-domain split) — corrected both schema_ref and response_schema_ref; (3) context_outputs on that same step used key: instead of name: causing $context.fresh_creative_id to never be populated; (4) deriveStatus in task-handlers.ts returned pending_creatives even after comply_test_controller forced status to active — fixed by adding a complyControllerForced flag (set only on real status writes, consumed-and-cleared on first deriveStatus read). diff --git a/.changeset/fix-dup-migration-433.md b/.changeset/fix-dup-migration-433.md deleted file mode 100644 index 2191bf353a..0000000000 --- a/.changeset/fix-dup-migration-433.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Renumber `433_auto_provision_verified_domain.sql` to `434_auto_provision_verified_domain.sql` to resolve a collision with `433_catalog_adagents_lookup_index.sql` that landed in the same window. The migration runner (`server/src/db/migrate.ts:76-84`) throws on duplicate version numbers at startup, so this collision blocks every deploy and every PR's "No duplicate migration numbers" check until resolved. Catalog landed first (PR #3244, commit 3496020) and may already be recorded in `schema_migrations` as version 433 in some envs — auto_provision is renumbered instead so envs that already applied catalog see no mismatch above the baseline. diff --git a/.changeset/fix-dup-migration-434.md b/.changeset/fix-dup-migration-434.md deleted file mode 100644 index 887bff6680..0000000000 --- a/.changeset/fix-dup-migration-434.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Restore `434_catalog_adagents_lookup_index.sql` back to `433_catalog_adagents_lookup_index.sql`. PRs #3256 and #3257 both fixed the original 433 collision in parallel from different conductor workspaces — #3257 (auto_provision → 434) merged first, then #3256 (catalog → 434) merged on a stale view and re-introduced a collision, this time at version 434. This restores catalog to its original 433 number, leaving auto_provision at 434, matching how envs that deployed between #3257 and #3256 would have recorded the migrations in `schema_migrations`. diff --git a/.changeset/fix-dup-migration-436.md b/.changeset/fix-dup-migration-436.md deleted file mode 100644 index 9afdab95d9..0000000000 --- a/.changeset/fix-dup-migration-436.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -fix(migrations): renumber duplicate 436 to 438 to unblock deploys - -PRs #3282 and #3295 both shipped a `436_*.sql` migration in parallel before -PR #3288 (the duplicate-detection check) landed. The collision blocks every -subsequent deploy at the Preflight check that #3288 added — main is wedged -and prod is on stale code. - -Renames `436_organization_membership_provisioning_source.sql` → `438_*` (the -later-merged of the two). Both migrations use `IF NOT EXISTS` so dev DBs -that already applied the file as 436 will see the renumbered 438 as a -no-op next migrate run; prod's `release_command` never succeeded with the -duplicate present, so prod applies it fresh as 438. - -Updates the matching changeset reference in `provisioning-source-attribution.md`. diff --git a/.changeset/fix-dup-migration-444.md b/.changeset/fix-dup-migration-444.md deleted file mode 100644 index db9581acbc..0000000000 --- a/.changeset/fix-dup-migration-444.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -fix(migrations): renumber duplicate 444 to 445 — unblock deploys - -PRs #3258 and #3136 both shipped a `444_*.sql` migration in parallel before the duplicate-detection preflight could fire. Result: every deploy after the second of the two merges fails at the migration-numbering check, blocking the queue. - -Renames the later-merged of the two: -- `444_agent_test_runs.sql` → `445_*` (from #3258, merged after #3136) - -Idempotent: `444_drop_addie_rules.sql` uses `DROP TABLE IF EXISTS`; `445_agent_test_runs.sql` uses `CREATE TABLE IF NOT EXISTS`. Dev DBs that already pulled the original `444_agent_test_runs.sql` will see the renumbered 445 as a no-op next migrate run; prod's `release_command` was blocked at the duplicate, so it applies fresh as 445. - -Same shape as the earlier 436 collision fix (#3300). diff --git a/.changeset/fix-fallback-say-empty-guard.md b/.changeset/fix-fallback-say-empty-guard.md deleted file mode 100644 index 9fc09e8b37..0000000000 --- a/.changeset/fix-fallback-say-empty-guard.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Fix `Addie Bolt: Fallback say() also failed` errors during upstream Anthropic overloads: - -- When streaming fails before producing any text, the stream-stop fallback built a Slack `section` block with empty `mrkdwn` text, which Slack rejects as `invalid_blocks`. Now falls back to a plain apology (`say(apology)`) when `slackText` is empty. -- Demote the "fallback also failed" log from `error` to `warn` when the root cause is an already-logged retries-exhausted error, so upstream outages don't page twice. diff --git a/.changeset/fix-format-asset-oneof-titles.md b/.changeset/fix-format-asset-oneof-titles.md deleted file mode 100644 index dccde467f0..0000000000 --- a/.changeset/fix-format-asset-oneof-titles.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Add `title` to all `oneOf` branches in `format.json`'s `assets[]` array so codegen tools (json-schema-to-typescript, datamodel-code-generator, oapi-codegen) produce named, discriminated per-asset-type interfaces instead of collapsing them to an untyped union. Adds titles `IndividualImageAsset` … `IndividualCatalogAsset` and `RepeatableGroupAsset` at the top level, plus `GroupImageAsset` … `GroupWebhookAsset` for the nested branches inside `repeatable_group.assets[]`. Purely annotation-level; no validation or wire-format change. diff --git a/.changeset/fix-get-signals-max-results-precedence.md b/.changeset/fix-get-signals-max-results-precedence.md deleted file mode 100644 index db0c102532..0000000000 --- a/.changeset/fix-get-signals-max-results-precedence.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Deprecates top-level `max_results` on `get_signals` and pins `pagination.max_results` precedence. - -`get-signals-request.json` carried two independent pagination fields — a legacy top-level `max_results` (no cap, no default, predates the pagination envelope) and the standard `pagination` envelope (`pagination.max_results`, max: 100, default: 50). The schema was silent on which wins when both are present. - -This change adds a MUST-level precedence rule: when both fields are present, agents MUST honor `pagination.max_results`. It also deprecates the top-level field with guidance for sellers receiving it without a pagination envelope. The top-level `max_results` will be removed in AdCP 4.0. - -All other paginated read endpoints (`get_products`, `list_creatives`, `list_creative_formats`, `get-collection-list`, `get-property-list`, `get-media-buy-artifacts`, `tasks-list`) carry only `pagination` — this brings `get_signals` into alignment. - -Non-breaking: adds description-level deprecation and normative prose. No type, structure, or required-field changes. Existing callers unaffected; sellers adding the conflict check gain new conformance grounding. diff --git a/.changeset/fix-github-connected-account-unavailable.md b/.changeset/fix-github-connected-account-unavailable.md deleted file mode 100644 index d43c5eded5..0000000000 --- a/.changeset/fix-github-connected-account-unavailable.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -**Fix: distinguish WorkOS unavailable from not-connected in getGitHubConnectedAccount** - -`getGitHubConnectedAccount` previously swallowed all non-404 WorkOS errors -and returned `null`, causing the member-hub Connections card to show a -"Connect GitHub" button during a WorkOS outage. Users who were already -connected could inadvertently trigger double-consent or hit a 502 on the -authorize endpoint. - -**Changed:** - -- `server/src/services/pipes.ts` — `getGitHubConnectedAccount` now returns a - discriminated union: `{ status: 'connected', login }`, `{ status: 'not_connected' }`, - or `{ status: 'unavailable', reason }`. Non-404 WorkOS errors map to - `unavailable` instead of collapsing into `null`. -- `server/src/http.ts` — `GET /api/me/connected-accounts/github` returns HTTP - 503 with `{ connected: false, unavailable: true }` when the status is - `unavailable`. Callers that do not handle 503 degrade to their existing - error branch (no regression). -- `server/public/membership/hub.html` — `renderConnections` now renders a - "temporarily unavailable" message and omits the Connect button when the - API returns 503. This prevents users from triggering the authorize flow - during an outage. - -**Tests:** `server/tests/unit/pipes-connected-account.test.ts` — 5 new tests -covering connected (with external_user_handle), connected (with external_handle -fallback), not_connected (404), unavailable (5xx), and unavailable (network -error without status code). - -Closes #2997. diff --git a/.changeset/fix-gov-fixture-divergence.md b/.changeset/fix-gov-fixture-divergence.md deleted file mode 100644 index 7360c9d037..0000000000 --- a/.changeset/fix-gov-fixture-divergence.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Governance storyboards: two more seed-fixture alignment fixes to get -both dispatch modes cleaner post-adcp-client#794. - -- **`governance-spend-authority/index.yaml`** — rename the governance - plan from `gov_acme_q2_2027` to `gov_acme_spend_authority_q2_2027`. - `protocols/governance/index.yaml` already seeded a completely - different plan under the same id (different budget, flight, - policies), so the SDK's seed store rejected the second replay with - `INVALID_PARAMS: Fixture for seed_plan:gov_acme_q2_2027 diverges - from the previously seeded fixture`. The two plans are semantically - distinct (spend-authority with unlimited reallocation vs. the - media_buy_seller Q2 display-and-video flight), so unique ids are - correct. -- **`governance-delivery-monitor/index.yaml`** — add the missing - `products:` + `pricing_options:` fixtures for `outdoor_display_q2` - and `outdoor_video_q2` (both referenced in the storyboard's - `create_media_buy` packages). Values match the canonical seed in - `protocols/governance/index.yaml` to avoid divergence. - -Storyboard clean counts (overlay against compliance cache): -legacy 44 → 46, framework 38 → 39. Passing steps: legacy 362 → 373, -framework 362 → 372. diff --git a/.changeset/fix-governance-aware-seller-empty-phases.md b/.changeset/fix-governance-aware-seller-empty-phases.md deleted file mode 100644 index 485d4be879..0000000000 --- a/.changeset/fix-governance-aware-seller-empty-phases.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -fix(compliance): add capability_discovery phase to governance_aware_seller storyboard - -The `governance_aware_seller` specialism declared `phases: []`, leaving the conformance runner with nothing to enumerate and emitting `__no_phases__` against any agent under test. It was the only specialism in `static/compliance/source/specialisms/` with an empty phases list — every peer (governance-spend-authority, signal-marketplace, brand-rights, all sales-*) declares at least a local `capability_discovery` phase even when most of the work is delegated through `requires_scenarios:`. - -This adds the standard `get_adcp_capabilities` capability_discovery phase, mirroring `governance-spend-authority/index.yaml`. The four governance scenarios (`governance_approved`, `governance_conditions`, `governance_denied`, `governance_denied_recovery`) continue to compose in via `requires_scenarios:`. Closes #2972. diff --git a/.changeset/fix-governance-mode-binding-schema-drift.md b/.changeset/fix-governance-mode-binding-schema-drift.md deleted file mode 100644 index 80f10af062..0000000000 --- a/.changeset/fix-governance-mode-binding-schema-drift.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Add `mode` to `check_governance` response schema and fix `binding`→`check_type` drift in training agent audit entries. - -`check-governance-response.json` now declares the optional `mode` field (enforce/advisory/audit) that the training agent was already emitting, letting counterparties and regulators distinguish `approved`-with-finding decisions made under `enforce` from those made under `audit`. The training agent audit log handler no longer emits the non-canonical `binding` field (which caused schema-validation failures on the strict `entries[]` schema); it now emits `check_type: "intent"|"execution"` per the existing schema contract. The schema carries `x-status: experimental`. Audit-entry `mode` is added separately by #3160. diff --git a/.changeset/fix-impersonation-audit-tests.md b/.changeset/fix-impersonation-audit-tests.md deleted file mode 100644 index 14d7ff4b17..0000000000 --- a/.changeset/fix-impersonation-audit-tests.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -test(integration): fix impersonation-audit stale skip — rewrite 6 skipped tests as ThreadService unit tests against addie_threads; update schema assertions from legacy addie_conversations to live addie_threads table diff --git a/.changeset/fix-ipr-create-status-body.md b/.changeset/fix-ipr-create-status-body.md deleted file mode 100644 index 805d315756..0000000000 --- a/.changeset/fix-ipr-create-status-body.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Fix `scripts/ipr/github.mjs` `createStatus` — payload was being passed -at the wrong level of the `request()` option bag, so `state`/`context` -never reached the GitHub statuses API. - -`request(method, path, { body, query })` reads the payload from -`options.body`. `createIssueComment` and `updateIssueComment` already -wrap correctly (`{ body }`). `createStatus` was passing `{ state, -context, description, target_url }` directly at the top level, so the -outgoing request had an empty body and GitHub responded with -`422 Validation Failed: State is not included in the list`. - -Impact: the IPR Agreement check has been failing on every PR since -the workflow rewrite in #3011 landed — both the "awaiting signature" -pending status and the "signed" success status were hitting the same -failure path. Fix wraps the payload in `{ body: { ... } }` so the -state actually ships. diff --git a/.changeset/fix-ipr-signatures-link.md b/.changeset/fix-ipr-signatures-link.md deleted file mode 100644 index 8c7e183225..0000000000 --- a/.changeset/fix-ipr-signatures-link.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix Mintlify broken-links check failure introduced by #3011. IPR_POLICY.md §8 linked to `./signatures/README.md` and `./signatures/ipr-signatures.json` via relative paths; Mintlify's checker treats those as broken because the targets aren't in the docs tree it validates. Switch to absolute github.com URLs (same pattern used for IPR_POLICY itself in CONTRIBUTING.md). Unblocks docs-path-touching PRs from failing the `broken-links` workflow. diff --git a/.changeset/fix-ipr-signing-bug.md b/.changeset/fix-ipr-signing-bug.md deleted file mode 100644 index 651784243b..0000000000 --- a/.changeset/fix-ipr-signing-bug.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix IPR Policy signing workflow: replace `contributor-assistant/github-action` with a custom workflow that validates the PR author's GitHub identity instead of commit-author emails. The previous bot silently refused to record signatures when a commit's `git config user.email` didn't resolve to a registered GitHub user, even when a valid GitHub user commented the sign phrase — blocking @mikulbhatt, @tdgonzales-boosted-to-11, @lclaudon, @kmoegling-scope3, @numarasSigmaSoftware, @damianbedrock, @benjaminclot, and @thejamesbox from ever being recorded. Ledger backfilled from the historical `ipr-signatures` branch and from a live scan of sign comments across `adcp`, `adcp-client`, `adcp-client-python`, `creative-agent`, and `prebid/salesagent` — 28 signers total, up from 1 on main. New workflow sets a required `IPR Policy / Signature` commit status so branch protection can actually gate merges. See `signatures/README.md` for operating details. diff --git a/.changeset/fix-mcp-strict-bearer-bypass-test.md b/.changeset/fix-mcp-strict-bearer-bypass-test.md deleted file mode 100644 index b158da57f5..0000000000 --- a/.changeset/fix-mcp-strict-bearer-bypass-test.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -test(training-agent): fix /mcp-strict 401 test to omit bearer token - -The `unsigned create_media_buy on /mcp-strict returns 401` test was failing because -`callTool` sends `Authorization: Bearer test-token-for-strict` by default. The SDK's -`requireAuthenticatedOrSigned` short-circuits on a successful bearer result before the -`required_for` gate runs — bearer bypass is intentional per SDK design (#2586). - -Fix: pass `{ auth: false }` so the test simulates the actual grader scenario. Compliance -graders send no bearer token; they authenticate via RFC 9421 signature credentials only. -With no bearer and no signature, the `required_for: ['create_media_buy']` gate fires and -returns `401 request_signature_required` as expected. - -Non-protocol change (server test only); changeset is `--empty`. - -Closes #3080. diff --git a/.changeset/fix-media-buy-compliance-sandbox-flag.md b/.changeset/fix-media-buy-compliance-sandbox-flag.md deleted file mode 100644 index 57792d0db6..0000000000 --- a/.changeset/fix-media-buy-compliance-sandbox-flag.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -fix(compliance): add sandbox: true to create_media_buy sample_requests that use fixture product IDs - -Four media_buy_seller storyboard steps sent fixture product IDs (sports_preroll_q2, outdoor_ctv_q2, outdoor_display_q2, outdoor_video_q2) and a synthetic proposal_id (balanced_reach_q2) in create_media_buy sample_requests without sandbox: true on the account reference. Seller agents correctly reject unknown IDs with PRODUCT_NOT_FOUND under real catalog validation, causing the harness to report false failures against the seller. - -Fixed by adding sandbox: true to the account natural key in the sample_request of: -- protocols/media-buy/index.yaml (create_buy/create_media_buy) -- protocols/media-buy/scenarios/governance_conditions.yaml (buy_with_conditions/create_media_buy_conditions) -- protocols/media-buy/scenarios/proposal_finalize.yaml (accept_proposal/create_media_buy) -- protocols/media-buy/scenarios/governance_denied.yaml (buy_denied/create_media_buy_denied) - -Scenarios with controller_seeding: true and explicit fixtures blocks (governance_approved.yaml, delivery_reporting.yaml) are not affected — those product IDs are seeded into the seller's test environment before the scenario runs. diff --git a/.changeset/fix-migration-433-dup.md b/.changeset/fix-migration-433-dup.md deleted file mode 100644 index b32b68f6e2..0000000000 --- a/.changeset/fix-migration-433-dup.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Renumbers `433_catalog_adagents_lookup_index.sql` to `434_*` to break the duplicate-migration-433 deadlock on main. PRs #3235 and #3244 both landed migration 433 simultaneously, blocking every open PR at the duplicate-check and migration-build CI gates. Both files are kept (no SQL change) — only the filename prefix is bumped on the second-landed file. Migrations apply cleanly in 432 → 433 → 434 order locally; no code references either filename so this is a pure file rename. diff --git a/.changeset/fix-nonstream-empty-guard.md b/.changeset/fix-nonstream-empty-guard.md deleted file mode 100644 index 9d48c30110..0000000000 --- a/.changeset/fix-nonstream-empty-guard.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Guard the non-streaming Addie response path against empty `slackText`. Same fix as #2947 applied to the non-streaming branch at `server/src/addie/bolt-app.ts:1689-1715`: if the model returns nothing (or validation/extraction produces empty text), send a plain apology via `say()` instead of a malformed Slack blocks payload with empty `mrkdwn` text. Closes #2951. diff --git a/.changeset/fix-policies-evaluated-description.md b/.changeset/fix-policies-evaluated-description.md deleted file mode 100644 index ca4e37370e..0000000000 --- a/.changeset/fix-policies-evaluated-description.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Clarify `policies_evaluated` description in `check-governance-response.json` and `get-plan-audit-logs-response.json`. The previous wording ("Registry policy IDs...") was incomplete and misleading: governance agents also record inline `policy_id`s from `custom_policies` in this field, and a consumer reading the description literally could write a parser that filters them out. The new wording names both sources. Both schemas carry `x-status: experimental`. Description-only clarification; no type, enum, or wire change. diff --git a/.changeset/fix-rate-limited-schema-title.md b/.changeset/fix-rate-limited-schema-title.md deleted file mode 100644 index bf62916928..0000000000 --- a/.changeset/fix-rate-limited-schema-title.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix `title` in `error-details/rate-limited.json` from `"RATE_LIMITED Details"` to `"Rate Limited Details"`. The JSON Schema `title` annotation is non-normative; no validation or wire-format change. This corrects the generated TypeScript type name from `RATE_LIMITEDDetails` to `RateLimitedDetails` in downstream codegen consumers (see adcp-client#942 for the SDK alias layer). diff --git a/.changeset/fix-registry-jwks-per-client.md b/.changeset/fix-registry-jwks-per-client.md deleted file mode 100644 index 8f16c2d5e4..0000000000 --- a/.changeset/fix-registry-jwks-per-client.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix OIDC JWT verification on `/api/registry/operator` and `/api/registry/agents` to pick the JWKS per-token from the `iss` claim instead of a single server-wide `WORKOS_CLIENT_ID`. The previous implementation built `https://api.workos.com/sso/jwks/`, which only serves keys for the server's own AuthKit app — third-party OAuth clients mint tokens signed with their own keys at `/sso/jwks/`, so verification silently failed and authenticated member JWTs still got `agents: []`. Now the JWT's `iss` claim is decoded unverified, the issuing `client_id` is extracted from its `/user_management/` suffix, and `jwtVerify` is called with `{ issuer }` pinned so the unverified decode can't redirect verification at an attacker-controlled JWKS. Adds warn-level logging for every failure mode. diff --git a/.changeset/fix-registry-oidc-jwt-auth.md b/.changeset/fix-registry-oidc-jwt-auth.md deleted file mode 100644 index 127d07b9cf..0000000000 --- a/.changeset/fix-registry-oidc-jwt-auth.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix `/api/registry/operator` and `/api/registry/agents` to recognize WorkOS OIDC access tokens in the `Authorization: Bearer` header, not just API keys and sealed sessions. Previously, authenticated OAuth clients silently fell through to public-only visibility — `scope3.com` returned `agents: []` for a valid member JWT while returning 16 agents for an `sk_*` API key from the same org. The shared `resolveCallerOrgId` helper now extracts `org_id` from the verified JWT (via WorkOS JWKS) before falling back to API key or session-user lookup. diff --git a/.changeset/fix-registry-operator-auth.md b/.changeset/fix-registry-operator-auth.md deleted file mode 100644 index 8a0e5bd335..0000000000 --- a/.changeset/fix-registry-operator-auth.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix `GET /api/registry/operator` to honor caller auth. The endpoint now accepts a WorkOS Bearer API key (or session cookie) and tiers agent visibility accordingly: `public` always, `members_only` when the caller's org has API-access tier, and `private` when the caller's org owns the profile. Previously the route hardcoded `visibility === 'public'` and ignored the Authorization header, so members_only/private agents were never returned. diff --git a/.changeset/fix-registry-review-json-fences.md b/.changeset/fix-registry-review-json-fences.md deleted file mode 100644 index 578a394d08..0000000000 --- a/.changeset/fix-registry-review-json-fences.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Fix Addie registry review JSON parse crash when the model wraps its verdict in -markdown code fences (e.g. ```` ```json ... ``` ````). The review now tolerates -fenced responses and falls back to extracting the first `{...}` block from the -text, so community edits no longer trigger the `Unexpected token '`'` error. diff --git a/.changeset/fix-replay-store-3338.md b/.changeset/fix-replay-store-3338.md deleted file mode 100644 index 8648fababa..0000000000 --- a/.changeset/fix-replay-store-3338.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -fix(training-agent): isolate replay store on /mcp-strict (closes #3338) - -The post-5.21.1 grader run surfaced `neg/016-replayed-nonce` accepting both submissions of the same `(keyid, nonce)` pair on `/mcp-strict` — a MUST-level RFC 9421 §3.3.2 violation. - -Root cause: `/mcp-strict` was using the same `lazySigningAuth()` singleton as `/mcp`, so they shared one `InMemoryReplayStore`. The shared singleton was also bound to the *default* capability (`required_for: []`) rather than the strict one (`required_for: ['create_media_buy']`) — a quieter conformance gap that compounded with the replay leak. - -Adds `buildStrictRequestSigningAuthenticator()` in `request-signing.ts` (parallel to the existing strict-required and strict-forbidden builders), and a matching `lazyStrictSigningAuth()` in `index.ts`. `/mcp-strict` now binds to its own replay store and the strict capability. - -Un-skips the regression test at `server/tests/integration/training-agent-strict.test.ts:124` (was skipped per #3080 with a stale assertion); the message regex is updated to match the SDK's current `Signature required for create_media_buy.` text. - -The triage's bug #1 ("bearer evaluated before signing") didn't reproduce against `@adcp/client@5.21.1` — `requireSignatureWhenPresent` already implements presence-first ordering. The per-route signing-auth instances eliminate any leftover bypass surface regardless. diff --git a/.changeset/fix-revenue-tracking-stripe-mock.md b/.changeset/fix-revenue-tracking-stripe-mock.md deleted file mode 100644 index b26f715c79..0000000000 --- a/.changeset/fix-revenue-tracking-stripe-mock.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Un-skip revenue-tracking integration tests by adding a fuller stripe-client mock. - -Adds `vi.hoisted` + `vi.mock('stripe-client')` following the pattern from -`admin-sync-revenue-backfill.test.ts` (#3313). The webhook route guard requires -both `stripe` and `STRIPE_WEBHOOK_SECRET` to be non-null; the mock satisfies both. -Removes stale dead-code in `beforeAll` that attempted to patch the live stripe object -at runtime (which was a no-op since `stripe` was `null` in tests). - -Refs #3318. Part of #3289 integration-test restoration. diff --git a/.changeset/fix-sales-social-orphan-event-source.md b/.changeset/fix-sales-social-orphan-event-source.md deleted file mode 100644 index b2024d2147..0000000000 --- a/.changeset/fix-sales-social-orphan-event-source.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -fix(storyboards): seed event_source_id via sync_event_sources in sales_social - -The sales_social specialism storyboard referenced `event_source_id: "acmeoutdoor_website"` in a -log_event step without a preceding sync_event_sources call, producing an orphan reference that -seller agents validating event-source existence would reject with EVENT_SOURCE_NOT_FOUND. Adds an -`event_setup` phase that registers `acmeoutdoor_website` before `event_logging`, and adds -`sync_event_sources` to `required_tools` — matching the sales_catalog_driven pattern. Closes #2909. diff --git a/.changeset/fix-schema-link-pinning-and-addie-registry.md b/.changeset/fix-schema-link-pinning-and-addie-registry.md deleted file mode 100644 index ecdfe086dd..0000000000 --- a/.changeset/fix-schema-link-pinning-and-addie-registry.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Pin all `docs/` schema links from `/schemas/latest/` to `/schemas/v3/` so the current stable docs stay aligned with v3 schemas after v4 development begins. Addie's schema tools now fetch the live `index.json` registry instead of a stale hardcoded list — fuzzy match and error messages cover every schema the spec ships, so guesses like `core/get-capabilities-response.json` resolve to `protocol/get-adcp-capabilities-response.json` instead of 404ing. Dist-docs rewriter also pins major-version aliases to the exact release version on snapshot, and `check-schema-links.yml` now flags any new `/schemas/latest/` references in `docs/`. diff --git a/.changeset/fix-schema-validation-expected-text.md b/.changeset/fix-schema-validation-expected-text.md deleted file mode 100644 index c052e8a134..0000000000 --- a/.changeset/fix-schema-validation-expected-text.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -docs(compliance): list all eight required product fields in schema-validation storyboard - -The `get_products_schema` step's `expected` description listed only four of the eight required fields in `core/product.json`, leading external implementors to believe their responses were well-formed when they were missing `description`, `publisher_properties`, and `reporting_capabilities`. Updated to enumerate all eight required fields and note that `reporting_capabilities` must be a fully-formed object with its own required sub-fields, not an empty object. No change to validation logic or schema. diff --git a/.changeset/fix-show-on-registry-toggle.md b/.changeset/fix-show-on-registry-toggle.md deleted file mode 100644 index ac871d299c..0000000000 --- a/.changeset/fix-show-on-registry-toggle.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix the "Show on registry" toggle on the Agents dashboard. The `/api/registry/agents/{url}/compliance` endpoint now returns `compliance_opt_out` so the checkbox reflects the persisted state on page refresh. The toggle's change handler is also scoped to a dedicated `registry-visibility-toggle` class so clicking "Pause automated checks" no longer accidentally flips registry visibility, and the card re-renders after a successful change so the status label updates immediately. diff --git a/.changeset/fix-sidecar-availability-caveat.md b/.changeset/fix-sidecar-availability-caveat.md deleted file mode 100644 index 874662de2a..0000000000 --- a/.changeset/fix-sidecar-availability-caveat.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -docs(building): caveat sidecar availability in schemas-and-sdks.mdx tarball table - -Adds a one-line availability note to the .tgz.sig and .tgz.crt table rows clarifying -that sidecars are produced only by the cosign step in release.yml on changeset version bumps -and may be transiently absent during out-of-band republishes. Also extends the existing -checksum-only paragraph to cover this transient case alongside pre-signing releases. diff --git a/.changeset/fix-skipped-integration-tests-3321-3323.md b/.changeset/fix-skipped-integration-tests-3321-3323.md deleted file mode 100644 index 72368e919f..0000000000 --- a/.changeset/fix-skipped-integration-tests-3321-3323.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Un-skip 10 integration tests across 4 files (#3321, #3322, #3323): - -- join-request-approval.test.ts: add @workos-inc/node class mock + WorkOS env var priming so handlers' new WorkOS() calls see test mocks; un-skip 5 tests -- personal-workspace-restrictions.test.ts: same fix; un-skip 4 tests -- self-service-delete.test.ts: update 404→403 assertion (membership check precedes existence check by design); fix active-subscription test to seed subscription_status='active' in DB rather than mocking stripe-client import -- admin-endpoints.test.ts: same DB-seed fix for active-subscription test diff --git a/.changeset/fix-storyboard-handler-tightening.md b/.changeset/fix-storyboard-handler-tightening.md deleted file mode 100644 index 9f8c29aaab..0000000000 --- a/.changeset/fix-storyboard-handler-tightening.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Tighten storyboard handlers across both legacy and framework dispatch -to close five shared step failures. Lift (against overlaid compliance -cache): legacy 44 → 50 clean, framework 38 → 44 clean. - -Handler changes (`server/src/training-agent/`): - -- `comply_test_controller.forceCreativeStatus` allows the - `approved → rejected` transition so `force_creative_rejected` can - test post-approval brand-safety rejection flows. -- `handleListCreatives` always emits `name` and - `format_id.agent_url`, falling back to `creative_id` and the agent's - own URL when absent. Keeps response-schema validation green when a - sync_creatives payload omits either. -- `handleCalibrateContent` scans the artifact text for must-rule - keywords (violent, gambling, alcohol, stock photo, missing alt - text) and returns `verdict: fail` when a match hits. Prior behavior - returned pass unconditionally. -- `handleLogEvent` / `handleProvidePerformanceFeedback` fall back to a - global scan when the request-level session key misses. The SDK - strips `account` against each tool's published schema, so these - tools land on `open:default` while `sync_event_sources` / - `create_media_buy` wrote under `open:`. - -Spec-side tightening (`static/compliance/source/`): - -- `protocols/brand/index.yaml` drops two validation checks the SDK - doesn't implement (`array_contains`, `is_error`) in favor of - `field_present` + `error_code` with allowed values. -- `specialisms/brand-rights/index.yaml` captures - `rights.0.pricing_options.0.pricing_option_id` as - `$context.pricing_option_id` and references it in `acquire_rights`, - replacing the hard-coded `standard_monthly` (no agent offering - exposed that id). diff --git a/.changeset/fix-strict-rawbody-resolve-operation.md b/.changeset/fix-strict-rawbody-resolve-operation.md deleted file mode 100644 index 58fbdc48aa..0000000000 --- a/.changeset/fix-strict-rawbody-resolve-operation.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -fix(training-agent): restore 401 for unsigned create_media_buy on /mcp-strict - -`resolveOperation` in `buildStrictAuthenticator` read `req.rawBody` exclusively and silently returned `undefined` when it was absent. In test harnesses that mount `express.json()` without the production `verify` callback, `rawBody` is never populated, so the `required_for: ['create_media_buy']` gate never fired and unsigned requests authenticated via bearer → 200. - -**Two-part fix:** - -1. `server/src/training-agent/index.ts` — `buildStrictAuthenticator`'s `resolveOperation` now falls back to `req.body` (already parsed by express.json) when `rawBody` is absent. Safe because `resolveOperation` drives only the `required_for` routing decision, not cryptographic verification — the signing authenticator's own `resolveOperation` deliberately remains `rawBody`-only. - -2. `server/src/training-agent/request-signing.ts` — `enforceSigningWhenWebhookAuthPresent` applies the same fallback for the webhook-authentication downgrade-resistance check so a test harness without the `verify` callback still catches `push_notification_config.authentication` payloads. - -3. `server/tests/integration/training-agent-strict.test.ts` — `beforeAll` now mirrors production `http.ts` by adding the `verify` callback to `express.json`, populating `req.rawBody` for all test requests. - -Non-protocol server change; `--empty` changeset per playbook. - -Closes #3080. diff --git a/.changeset/fix-tool-set-always-available-overlap.md b/.changeset/fix-tool-set-always-available-overlap.md deleted file mode 100644 index 41a35d46cd..0000000000 --- a/.changeset/fix-tool-set-always-available-overlap.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -fix(addie): remove always-available tools from set arrays to prevent hallucinations - -Audits every TOOL_SETS[x].tools array against ALWAYS_AVAILABLE_TOOLS and -ALWAYS_AVAILABLE_ADMIN_TOOLS, removing 9 duplicates that caused Sonnet to -hallucinate capability unavailability when a set was not routed (#2998): - -- Removed propose_content, get_my_content, set_outreach_preference from member.tools -- Removed list_pending_content, approve_content, reject_content from content.tools -- Removed get_github_issue from knowledge.tools -- Removed list_escalations, resolve_escalation from admin.tools - -Updated descriptions for member and content sets to not claim ownership of -always-available capabilities. - -Expanded ALWAYS_AVAILABLE_BLURBS with 7 new entries (get_escalation_status, -propose_content, get_my_content, list_pending_content, approve_content, -reject_content, set_outreach_preference) so the correction section in the -unavailable-sets hint explicitly covers the reclaimed tools. - -Added a tools-array invariant test that will fail if a future edit re-introduces -a duplicate, unioning both ALWAYS_AVAILABLE_TOOLS and ALWAYS_AVAILABLE_ADMIN_TOOLS. diff --git a/.changeset/fix-webhook-agreement-resolution.md b/.changeset/fix-webhook-agreement-resolution.md deleted file mode 100644 index 4d3a04cf39..0000000000 --- a/.changeset/fix-webhook-agreement-resolution.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix silent-failure bug in Stripe `customer.subscription.created` webhook that left paid members with no `user_agreement_acceptances` row. The old path resolved the WorkOS user solely via Stripe customer email; when emails didn't match (different from WorkOS, alias domain, case drift), the lookup returned empty, the `if (workosUser)` block was skipped, a single error was logged, and the webhook returned 200 — dashboard showed missing agreement despite active subscription. New path resolves via `subscription.metadata.workos_user_id` → customer `metadata.workos_user_id` → email (fallback), propagates `workos_user_id` through checkout-session `subscription_data.metadata`, and records the org-level agreement regardless of user resolution. Every candidate user is verified as a member of the subscribing org before being accepted — stale metadata pointing to an offboarded or wrong-org user is rejected rather than silently misattributing the agreement. Customer `deleted` case is narrowed explicitly. When no source resolves or the user-level DB insert fails, the webhook now calls `notifySystemError` and logs `needs_manual_reconciliation: true` without throwing — the rest of the webhook (subscription DB sync, tier detection, directory activation) continues to run so a transient error on the user-level insert doesn't leave the org stuck. Subscription-metadata stamping happens best-effort after attestation. diff --git a/.changeset/fix-workos-mock-unskip-user-context-tests.md b/.changeset/fix-workos-mock-unskip-user-context-tests.md deleted file mode 100644 index 8e6f043be3..0000000000 --- a/.changeset/fix-workos-mock-unskip-user-context-tests.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix WorkOS mock shape in user-context integration tests and narrow `getWebMemberContext` error handling so non-existent users return 404 instead of empty 200, while transient WorkOS errors leave authenticated sessions unaffected. Unskips all 10 tests in `user-context.test.ts`. diff --git a/.changeset/fly-worker-auto-start.md b/.changeset/fly-worker-auto-start.md deleted file mode 100644 index 760dcbd130..0000000000 --- a/.changeset/fly-worker-auto-start.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Fix Fly worker process group lifecycle — rolling deploys were leaving worker -machines in `stopped` state forever because the worker pgroup had no -auto_start_machines path (only `[http_service]` had it, scoped to web). -Replace the bare `[[checks]]` block with a `[[services]]` block targeting -`processes=["worker"]` with `auto_start_machines=true` and -`min_machines_running=2`. No external ports — workers stay private; the -internal_port is only used by the http_check. - -Symptom this fixes: the periodic catalog crawler (every 30 min) and the -buying-agents crawler (every 6h) only run on workers. Workers being -stopped meant the property-registry-unification chain (#3274/#3314/#3312/#3352) -was unexercised in production until manual `/api/registry/crawl-request` -calls fired it. diff --git a/.changeset/force-task-completion-roundtrip.md b/.changeset/force-task-completion-roundtrip.md deleted file mode 100644 index a1728dc323..0000000000 --- a/.changeset/force-task-completion-roundtrip.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -patch: force_task_completion controller scenario - -Adds the controller primitive needed to test the async `create_media_buy` submitted → completed roundtrip end-to-end. Companion to `force_create_media_buy_arm` (#3104): that scenario drives the seller into the submitted envelope; this one closes the loop by transitioning the task store entry from `submitted` to `completed` and stamping the registered result. The buyer observes the result via webhook delivery to `push_notification_config.url` (the canonical 3.0 path); a typed result projection on the `tasks/get` polling response is tracked for 3.1 (#3123). - -**Scenario semantics.** `{ scenario: 'force_task_completion', params: { task_id, result } }`. The seller stores `result` (validated against `async-response-data.json`) against `task_id` and delivers it verbatim to the buyer's webhook. Returns the standard `StateTransitionSuccess` shape with `previous_state: 'submitted'` / `current_state: 'completed'`. Sellers MUST emit `NOT_FOUND` for unknown task_ids and `INVALID_TRANSITION` if the task is already terminal. - -**Files.** -- `static/schemas/source/compliance/comply-test-controller-request.json` — added to enum, new `result` param ($ref `async-response-data.json`), conditional `if/then` requiring `task_id` and `result`. -- `static/schemas/source/compliance/comply-test-controller-response.json` — added to `list_scenarios.scenarios` enum (sellers advertising support don't schema-fail their own list response). Reuses the existing `StateTransitionSuccess` branch. -- `docs/building/implementation/comply-test-controller.mdx` — new `### force_task_completion` section + inline params doc + tool-definition enum + example. - -**Storyboard extension lives in the follow-up.** The `create_media_buy_async` storyboard is unchanged in this PR. Extending it to exercise the new scenario via `tasks/get` polling lands together with the training-agent's implementation of `force_task_completion`, mirroring the #3104 → #3115 pattern. That keeps the runner from grading the storyboard as `failed` during the window where the controller scenario exists in spec but no reference seller implements it yet (the half-implemented case can't gracefully degrade to `not_applicable` because the storyboard's earlier phases already pass). - -Why patch: new sandbox-only controller scenario, opt-in via `UNKNOWN_SCENARIO` grading. No on-wire obligation change for sellers that don't implement the controller — the scenario only binds sellers advertising `force_task_completion` in `list_scenarios`. diff --git a/.changeset/format-id-docs-clarification.md b/.changeset/format-id-docs-clarification.md deleted file mode 100644 index d83c82801d..0000000000 --- a/.changeset/format-id-docs-clarification.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Clarify format_id (structured object reference) vs format (full definition object) naming convention. - -Adds normative docs, schema description updates, and llms.txt entries to prevent two recurring -implementation errors: setting format_id to a plain string, and putting a format_id object into a -format definition slot. No protocol wire change — purely additive docs and schema descriptions. diff --git a/.changeset/framework-replayed-noop-cleanup.md b/.changeset/framework-replayed-noop-cleanup.md deleted file mode 100644 index b52a61a721..0000000000 --- a/.changeset/framework-replayed-noop-cleanup.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -`framework-server.ts` `toAdaptedResponse`: stop passing -`replayed: false` to `wrapEnvelope`. Per `protocol-envelope.json`, the -field MUST be omitted on fresh execution to avoid polluting task -payloads under `additionalProperties: false` response schemas. The -framework stamps `replayed: true` only on idempotency replays — which -is already correct. - -Previously we emitted `replayed: false` on every fresh response -(`wrapEnvelope`'s success path writes any key passed in opts; the -strip-on-false allowlist only applies to `adcp_error` envelopes). Now -matches the envelope spec exactly. The lingering -`idempotency/create_media_buy_initial` storyboard failure was a -storyboard assertion bug — `field_value: replayed [false]` fired when -the field was correctly absent on fresh exec. Fixed in adcp-client#859 -and mirrored into this repo's compliance source overlay. diff --git a/.changeset/fvora-lint-and-fresh-key.md b/.changeset/fvora-lint-and-fresh-key.md deleted file mode 100644 index 51e560a7a5..0000000000 --- a/.changeset/fvora-lint-and-fresh-key.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -fix(compliance): add field_value_or_absent to contradiction-lint allowlist and cover fresh-key idempotency step - -Follow-up to #3032 (documented the check) and #3034 (restored the initial-step `replayed` assertion). Two small gaps remained, both surfaced when triaging the superseded PR #3037: - -1. **`scripts/lint-storyboard-contradictions.cjs`** — `hasPositiveAssertion` listed `field_present`, `field_value`, `response_schema`, `http_status`, `http_status_in` but not `field_value_or_absent`. Any storyboard using `field_value_or_absent` as its only positive assertion was misclassified as `unspecified`. Added to the allowlist so the linter correctly classifies such steps as `success`. - -2. **`static/compliance/source/universal/idempotency.yaml`** — the `create_media_buy_fresh_key` step is a non-replay call, so the same `replayed` tolerance that #3034 restored on `create_media_buy_initial` applies here: `replayed` MAY be omitted, but if present MUST NOT be `true`. Added the symmetric `field_value_or_absent [false]` assertion so fresh-key coverage is not weaker than initial-call coverage. - -Non-breaking: both additions are purely additive (new allowlist entry + new optional assertion on a step that was already asserting other fields). Agents that omit `replayed` on fresh execution pass both (spec-correct). Only agents setting `replayed: true` on a fresh call fail — already a spec violation per PR #3013. diff --git a/.changeset/gcp-kms-request-signing.md b/.changeset/gcp-kms-request-signing.md deleted file mode 100644 index 8157ce80f3..0000000000 --- a/.changeset/gcp-kms-request-signing.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -feat(addie): GCP KMS-backed Ed25519 signing for outbound AdCP requests - -Addie's outbound AdCP calls (`AdCPClient.executeTask`) now attach an RFC 9421 -request-signing block backed by a GCP KMS Ed25519 key when `GCP_SA_JSON` and -`GCP_KMS_KEY_VERSION` are set. Private key material never enters process -memory; signing routes through the `SigningProvider` interface added in -`@adcp/client@5.20.0`. - -Verifiers fetch the public key from `${BASE_URL}/.well-known/jwks.json` -(kid: `aao-signing-2026-04`). The committed `expected-public-key.ts` is the -single source of truth for both the published JWKS and the boot-time -tripwire that asserts the KMS-returned public key matches the repo — -silent key swaps in GCP fail loudly rather than producing signatures -verifiers reject. - -Webhook signing in the training agent still uses an in-process JWK; lifting -that to KMS is a follow-up that needs a `SigningProvider` integration in -`@adcp/client`'s `createWebhookEmitter`. diff --git a/.changeset/gemini-illustration-cap-ops-metric.md b/.changeset/gemini-illustration-cap-ops-metric.md deleted file mode 100644 index 10b177b848..0000000000 --- a/.changeset/gemini-illustration-cap-ops-metric.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Surface Gemini illustration-generation activity against the workspace -cap on the admin dashboard (closes #2796 ops metric requirement). - -The workspace cap (50/day) and per-user co-author aggregation for -`generate_perspective_illustration` were already enforced in -`tool-rate-limiter.ts` (`WORKSPACE_CAPS`) and -`illustration-db.ts:countMonthlyGenerations`. What was missing from -#2796's acceptance criteria was ops visibility — a dashboard metric -for "illustration generations today" so operators can see cap status -without reading Gemini's billing console. - -Changes: -- Exported `WORKSPACE_CAPS` from `tool-rate-limiter.ts` so the stats - endpoint reads the configured cap value instead of hard-coding it. -- `/api/admin/stats` now includes `illustrations_generated_24h`, - `illustrations_generated_7d`, `illustrations_cap_24h`, and - `illustrations_cap_remaining` fields. -- Added a card to `admin.html`: "Gemini Illustrations (24h cost cap)" - rendering generated / cap / remaining / 7d trend. - -No new enforcement — this is pure visibility over the already-active -cap. diff --git a/.changeset/get-account-user-fallback.md b/.changeset/get-account-user-fallback.md deleted file mode 100644 index bdb36af6cf..0000000000 --- a/.changeset/get-account-user-fallback.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -`get_account` now falls back to the `users` table when no organization matches the query. Inbound website signups don't create an org row (the user self-serves during onboarding), so previously asking Addie "who signed up from ?" would return "no record" even when the person was already in `users`. The fallback surfaces those signups, separates orphan signups (no org yet) from users already in another org, and points at `add_prospect` as the next step. diff --git a/.changeset/get-media-buys-account-ownership-scope.md b/.changeset/get-media-buys-account-ownership-scope.md deleted file mode 100644 index d29e0ba992..0000000000 --- a/.changeset/get-media-buys-account-ownership-scope.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -spec(media-buy): `get_media_buys` MUST return all account-owned buys regardless of creation surface - -Tightens the scope of `get_media_buys`, `get_media_buy_delivery`, and `update_media_buy` to be bounded by account ownership, not by the surface through which a buy was created. A sales agent MUST NOT partition its inventory into "AdCP-created" and "non-AdCP" subsets for account-scoped tasks, and MUST NOT refuse reporting or updates on the basis that a buy was booked directly in the ad server, via legacy APIs, or via manual trafficking. - -Closes #2963. Rationale: - -- **Adoption**: enterprises with large existing ad-server state (10K+ GAM campaigns) can't rebuild from zero through AdCP to adopt the protocol. -- **Attestation**: brownfield Tier-2 conformance (#2965) depends on the compliance engine being able to `get_media_buys` → discover a live campaign → `update_media_buy` with a verification `reporting_webhook`. If pre-existing campaigns are invisible, that path collapses. -- **Honesty**: AdCP is a protocol onto the seller's ad operations, not a shadow ledger beside them. - -Normative changes: - -- `docs/media-buy/task-reference/get_media_buys.mdx`: new "Scope of Results" section. -- `docs/media-buy/task-reference/update_media_buy.mdx`: new "Scope" section; updates operate on any `media_buy_id` returned by `get_media_buys`. -- `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: new "Scope" section; delivery reporting covers any returned buy. -- `docs/media-buy/specification.mdx`: new "Account Ownership vs. Creation Surface" core concept; cross-referenced from the `get_media_buys`, `update_media_buy`, and `get_media_buy_delivery` requirements lists. - -Business constraints on specific operations remain expressible via `valid_actions` — the seller omits the action rather than hiding the buy. diff --git a/.changeset/get-media-buys-pagination-integrity.md b/.changeset/get-media-buys-pagination-integrity.md deleted file mode 100644 index 2f4bc3c59d..0000000000 --- a/.changeset/get-media-buys-pagination-integrity.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -feat(compliance): pagination integrity for get_media_buys — handler fix + storyboard - -Adds cursor-based pagination to `handleGetMediaBuys` in the training agent and a new -`get-media-buys-pagination-integrity.yaml` conformance storyboard, completing the fourth -entry in the rolling pagination conformance series (#3095 list_creatives, #3100 total_count -honesty, #3109 get_signals). - -Handler changes: reads `pagination.max_results` (default 50, cap 100), decodes a -namespaced `mb:offset:` cursor (base64url), slices the post-filter buy set, and emits -`pagination: { has_more, total_count, cursor? }`. Pagination is skipped when `media_buy_ids` -is provided — that is a direct lookup, not a paginated broad-scope query per the request -schema semantics. Malformed cursors return `INVALID_REQUEST`. The `mb:` namespace prefix -ensures a `get_media_buys` cursor is rejected by `list_creatives` and vice versa, preventing -silent wrong-offset reads if a caller passes the wrong token. - -Storyboard: seeds three active media buys via `controller_seeding`, walks first page -(max_results=2, asserts has_more=true + cursor present) and terminal page (asserts -has_more=false + cursor absent). No `query_summary` assertions — the response schema does not -define that field for `get_media_buys`. - -No protocol schema changes — `pagination` was already an optional field in -`get-media-buys-response.json`. Existing callers unaffected: `media_buy_ids` lookups still -return all requested buys without a pagination envelope; broad-scope queries with small -fixture sets (≤50) return all results in a single page with `has_more: false`. diff --git a/.changeset/get-signals-pagination-integrity.md b/.changeset/get-signals-pagination-integrity.md deleted file mode 100644 index a61ca931d7..0000000000 --- a/.changeset/get-signals-pagination-integrity.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Adds `get_signals_pagination_integrity` — the second universal pagination-integrity storyboard, mirroring the cursor↔has_more invariant that gates `list_creatives` (#3095/#3100) onto `get_signals`. Sends a broad `signal_spec` ("audience") with `pagination.max_results: 1` and asserts the first page is non-terminal (`has_more=true` with cursor present). Page 2 follows the captured cursor and asserts schema conformance — the catalog size depends on the agent so the terminal state is not pinned, with the static lint covering cursor invariants on any sample fixture. - -Fixes the training agent's `get_signals` to honor `pagination.max_results` / `pagination.cursor` and emit a proper pagination block. Previously it capped internally at `MAX_SIGNAL_RESULTS=10` and returned no pagination field — exactly the dishonest shape this storyboard exists to catch. Negative-test verified: flipping the agent back to `has_more: false` fires the page-1 assertion with `Expected true, got false`. - -Generalizes the cursor codec from #3095 (`encodeCreativeCursor` / `decodeCreativeCursor`) into a `kind`-prefixed pair (`encodeOffsetCursor`, `decodeOffsetCursor`) so a list_creatives cursor can't decode to a meaningful offset on a different list endpoint. Existing list_creatives behavior preserved. diff --git a/.changeset/governance-audit-trail-doc-and-addie-tools.md b/.changeset/governance-audit-trail-doc-and-addie-tools.md deleted file mode 100644 index 3eec5a40b0..0000000000 --- a/.changeset/governance-audit-trail-doc-and-addie-tools.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `docs/governance/campaign/audit-trail.mdx` documenting the internal-vs-shareable view split for `get_plan_audit_logs`, with field-by-field tagging and worked examples (clean buy, denied seller, Annex III coaching, three-mode comparison) generated by `scripts/gen-governance-audit-examples.ts`. Wire anonymous web-chat callers (Addie) to receive `search_docs`/`get_doc`/`search_repos`/`search_resources`/`get_recent_news` so spec questions get grounded answers instead of in-prompt speculation. Add a "Tool Unavailable Is Not 'No Result'" constraint in Addie's rules to distinguish tool-error from empty-result and avoid retry loops. Extend `skills/adcp-governance/SKILL.md` with the campaign-governance task surface (sync_plans, check_governance, report_plan_outcome, get_plan_audit_logs), three operator-facing invariants, and the `inline-policies-cannot-relax-registry` rule. diff --git a/.changeset/governance-list-pagination.md b/.changeset/governance-list-pagination.md deleted file mode 100644 index 10f0b05b44..0000000000 --- a/.changeset/governance-list-pagination.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -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/.changeset/governance-mode-experimental-status-and-per-check-clarification.md b/.changeset/governance-mode-experimental-status-and-per-check-clarification.md deleted file mode 100644 index 194d8915f5..0000000000 --- a/.changeset/governance-mode-experimental-status-and-per-check-clarification.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Mark `governance-mode.json` enum as `x-status: experimental` and clarify the per-check semantics of the audit-entry `mode` field. - -The enum is referenced exclusively from experimental schemas (`check-governance-response.json`, `get-plan-audit-logs-response.json` `entries[]`); annotating it explicitly prevents the enum from being treated as stable while its consumers are still experimental. The `entries[].mode` description is tightened to clarify that the field reflects the mode active for that specific check, distinct from a future `governed_actions[].mode` (which would describe the action's current mode and may differ if the plan has been re-synced since). diff --git a/.changeset/hoist-call-adcp-agent-skill.md b/.changeset/hoist-call-adcp-agent-skill.md deleted file mode 100644 index d59a6e94e7..0000000000 --- a/.changeset/hoist-call-adcp-agent-skill.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Hoist the `call-adcp-agent` skill from `@adcp/client@5.17.0` into the canonical `skills/` directory of the adcp main repo and make it the cross-SDK source of truth. - -- New `skills/call-adcp-agent/SKILL.md` — agent-facing buyer-side wire contract (idempotency replay, account `oneOf` variants, async `status:'submitted'` polling, `adcp_error.issues[]` recovery). Frontmatter declares `adcp_version: "3.x"` and `type: cross-cutting` so the skill loader and SDK consumers can pin compatibility. -- Per-protocol skills (`adcp-{brand,creative,governance,media-buy,si,signals}`) gain a one-line pointer at the top deferring cross-cutting rules to `call-adcp-agent`. -- New `docs/protocol/calling-an-agent.mdx` — human-readable canonical narrative form, registered in both `docs.json` nav configs and cited from the SKILL.md. -- `scripts/build-protocol-tarball.cjs` bundles `skills/` into the published protocol tarball at `adcontextprotocol.org/protocol/.tgz`. SDKs (`@adcp/client`, `adcp` Python, `adcp-go`) already pull this tarball at sync time for schemas and compliance — they get skills for free, version-pinned, Sigstore-verified, no manual copy. `manifest.json.contents.skills` is now an enumerated list so SDK sync scripts can pick out skill names without re-walking the tree. -- `server/src/addie/mcp/adcp-tools.ts` — `loadSkillDocs` is now frontmatter-driven (filters by `name: adcp-*` or `type: cross-cutting`) instead of hardcoding directory names. `resolveSkillsDir` is exported and tries multiple candidate paths so the loader works in dev (`server/src/`), production (`dist/`), and CWD layouts. Cross-cutting rules are consolidated into a single `BUYER_RULES_PREAMBLE` injected at the top of every search response. `call_adcp_task`'s tool description is trimmed to the two non-negotiable rules (`idempotency_key` replay, `issues[].variants[]` recovery). Failure-path output appends a recovery hint pointing at `issues[]`. -- `Dockerfile` copies `/app/skills` into the runtime image and `docker-compose.yml` bind-mounts it for dev iteration. (Previously absent — the skill loader silently returned empty for all `adcp-*` skills in production.) -- `tests/addie/buyer-skill-wiring.test.ts` — 12 new tests locking down the wiring (skill content, frontmatter pin, `resolveSkillsDir` resolution, search preamble injection, tool description shape). diff --git a/.changeset/hoist-duplicate-inline-enums.md b/.changeset/hoist-duplicate-inline-enums.md deleted file mode 100644 index 0ded89e84f..0000000000 --- a/.changeset/hoist-duplicate-inline-enums.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -feat(schema): hoist 4 duplicate inline enum literal sets into shared `enums/` definitions (closes #3144) - -Several inline string-literal unions in the AdCP source schemas had byte-identical value sets across multiple parent schemas but no shared `$ref`, causing the TypeScript SDK to emit per-parent duplicate exports (`Account_PaymentTermsValues`, `GetAccountFinancialsSuccess_PaymentTermsValues`, etc.) when a single canonical `PaymentTermsValues` is what consumers expect. - -**New shared enum files added** (4 new `$id`-bearing schemas in `static/schemas/source/enums/`): - -- `payment-terms.json` — `["net_15","net_30","net_45","net_60","net_90","prepay"]` -- `audio-channel-layout.json` — `["mono","stereo","5.1","7.1"]` -- `media-buy-valid-action.json` — `["pause","resume","cancel","update_budget","update_dates","update_packages","add_packages","sync_creatives"]` -- `rights-billing-period.json` — `["daily","weekly","monthly","quarterly","annual","one_time"]` - -**Schemas updated to use `$ref`** (10 files; wire format unchanged in all cases): - -- `core/account.json`, `account/sync-accounts-request.json`, `account/sync-accounts-response.json`, `account/get-account-financials-response.json` → `payment_terms` now refs `enums/payment-terms.json` -- `core/assets/audio-asset.json`, `core/assets/video-asset.json` → `channels`/`audio_channels` now ref `enums/audio-channel-layout.json` -- `media-buy/create-media-buy-response.json`, `media-buy/update-media-buy-response.json` → `valid_actions` items now ref `enums/media-buy-valid-action.json` -- `brand/rights-terms.json`, `brand/rights-pricing-option.json` → `period` now refs `enums/rights-billing-period.json` - -**Not changed:** `core/insertion-order.json` `payment_terms` (`["net_30","net_60","net_90","prepaid","due_on_receipt"]` — different set, kept inline). - -Non-breaking: replacing inline `{"type":"string","enum":[...]}` with a `$ref` to an equivalent standalone schema produces an identical JSON Schema subgraph; all existing validators behave identically. Source-schema refactor only; bundled wire format is unchanged — patch-eligible. - -After a `npm run sync-schemas` in `adcp-client`, the SDK will emit single canonical exports (`PaymentTermsValues`, `AudioChannelLayoutValues`, etc.) and should ship deprecated re-export aliases for any per-parent names that were in a published release. diff --git a/.changeset/hoist-inline-enum-duplicates-tranche-2.md b/.changeset/hoist-inline-enum-duplicates-tranche-2.md deleted file mode 100644 index 446f418eaa..0000000000 --- a/.changeset/hoist-inline-enum-duplicates-tranche-2.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Hoist 13 duplicate inline enum sets into shared `enums/` definitions (follow-up to #3148). - -Adds `match-type`, `collection-kind`, `frame-rate-type`, `scan-type`, `gop-type`, `moov-atom-position`, `binary-verdict`, `account-scope`, `governance-decision`, `billing-party`, `feature-check-status`, `snapshot-unavailable-reason`, and `travel-time-unit` as standalone `$id`-bearing enum files. Updates 21 source schemas to `$ref` these files instead of repeating the inline definitions. Source-schema refactor only; bundled wire format is unchanged in all cases. diff --git a/.changeset/index-adcp-go-repo.md b/.changeset/index-adcp-go-repo.md deleted file mode 100644 index eff5b3f9ba..0000000000 --- a/.changeset/index-adcp-go-repo.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Index the adcp-go repository in Addie's external-repos knowledge base. Adds the Go SDK to the Dockerfile clone script, the EXTERNAL_REPOS registry, and the search_repos tool metadata so Addie can answer questions about the Go SDK, TMP router, and Go types. diff --git a/.changeset/instrument-agent-test-runs.md b/.changeset/instrument-agent-test-runs.md deleted file mode 100644 index be09f3cfd5..0000000000 --- a/.changeset/instrument-agent-test-runs.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `agent_test_runs` table and wire-up instrumentation so Addie can surface staleness-aware agent-testing prompts (#2299 Stage 2). Adds `agent_testing` block to `MemberContext` with `last_test_at`, `last_outcome`, and `total_tests_30d` fields hydrated from the new table. Write calls added to `evaluate_agent_quality` and `run_storyboard` handlers. diff --git a/.changeset/intake-race-advisory-lock.md b/.changeset/intake-race-advisory-lock.md deleted file mode 100644 index 709a62b74e..0000000000 --- a/.changeset/intake-race-advisory-lock.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Close the millisecond intake race that the duplicate-subscription guard couldn't on its own (#3179). - -`POST /api/invoice-request` and `POST /api/invite/:token/accept` now wrap their `blockIfActiveSubscription` re-check + Stripe write in a per-org Postgres advisory lock (`pg_advisory_xact_lock(hashtext(orgId))`). Two concurrent intakes for the same org serialize: the first acquires the lock, runs the guard against live Stripe state, mints the subscription/invoice, commits, and releases; the second acquires the lock, runs the guard, sees the now-existing subscription, and returns 409. - -`POST /api/checkout-session` is intentionally not wrapped in the lock — Stripe Checkout sessions don't create the subscription until the user completes the hosted page, so two concurrent calls just produce two session URLs; the actual duplication can only happen if the user pays on both. That race is closed at the webhook layer (separate follow-up). diff --git a/.changeset/integrity-invariants-phase-1.md b/.changeset/integrity-invariants-phase-1.md deleted file mode 100644 index 38fac66285..0000000000 --- a/.changeset/integrity-invariants-phase-1.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Add Phase 1 of the cross-system integrity-invariants framework (#3181). - -Each invariant is a self-contained assertion about state across WorkOS, Stripe, and AAO Postgres. The runner orchestrates evaluation, isolates failures, and produces a per-violation report. New admin endpoints `GET /api/admin/integrity/check` (run all) and `GET /api/admin/integrity/check/:name` (run one) surface the report on demand. - -Phase 1 ships five invariants: - -- `stripe-customer-org-metadata-bidirectional` (critical) — every org's stripe_customer_id resolves to a Stripe customer whose metadata.workos_organization_id points back at the same org. Catches Triton-shape cross-contamination. -- `one-active-stripe-sub-per-org` (critical) — no org has more than one live (active/trialing/past_due) Stripe subscription. The literal Triton failure mode. -- `stripe-customer-resolves` (critical) — every referenced Stripe customer exists and is not deleted. -- `org-row-matches-live-stripe-sub` (warning) — when an org has both a stripe_subscription_id and a live status, the row's mirrored amount/lookup_key/status agree with Stripe. -- `workos-membership-row-exists-in-workos` (warning, sampled) — random sample of active organization_memberships rows are reflected in WorkOS. - -Phase 2 (separate PR) will add scheduled runs, persisted reports, Slack alerting, and an admin dashboard page. Phase 3+ extends to webhook-miss detection and inverse-walk orphan detection. diff --git a/.changeset/ipr-bot-setup-archive-note.md b/.changeset/ipr-bot-setup-archive-note.md deleted file mode 100644 index 5145c2eeaf..0000000000 --- a/.changeset/ipr-bot-setup-archive-note.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Drop `creative-agent` from the IPR Bot install list in `governance/ipr-bot-setup.md` — the repo is archived and read-only, so a PR workflow there has no effect. Updates the secret-scoping count from "five repos" to "four repos" to match. Active install scope is now `adcp`, `adcp-client`, `adcp-client-python`, `adcp-go`. diff --git a/.changeset/ipr-bot-verify-protection.md b/.changeset/ipr-bot-verify-protection.md deleted file mode 100644 index 5137dcbee1..0000000000 --- a/.changeset/ipr-bot-verify-protection.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add a "Verifying branch protection" section to `governance/ipr-bot-setup.md` documenting the smoke test for confirming the `IPR Policy / Signature` ruleset works (check posts, review enforcement holds, bot bypass still functions on adcp). diff --git a/.changeset/ipr-cross-repo-callable.md b/.changeset/ipr-cross-repo-callable.md deleted file mode 100644 index ba35b892ab..0000000000 --- a/.changeset/ipr-cross-repo-callable.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `LEDGER_DIR` support to `scripts/ipr/check-and-record.mjs` and a reusable `.github/workflows/ipr-check-callable.yml` so AAO repositories beyond adcp can write back to the central signature ledger via a GitHub App installation token. The script defaults `LEDGER_DIR` to cwd — adcp's existing workflow keeps working unchanged. Aligns adcp's concurrency group with the cross-repo group (`adcp-ipr-signature-write`) so signatures from any repo serialize against each other. Adds `governance/ipr-bot-setup.md` documenting the App configuration, secret rotation, revocation, and per-repo adoption steps. Per-repo caller workflows for adcp-client / adcp-client-python / adcp-go / creative-agent ship as separate PRs. diff --git a/.changeset/kms-webhook-signing.md b/.changeset/kms-webhook-signing.md deleted file mode 100644 index 03ed6cf7c8..0000000000 --- a/.changeset/kms-webhook-signing.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -feat(training-agent): GCP KMS-backed webhook signing - -Routes the training-agent's outbound webhook signing through a GCP KMS -`SigningProvider` (added in `@adcp/client@5.21.0` per #1020 / PR -adcp-client#1021). Private webhook-signing key material no longer enters -process memory in production. - -AdCP requires distinct key material per signing purpose -(`docs/guides/SIGNING-GUIDE.md` § Key separation), so this lands a second -KMS cryptoKeyVersion separate from the request-signing key. New Fly secret: -`GCP_KMS_WEBHOOK_KEY_VERSION` pointing at the webhook cryptoKeyVersion path. -The shared `GCP_SA_JSON` covers IAM for both. - -Refactors `server/src/security/gcp-kms-signer.ts` into a factory pattern -with two named exports — `getRequestSigningProvider()` and -`getWebhookSigningProvider()` — sharing init / tripwire / lazy-singleton / -in-flight-dedup logic. - -`server/src/security/expected-public-key.ts` now exports both committed -PEMs and KIDs (`aao-signing-2026-04` for requests, `aao-webhook-2026-04` -for webhooks). The published JWKS at `/.well-known/jwks.json` advertises -both keys with their respective `adcp_use` values; receivers enforce -purpose at that field. - -Dev fallback unchanged: when `GCP_KMS_WEBHOOK_KEY_VERSION` is unset, -`webhooks.ts` still loads `WEBHOOK_SIGNING_KEY_JWK` (stable JWK env) or -generates an ephemeral key. - -Bumps `@adcp/client` 5.20.0 → 5.21.0. diff --git a/.changeset/lift-storyboard-floors-after-518.md b/.changeset/lift-storyboard-floors-after-518.md deleted file mode 100644 index f9a51f0d5f..0000000000 --- a/.changeset/lift-storyboard-floors-after-518.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -CI: lift Training Agent Storyboards floors to match the post-5.18.0 baseline. Framework dispatch jumped from 374→401 passing / 42→53 clean once `@adcp/client` 5.18.0's schema-aware injection (adcp-client#943, the fix for #940) landed; legacy ticked up 384→388 with `force_task_completion` (#3194). The reduced framework floor was set as a temporary measure during the 5.17.0 regression and is no longer needed. diff --git a/.changeset/lint-test-dynamic-imports.md b/.changeset/lint-test-dynamic-imports.md deleted file mode 100644 index 181dca0303..0000000000 --- a/.changeset/lint-test-dynamic-imports.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Fix announcement test flakiness at the source (#3118). The 7 announcement test -files (plus tests/addie/escalation-tools.test.ts and email-conversation-flow.test.ts) -used `vi.resetModules()` + dynamic `await import()` per test, re-resolving the -entire transitive module tree on every `it()` and opening a mock-queue race -under thread-pool contention. Converted them to top-level static imports + -`vi.hoisted` mock refs — which is what `vi.mock`'s hoisting already supports. - -Adds `scripts/lint-test-dynamic-imports.cjs`, wired into the precommit chain, -to prevent the anti-pattern from regrowing. Two opt-out comments are supported -for legitimate cases (e.g. testing env-var-loaded module init): -`// lint-allow-resetmodules: ` and `// lint-allow-test-imports-file: `. - -Two existing files (tests/billing/organization-db.test.ts, -tests/addie/billing-tools.test.ts) use a different dynamic-import pattern -(reaching into mocked modules to grab fresh refs) — opted out with a TODO -pointing at a follow-up cleanup. tests/billing/stripe-client.test.ts -legitimately exercises `STRIPE_SECRET_KEY`-loaded module init and is opted -out permanently. diff --git a/.changeset/lint-universal-storyboard-doc-parity.md b/.changeset/lint-universal-storyboard-doc-parity.md deleted file mode 100644 index 765dff567e..0000000000 --- a/.changeset/lint-universal-storyboard-doc-parity.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -chore(compliance): lint that universal-storyboard doc tables match the filesystem - -New build-time + unit-test lint that prevents the drift #3099 just fixed from re-accumulating. Every graded universal storyboard MUST appear in both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx`; every backtick-quoted slug in those tables MUST resolve to a real graded storyboard on disk. - -**Why patch / `--empty` changeset.** Build-script + tests + new lint logic — no protocol-spec surface change. The lint encodes existing convention (the two index pages should agree with `static/compliance/source/universal/`), it doesn't introduce a new normative rule. - -**What it catches:** - -- New universal storyboard ships without a doc-table row → forward parity fails (specific "missing rows for X" error). -- Doc keeps a row for a renamed/deleted storyboard → reverse parity fails ("references X but no graded storyboard exists" error). -- Either index page loses its "Universal" heading → "missing expected heading" error. - -**Wiring:** - -- `scripts/lint-universal-storyboard-doc-parity.cjs` — new module exporting `lint({ sourceDir, repoRoot })` plus helpers, with a CLI entrypoint. -- `scripts/build-compliance.cjs` — calls the lint inside `generateIndex`, between `verifyEnumParity` and `lintStoryboardIdempotency`. Build fails loudly if drift is present. -- `tests/lint-universal-storyboard-doc-parity.test.cjs` — 10 tests: source-tree guard, clean-fixture, non-graded-fixture filtering, forward parity (both docs), reverse parity (both docs), missing-heading, helper unit tests. -- `package.json` — new `test:storyboard-doc-parity` script wired into the umbrella `test` target alongside the other storyboard lints, so CI's existing `npm run test` invocation picks it up automatically. - -Identifies "graded" by the presence of a `phases:` array in the YAML. Filters out the three non-graded fixtures (`storyboard-schema.yaml`, `runner-output-contract.yaml`, `fictional-entities.yaml`) which live alongside graded storyboards but aren't run by the suite. diff --git a/.changeset/list-orphaned-brands-admin-tool.md b/.changeset/list-orphaned-brands-admin-tool.md deleted file mode 100644 index a21a83a461..0000000000 --- a/.changeset/list-orphaned-brands-admin-tool.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -New `list_orphaned_brands` admin tool surfaces brands awaiting adoption — the prior owner relinquished, the manifest is preserved for the next claimant — so admins can audit the queue without raw DB access. Returns prior owner org name + id, days since relinquished, and a manifest preview (logo, color) per row. - -Pairs with the existing `transfer_brand_ownership` tool: when an admin confirms the legitimate next owner out of band, run transfer to hand it off; otherwise the row sits in the orphan pool until someone claims via the normal brand-identity flow (which now requires an explicit adopt-or-clear decision). - -Closes the "orphaned rows accumulate with no admin view" gap from the #3168 review. diff --git a/.changeset/lockdown-payment-link-issuer.md b/.changeset/lockdown-payment-link-issuer.md deleted file mode 100644 index 80751a2e67..0000000000 --- a/.changeset/lockdown-payment-link-issuer.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Lock down payment-link and invoice issuance to the authenticated member only. - -Removes four direct-issue paths that let an admin-typed `prospect_contact_email` or an LLM-supplied `customer_email` become the Stripe customer of record: - -- `POST /api/admin/accounts/:orgId/payment-link` (now 410 Gone) -- `POST /api/admin/prospects/:orgId/payment-link` (now 410 Gone) -- Addie billing tool `create_payment_link` no longer accepts `customer_email`; sources email from the signed-in member context only and stamps `workosUserId` on the checkout session -- Addie billing tools `send_invoice` / `confirm_send_invoice` no longer accept caller-supplied `contact_email`, `company_name`, `contact_name`, or `billing_address`; all four come from the signed-in member's identity and the org row -- Addie admin tool `send_payment_request` actions `payment_link` and `send_invoice` are removed; new `send_invite` action wraps the existing `invite-membership` flow so the recipient signs in and pays as themselves - -Admin UI payment-link modals (admin-account-detail.html, admin-accounts.html) are removed; admins use the membership-invite affordance, which already exists. Mirrors the same lockdown the invoice direct-issue path got in PR #2876 / commit 23018ce5e. diff --git a/.changeset/migration-436-renumber.md b/.changeset/migration-436-renumber.md deleted file mode 100644 index fff8b985c0..0000000000 --- a/.changeset/migration-436-renumber.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -fix(migrations): renumber organization_membership_provisioning_source from 436 to 438 - -Hotfix to unblock prod deploys. Two PRs landed migration 436 in parallel: - -- `436_addie_prompt_telemetry.sql` (#3270 ish, addie prompt telemetry) -- `436_organization_membership_provisioning_source.sql` (mine, via #3295) - -The deploy preflight blocked the boot because the migrate runner crashes on duplicate version numbers. Renumbers mine to 438 (next free; 437 is `437_auto_provision_digest_sent_at.sql`). - -Same recovery shape as the 2026-04 catalog/auto-provision 433 collision. The migration is fully idempotent (every `ALTER TABLE` and `CREATE INDEX` uses `IF NOT EXISTS`) so re-running it on systems that already applied it as 436 is a no-op. - -Independent of the CI fix landing in #3288 — the parallel-merge gap was in the workflow check, and #3288 plugs it for future PRs but can't retroactively prevent this collision. diff --git a/.changeset/migration-dup-check-hardening.md b/.changeset/migration-dup-check-hardening.md deleted file mode 100644 index ab024e6bb3..0000000000 --- a/.changeset/migration-dup-check-hardening.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -ci(migrations): close the parallel-merge gap in duplicate-number detection - -The "No duplicate migration numbers" workflow correctly detected collisions when a single PR's check ran against a stale view of main. But two PRs whose checks ran in parallel against *different* snapshots of main could both pass legitimately — which is how #3235 + #3244 both landed with migration 433 (and #2793 + #2800 both landed with 419 in 2026-02). - -Three layered fixes: - -1. **`merge_group` trigger** so the check runs on the real merge commit when GitHub merge queue is enabled — the only way to fully serialize the check. - -2. **Daily scheduled run** as a safety net for when merge queue isn't on. If a duplicate slips through, the workflow goes red on `main` within 24 hours; branch protection then blocks subsequent merges until the duplicate is renumbered. - -3. **Filesystem invariant test** in `server/tests/unit/migrate.test.ts` that scans the migrations directory and asserts no duplicate version numbers / malformed filenames. Runs locally on `npm test:unit` and on every CI run, regardless of workflow timing — catches duplicates the moment a developer's working tree has them. - -The error message is also clearer about the rebase-and-renumber recovery procedure, including the `IF NOT EXISTS` requirement so re-running on systems that already applied the old number is a no-op. - -The recommended **branch-protection setting** ("Require branches to be up to date before merging") is documented inline in the workflow comments — that's the GitHub-config-level fix for the parallel-PR case when merge queue isn't available. diff --git a/.changeset/my-content-status-toast-and-enabled-dropdown.md b/.changeset/my-content-status-toast-and-enabled-dropdown.md deleted file mode 100644 index 2363df1633..0000000000 --- a/.changeset/my-content-status-toast-and-enabled-dropdown.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix `/my-content` status dropdown for non-leads (#2719). Before this change `updateStatusHint()` force-set the dropdown to `draft` and disabled it, so a member submitting to a committee they were a member of could not actually pick "Submit for Review" — the option existed in the markup but was unreachable. The dropdown is now always enabled with `Submit for Review` as the default for non-leads, and the hint text explains what each option does. After a successful submit, `saveContent()` compares the requested status against the server-resolved `status` field and surfaces a toast — most importantly, when a non-lead picks "Publish Now" and the propose endpoint demotes the request to `pending_review`, the user sees "Submitted for review — a committee lead will approve before publishing. Track it in the Pending tab." instead of a silent demotion. diff --git a/.changeset/new-member-announce-linkedin-spec.md b/.changeset/new-member-announce-linkedin-spec.md deleted file mode 100644 index 35c0320ed0..0000000000 --- a/.changeset/new-member-announce-linkedin-spec.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Update `specs/new-member-announcements.md` (Workflow B, follow-up to PR #2246): - -- LinkedIn posting is treated as a permanent human step, not a v1 shortcut — LinkedIn's API does not grant posting scopes for company pages or personal profiles without partner status we don't have. -- Announcement state is now **per-channel**. `announcement_published` `org_activities` rows are written once per channel (`metadata.channel = "slack" | "linkedin"`) and an org is "fully announced" only when both exist. -- Editorial review flow updated: `Approve & Post to Slack` posts + records the Slack row; the review message then exposes a **Mark posted to LinkedIn** action (also available on the admin members page) so admins who post the LinkedIn copy manually can close the loop. -- "What exists vs build" table reflects the pieces already shipped in #2246 (`profile_published` emit, admin members announce-ready columns) and flags the LI mark-posted action as the next build item. diff --git a/.changeset/nudge-at-mention.md b/.changeset/nudge-at-mention.md deleted file mode 100644 index ff20c1bf4e..0000000000 --- a/.changeset/nudge-at-mention.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Switch the manual triage nudge from `/claude-triage` to `@claude-triage`. GitHub's native `/` autocomplete menu pops up when you type `/` in a comment (Alerts, Code block, Details…) and our non-registered slash command collides with it visually. `@` prefix avoids the menu, matches the `@dependabot` / `@claude` conventions users already recognize, and reads more naturally as "get this agent's attention." Bridge workflow filter and triage-prompt modifier docs updated. diff --git a/.changeset/orphan-adoption-ux-prompt.md b/.changeset/orphan-adoption-ux-prompt.md deleted file mode 100644 index c1618c8f70..0000000000 --- a/.changeset/orphan-adoption-ux-prompt.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Member-profile dashboard now surfaces the orphan-adoption choice when saving brand identity for a previously-registered domain. - -When `PUT /api/me/member-profile/brand-identity` returns 409 with `code: 'orphan_manifest_decision_required'` (the explicit-decision contract added in #3168), the form shows an inline prompt naming the prior owner and offering "Adopt prior identity" or "Start fresh." Either button re-fires the save with `adopt_prior_manifest` set explicitly. Cancel dismisses without writing. - -The success message after a claim reflects the choice ("Brand identity saved — adopted prior identity." vs "started fresh."), so members can confirm what landed. - -Closes the "members hit a generic 409 with no way to pick" gap from the #3168 reviewer feedback. Without a UI surface, the orphan-decision-required design was invisible to the people it's meant to serve. diff --git a/.changeset/pagination-cursor-invariant-test.md b/.changeset/pagination-cursor-invariant-test.md deleted file mode 100644 index b3ebc2fbac..0000000000 --- a/.changeset/pagination-cursor-invariant-test.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Pagination cursor↔has_more invariant: new authoring lint (`scripts/lint-pagination-invariant.cjs`) plus a universal storyboard (`pagination-integrity`) that walks `list_creatives` from a continuation page to a terminal page with three seeded fixtures and `max_results=2`. - -The lint scans schema `examples[]` payloads and storyboard `sample_request` / `sample_response` fixtures for the two violation classes the prose contract on `pagination-response.json` doesn't enforce: `has_more=true` without a `cursor`, and `has_more=false` with a stale `cursor`. Wired into the `test` chain. - -The storyboard catches the dishonest-pagination case at runtime — an agent that hides the seeded third creative behind `has_more=false` on the first page fails the first-page assertion, and an agent that carries a stale cursor onto the terminal page fails the second-page assertion. `total_count` is intentionally unchecked since the schema permits omission. - -Also fixes the training agent's `list_creatives` to honor `pagination.max_results` (default 50, capped at 100 per the request schema) and emit an opaque base64url offset cursor when the page is non-terminal. Previously every response carried `has_more: false` regardless of input, the exact dishonest shape the new storyboard catches. diff --git a/.changeset/pagination-total-count-honesty.md b/.changeset/pagination-total-count-honesty.md deleted file mode 100644 index 2029fe32f8..0000000000 --- a/.changeset/pagination-total-count-honesty.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Extends `pagination_integrity` with count-honesty assertions that complement the cursor↔has_more invariant. Each page now asserts `query_summary.total_matching = 3`, `query_summary.returned` matches the slice (2 then 1), and `pagination.total_count` equals 3 when volunteered (`field_value_or_absent` so omitting it stays conformant per the schema). - -Catches the dishonest pagination class where an agent honors `max_results` and the cursor handshake but lies in the summary numbers — under-reporting `total_count` to hide inventory the same way a dishonest `has_more: false` would, or drifting `total_matching` between pages. Verified by spot-flipping the training agent's `total_count` to the page-local count: page-1 assertion fires with the expected diagnostic. diff --git a/.changeset/pending-agreement-user-id.md b/.changeset/pending-agreement-user-id.md deleted file mode 100644 index ea6277b899..0000000000 --- a/.changeset/pending-agreement-user-id.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `organizations.pending_agreement_user_id` column and wire it through the membership-agreement lifecycle so the Stripe `customer.subscription.created` webhook can attribute user-level acceptance deterministically instead of reverse-looking-up via `stripe_customer.email → WorkOS`. Populated at checkbox time from every route that sets `pending_agreement_*` (`POST /api/organizations/:orgId/pending-agreement`, invite-accept, invoice-request). The webhook's `resolveWorkosUserForSubscription` now consults this column first, ahead of subscription/customer metadata and email lookup — removing the brittle fallback that caused PR #3011's silent-failure incident for users whose Stripe email didn't match their WorkOS email. Clears pending fields after the user-level record lands so retries don't double-apply. Adds `scripts/backfill-agreement-acceptance.ts` — a one-shot tool admins can run when `needs_manual_reconciliation: true` fires, with org-membership verification before insert. Migration 427; three resolver tests for the new source priority. diff --git a/.changeset/persona-driven-suggested-prompts.md b/.changeset/persona-driven-suggested-prompts.md deleted file mode 100644 index b20e887e16..0000000000 --- a/.changeset/persona-driven-suggested-prompts.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Stage 1 of persona-driven Addie suggested prompts (#2299). Replaces the static three-tier `buildDynamicSuggestedPrompts` with a unified rule registry that drives prompts in Slack Assistant threads, the App Home tab, and the web home. Adds rules for persona, profile completeness, lapsed re-engagement, low-login soft re-engagement, working-group leader/member, Explorer-tier upgrade, and solo-org-owner invite-team. Persona prompts now fire correctly (the previous web builder used persona IDs that did not match the actual codes). diff --git a/.changeset/pin-npx-latest.md b/.changeset/pin-npx-latest.md deleted file mode 100644 index c8e6750b31..0000000000 --- a/.changeset/pin-npx-latest.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Pin `@latest` in every documented `npx @adcp/client` invocation. Unpinned `npx` silently reuses whatever version is cached in `~/.npm/_npx/` — users follow our docs, run months-old CLI code, hit bugs that are already fixed, and don't realize they're on a stale version. Touches all five `docs/building/` guides, the buyer/publisher/platform learning tracks + instructional-design, the `create_media_buy` task reference, `server/public/llms.txt` + `llms-full.txt`, Addie's certification-tools + prompts + knowledge rules (so Addie recommends the pinned form and can diagnose stale-cache symptoms), and `scripts/build-protocol-tarball.cjs`. Mirrors adcontextprotocol/adcp-client#835, which also adds a CLI startup staleness check for every path that `@latest` doesn't cover (global installs, locked `package.json`, corporate forks, `pnpm dlx`). diff --git a/.changeset/policy-entry-shape-examples-and-additive-only-warning.md b/.changeset/policy-entry-shape-examples-and-additive-only-warning.md deleted file mode 100644 index 1550097202..0000000000 --- a/.changeset/policy-entry-shape-examples-and-additive-only-warning.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Fix schema-invalid `custom_policies` and `shared_exclusions` examples in governance docs. The `sync-plans-request.json` schema defines both as `PolicyEntry[]` (requiring `policy_id`, `enforcement`, `policy` text), but the existing examples in `docs/governance/campaign/specification.mdx` and `docs/governance/campaign/tasks/sync_plans.mdx` used plain strings. A reader copy-pasting those examples and validating against the schema would fail. Updates the field-table descriptions in `sync_plans.mdx` to reflect the actual shape, and adds a `` block in the Policy resolution section of `specification.mdx` highlighting the additive-only invariant from `policy-entry.json`. Cherry-picked from PR #3143 (which is superseded by the merged #3187 for the conceptual content but carried this real schema-validity fix). diff --git a/.changeset/policy-registry-sync-and-versioning-doc.md b/.changeset/policy-registry-sync-and-versioning-doc.md deleted file mode 100644 index 894d8727ec..0000000000 --- a/.changeset/policy-registry-sync-and-versioning-doc.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `docs/governance/policy-registry-sync.mdx` documenting the operational pattern for keeping campaign plans synchronized with the Policy Registry. Covers the headline question (`policy_ids[]` carries no version qualifier — so versions resolve at evaluation time, latest-wins), the inline-copy workaround for pinning a specific text into `custom_policies[]` under a renamed ID, behavior when a registry policy version-bumps mid-campaign, the `effective_date` staged-adoption pattern, sunset behavior, the additive-only invariant for inline policies relative to registry-sourced policies, and a working-group FAQ. Closes [#3140](https://github.com/adcontextprotocol/adcp/issues/3140). Pure descriptive doc — no schema, task, or protocol changes. diff --git a/.changeset/post-2993-valid-actions-and-partitioning.md b/.changeset/post-2993-valid-actions-and-partitioning.md deleted file mode 100644 index e30d8a2cf0..0000000000 --- a/.changeset/post-2993-valid-actions-and-partitioning.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -spec(media-buy): close the empty-`valid_actions` loophole and specify where partitioning belongs - -Post-#2993 follow-up tightening, surfaced by expert review of the Tier-2 conformance PR (#2965). The merged #2993 rules say a seller MUST return every account-owned buy and MUST NOT mark non-AdCP buys read-only — but they also say an action MAY be omitted from `valid_actions` for "business reasons." Those two clauses combined let a seller technically comply by returning non-AdCP buys with a systematically empty `valid_actions`, which is indistinguishable from hiding the buy and defeats the normative intent. - -Two clarifications: - -1. **Creation surface is never a business reason.** Sales agents MUST NOT omit an action from `valid_actions` — or return `INVALID_STATE` on an otherwise-valid update — solely because the buy was created outside AdCP. `valid_actions` omissions are legitimate only when grounded in contractual, platform, or policy constraints that would apply equally to an AdCP-created buy in the same state. A systematically-empty `valid_actions` set on non-AdCP buys is non-conformant. - -2. **Partitioning belongs at the account boundary.** When a seller has a legitimate reason to keep a set of buys outside a caller's operational reach (child-seller models, NDA-scoped PMP deals, sandbox-vs-prod separation, tenant-level privacy partitions), the correct mechanism is expressing that as a separate account the caller is not authorized to reference — not filtering a subset of the caller's authorized account. Within-account filtering reintroduces the shadow-ledger problem #2963 forbade. - -Updated: `docs/media-buy/specification.mdx` (new "Partitioning belongs at the account boundary" subsection under Account Ownership vs. Creation Surface; strengthened MUST NOTs), `docs/media-buy/task-reference/get_media_buys.mdx`, `docs/media-buy/task-reference/update_media_buy.mdx`. - -No schema changes. Closes the gap flagged on PRs #2994 and #3001 reviews. diff --git a/.changeset/post-3.0-release-fixes.md b/.changeset/post-3.0-release-fixes.md deleted file mode 100644 index 1d9053e480..0000000000 --- a/.changeset/post-3.0-release-fixes.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Post-3.0 GA release fixes to make the next cut work end-to-end without manual intervention: - -- `changeset tag` now runs on private packages (`privatePackages.tag: true`), so future Version Packages merges auto-tag and `changesets/action` auto-creates the GitHub Release with artifacts -- `.dockerignore` keeps `dist/protocol/` so cosign-signed versioned tarballs ship in the Fly.io image and `/protocol/{version}.tgz` actually serves -- Two flaky tests in `adagents-manager.test.ts` get a 10s timeout (matches their sibling at line 410). The underlying issue — `@adcp/client` makes real network calls from inside a "unit" test — should be fixed by mocking `@adcp/client` at the test file level -- Drop the duplicate autogen `## 3.0.0` section from `CHANGELOG.md` (the curated narrative at the kept block is enough) -- Add `.agents/shortcuts/cut-major.md` runbook covering the full cut-a-major process with the gotchas we hit on 3.0: audit non-protocol changesets, don't hand-edit `CHANGELOG.md` on the release branch, verify `/protocol/{version}.tgz` serves end-to-end diff --git a/.changeset/postgres-replay-store-3338.md b/.changeset/postgres-replay-store-3338.md deleted file mode 100644 index b097e64369..0000000000 --- a/.changeset/postgres-replay-store-3338.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -fix(training-agent): swap to PostgresReplayStore so vector 016 catches cross-instance replays - -The per-route fix in #3346 closed the cross-route bug but vector 016 still failed in prod. Root cause: Fly runs `min_machines_running = 2` web machines and the per-process `InMemoryReplayStore` can't see across machines. Probe 1 hits machine A and consumes the nonce; probe 2 gets routed to machine B by the LB, machine B has never seen the nonce locally, accepts it. - -Swaps `InMemoryReplayStore` for `PostgresReplayStore` from `@adcp/client/signing/server` (5.21.0+, [adcp-client#1018](https://github.com/adcontextprotocol/adcp-client/pull/1018)). All instances share one `adcp_replay_cache` table, so the replay window holds across the LB. - -Adds: -- Migration `447_adcp_replay_cache.sql` — schema mirrors the SDK's `getReplayStoreMigration()` output: `(keyid, scope, nonce)` PK + `expires_at` TTL column, two indexes for the lookup and sweep paths. -- `startReplayCacheSweeper()` in `request-signing.ts` and `index.ts` boot wiring — runs `sweepExpiredReplays(pool)` every 60s. Postgres has no native TTL; without sweeping the table grows unboundedly. - -Singleton replay store across the per-route authenticators (default / strict / strict-required / strict-forbidden) — the (keyid, scope, nonce) primary key already partitions by route via the `@target-uri`-derived scope, so a shared table is safe and avoids four separate connections. - -Closes the remaining piece of #3338. Once deployed, grader vector 016 should pass against `/mcp-strict`, completing 33/33 graded vectors across the four strict-mode routes. diff --git a/.changeset/prohibit-dual-status-emission.md b/.changeset/prohibit-dual-status-emission.md deleted file mode 100644 index 6d161cc030..0000000000 --- a/.changeset/prohibit-dual-status-emission.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Clarify that v3 agents MUST NOT emit legacy status fields (`task_status`, `response_status`, or any alias) alongside the v3 `status` field. Adds a migration checklist row, a conformance warning in the task-lifecycle reference, and extends the protocol envelope schema's `status` description with the prohibition. Closes #2987. diff --git a/.changeset/prospect-slack-contact-info.md b/.changeset/prospect-slack-contact-info.md deleted file mode 100644 index 0c27ca3262..0000000000 --- a/.changeset/prospect-slack-contact-info.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Include the signup contact name and email on prospect Slack notifications so owners know who just signed up. diff --git a/.changeset/provisioning-source-attribution.md b/.changeset/provisioning-source-attribution.md deleted file mode 100644 index 6cdd5377fb..0000000000 --- a/.changeset/provisioning-source-attribution.md +++ /dev/null @@ -1,20 +0,0 @@ ---- ---- - -feat(membership): track provisioning source on each org membership - -Each row in `organization_memberships` now carries a `provisioning_source` tag identifying how it came to exist: - -- `verified_domain` — `autoLinkByVerifiedDomain` matched the user's email domain to a verified org with auto-provision on -- `invited` — accepted via `POST /:orgId/invitations` or `/members/by-email` Path 1 -- `admin_added` — created via `/members/by-email` Path 2 (admin/owner direct add) -- `webhook` — surfaced by an `organization_membership.created` event with no staged source (e.g. someone added the membership directly in the WorkOS dashboard) -- `null` — pre-existing rows that haven't been touched since this migration - -The originating endpoint stages source + seat_type into `invitation_seat_types` (a new `source` column there). The `organization_membership.created` webhook handler reads both back via `consumeInvitationSeatType` and writes `provisioning_source` on the local cache row. The upsert preserves an existing source on conflict, so a subsequent webhook upsert can't wipe a more-specific origin. - -Sets up the new-member digest in the auto-provision notification feature so org owners can see which auto-joined members showed up via verified-domain vs. were explicitly invited. - -## Migration - -`438_organization_membership_provisioning_source.sql` adds the columns and an index keyed on `(workos_organization_id, provisioning_source, created_at DESC)` for the digest query. diff --git a/.changeset/quiet-schema-link-warnings.md b/.changeset/quiet-schema-link-warnings.md deleted file mode 100644 index f2349a222e..0000000000 --- a/.changeset/quiet-schema-link-warnings.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Schema link check only comments on PRs for actionable errors. Warnings -("schema exists in source but not yet released") now go to the workflow job -summary instead of a PR comment — they self-resolve on the next release and -were generating email noise on every schema-touching docs PR. diff --git a/.changeset/redteam-runner-admin-bypass.md b/.changeset/redteam-runner-admin-bypass.md deleted file mode 100644 index 55653ecd0e..0000000000 --- a/.changeset/redteam-runner-admin-bypass.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Adds an env-gated admin bypass to the Addie redteam runner. When `ADMIN_API_KEY` is set, the runner sends `Authorization: Bearer …` on each request and is treated as admin server-side, skipping the anonymous 50-msg/IP daily limiter and the per-IP anonymous cost cap. Without this, a single 33-scenario run could exhaust the daily IP budget mid-pass and contaminate results with HTTP 429 / cost-cap responses (which contain none of the redteam's marker words and surface as spurious `missing_marker` failures). No-op when the env var is unset, so default behavior is unchanged. diff --git a/.changeset/refresh-current-context-2026-04-24.md b/.changeset/refresh-current-context-2026-04-24.md deleted file mode 100644 index f680578799..0000000000 --- a/.changeset/refresh-current-context-2026-04-24.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Weekly `.agents/current-context.md` refresh (2026-04-24). Rewrites the -snapshot from fresh repo signal across adcp, adcp-client, adcp-client-python, -and adcp-go — carries forward active initiatives (3.0 GA, v2 sunset, -release cadence, TMP/Go SDK, A2A 1.0 migration), surfaces new themes -(Dependency Impact RFC epic, RFC 9421 signing cluster, AI Provenance -Extensions, Addie cost-cap observability, WorkOS OAuth rollout, A2UI/brand.json -theming, governance WG publishing, editorial workflow epic, Claude Code -routines), and drops entries with no recent activity. diff --git a/.changeset/refresh-universal-storyboard-tables.md b/.changeset/refresh-universal-storyboard-tables.md deleted file mode 100644 index 1b00df2f94..0000000000 --- a/.changeset/refresh-universal-storyboard-tables.md +++ /dev/null @@ -1,20 +0,0 @@ ---- ---- - -docs(conformance, catalog): refresh universal-storyboards tables to match `static/compliance/source/universal/` - -Both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx` had stale universal-storyboards tables. The actual `static/compliance/source/universal/` directory now contains 9 graded storyboards; the docs listed 5–7. Drift accumulated as new storyboards landed without back-filling the index pages. - -**Adds (both files):** - -- `webhook-emission` — outbound webhook conformance (idempotency_key + RFC 9421 webhook signing). Runs for any agent accepting `push_notification_config`. Has been universal since #2417 / 3.0; never indexed in the catalog tables. -- `pagination-integrity` — `cursor` ↔ `has_more` invariant. Recently landed; missing from both pages. - -**Adds (compliance-catalog only):** - -- `idempotency` — was missing entirely from the catalog table though present in conformance.mdx and shipped as universal in 3.0. -- `signed-requests` — added in #3077 to the conformance.mdx table; the parallel catalog entry was missed. - -**Framing fix (compliance-catalog):** the lead-in said "Every agent runs every storyboard… regardless of which protocols or specialisms it claims," which is true in scope but confusing on capability-gated rows (`deterministic-testing`, `signed-requests`). Reworded to "every agent runs every storyboard, with a few capability-gated by an explicit `supported: true` advertisement" plus a closing paragraph noting that gated storyboards can't be partially implemented (advertise `false` if you don't ship the full surface). - -No new normative content. The two index pages now reflect the same 9 universal storyboards in the same order, matching the published `/compliance/{version}/universal/` directory. diff --git a/.changeset/registry-agents-snapshot-tables.md b/.changeset/registry-agents-snapshot-tables.md deleted file mode 100644 index bf6ea3af1a..0000000000 --- a/.changeset/registry-agents-snapshot-tables.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Materialize agent health + capabilities into `agent_health_snapshot` and `agent_capabilities_snapshot` tables, written by the crawler and bulk-read by `GET /api/registry/agents`. Replaces the live MCP/A2A fan-out on every registry page load (which would hang when any single agent was slow) with two DB queries, matching the existing compliance-status pattern. diff --git a/.changeset/registry-auth-model-one-pager.md b/.changeset/registry-auth-model-one-pager.md deleted file mode 100644 index 2d2121679c..0000000000 --- a/.changeset/registry-auth-model-one-pager.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Add `specs/registry-authorization-model.md` — the gating decision artifact -for PR 4b of the property registry unification (#3177). Compares three -options for storing agent → publisher / agent → property authorizations -in the catalog (first-class table, JSONB-only, fact-based), recommends -the first-class `catalog_agent_authorizations` table to mirror -`catalog_identifiers`, and lays out a sequencing for PR 4b-prereq -(schema), PR 4b (writer + reader cutover), and PR 5 (drop legacy). - -Spec-only — no code changes. - -Refs #3177. diff --git a/.changeset/registry-crawler-cache.md b/.changeset/registry-crawler-cache.md deleted file mode 100644 index 544c954038..0000000000 --- a/.changeset/registry-crawler-cache.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Crawler now caches successful adagents.json fetches into the publishers -overlay table (migration 432) and projects parsed properties into -catalog_properties + catalog_identifiers in the same transaction. The -existing discovered_properties / agent_property_authorizations writes -continue alongside the new ones for one release as a fallback before PR 5 -of #3177 drops the old tables. - -Closes the gap surfaced by Setupad escalation #218: properties that landed -in discovered_properties via the crawler never made it into the catalog -because migration 336 was a one-time seed. Every successful crawl now -lands in both places. - -Refs #3177. Builds on #3195. diff --git a/.changeset/registry-overlay-schema.md b/.changeset/registry-overlay-schema.md deleted file mode 100644 index 589793ede4..0000000000 --- a/.changeset/registry-overlay-schema.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Add empty schema for the property registry overlay model: `publishers` table (one row per domain, caches `adagents.json` body as JSONB) and `adagents_authorization_overrides` (per-agent corrections with `bad_actor` / `correction` / `file_broken` reasons, lifecycle differs by reason). Mirrors the brand registry pattern. Schema invariants enforce the design rules — bad_actor overrides cannot be silently expired, supersession reasons must match override reasons, active-set uniqueness via partial index. No backfill, no readers/writers yet — those land in subsequent PRs. - -Tracking: #3177. diff --git a/.changeset/registry-property-readers-catalog.md b/.changeset/registry-property-readers-catalog.md deleted file mode 100644 index bc9ec36c42..0000000000 --- a/.changeset/registry-property-readers-catalog.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Property-side readers in `federated-index-db.ts` and `property-db.ts` now -union the catalog-side `publishers` cache (PR 1 of #3177) with the legacy -`discovered_properties` and `discovered_publishers` tables. Crawl-sourced -properties that landed via the new writer path but missed the legacy -table — gatavo.com surfaced via Setupad escalation #218 — now appear on -the registry surfaces alongside legacy data. - -Affects `hasValidAdagents`, `getPropertiesForDomain`, -`getDiscoveredPropertiesByDomain`, `getAllPropertiesForRegistry`, and -`getPropertyRegistryStats`. Authorization-side readers -(`getPropertiesForAgent`, `findAgentsForPropertyIdentifier`, -`getPublisherDomainsForAgent`) stay on legacy tables — those need the -authorization model decision and ship in PR 4b. - -Legacy wins on collisions so callers that hold a -`discovered_properties.id` keep dereferencing it correctly during the -dual-write window. After PR 5 drops the legacy tables the legacy half -of the union goes away. - -Refs #3177. Builds on #3195 / #3218 / #3221. diff --git a/.changeset/registry-reader-baseline-tests.md b/.changeset/registry-reader-baseline-tests.md deleted file mode 100644 index 9fffa25a88..0000000000 --- a/.changeset/registry-reader-baseline-tests.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Add baseline integration coverage for the federated index + property registry reader functions ahead of the property registry unification (issue #3177). - -Tests-only, no production code changes. PR 1 (#3195) shipped the empty `publishers` + `adagents_authorization_overrides` schema. PR 4 will swap the readers under `getPropertiesForAgent` / `getPropertiesForDomain` / `getAgentsForDomain` / `validateAgentForProduct` / `getAllPropertiesForRegistry` and friends, plus the public registry endpoints (`/registry/agents`, `/registry/publishers`, `/registry/publisher`, `/registry/operator`, `/registry/stats`) and the directory MCP tools (`lookup_domain`, `list_publishers`), to consult the new schema. These four new test files pin the I/O of the current readers — same fixtures, same response shapes / counts / ordering — so the cutover fails loudly if it changes any caller-visible behavior. diff --git a/.changeset/registry-search-by-owner.md b/.changeset/registry-search-by-owner.md deleted file mode 100644 index 22c5180357..0000000000 --- a/.changeset/registry-search-by-owner.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Registry Agents page: extend search to match on owner (contact name, email, website) in addition to agent name, URL, properties, and formats. diff --git a/.changeset/replay-store-fallback.md b/.changeset/replay-store-fallback.md deleted file mode 100644 index 68134c2eaf..0000000000 --- a/.changeset/replay-store-fallback.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -fix(training-agent): InMemoryReplayStore fallback when no Postgres pool (test/dev only) - -#3351 swapped to `PostgresReplayStore` to close the cross-instance replay gap. That worked in production but broke the storyboard runner: CI runs the full server in-process without initializing a Postgres pool, and `getReplayStore()` was unconditionally calling `getPool()` which throws. - -Symptom: `signed_requests` storyboard regressed from `31P / 9S / 0N/A` to `3P / 28F / 9S` — every positive vector returned 401 because `PostgresReplayStore.insert` rejected on the unavailable pool, and the verifier failed closed. - -Fix: `getReplayStore()` now falls back to `InMemoryReplayStore` when `getPool()` throws — gated on `NODE_ENV !== 'production'` so a misconfigured prod still fails loudly. The sweeper is a silent no-op when no pool is initialized. Reset hook in `resetRequestSigning()` clears the cached store so test suites that swap process state stay coherent. - -Production unaffected: prod always has a Postgres pool, so `PostgresReplayStore` is used and cross-instance replay protection holds. Verified via `adcp grade request-signing https://agenticadvertising.org/api/training-agent/mcp-strict --only 016-replayed-nonce` → still PASS. diff --git a/.changeset/reset-subscription-state-tests.md b/.changeset/reset-subscription-state-tests.md deleted file mode 100644 index e41e3b815f..0000000000 --- a/.changeset/reset-subscription-state-tests.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Adds integration tests for `POST /api/admin/accounts/:orgId/reset-subscription-state` (#3222). Covers each safety guard (404/400/503), the happy path, and transaction atomicity (UPDATE + audit-log INSERT must commit together; either failure must roll back). Uses supertest + mocked auth/pool/stripe, matching the existing admin-route test pattern. diff --git a/.changeset/rest-api-oauth-sso.md b/.changeset/rest-api-oauth-sso.md deleted file mode 100644 index fd50963dd4..0000000000 --- a/.changeset/rest-api-oauth-sso.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Accept OAuth-issued user JWTs at `/api/*`, closing the gap between what the OpenAPI spec advertised and what the server enforced. - -The MCP OAuth flow (`mcpAuthRouter` at `/authorize` → AuthKit → `/token`) already issued WorkOS-signed JWTs and verified them on `/mcp`. REST's `requireAuth` only accepted sealed-session cookies and `sk_...` organization API keys — so an agent client that SSO'd a user had no way to call the REST API on that user's behalf, even though the spec said `bearerAuth` was accepted. - -**What's new** - -- `requireAuth` accepts WorkOS-signed user JWTs via `Authorization: Bearer `. Same verifier the MCP OAuth flow uses (WorkOS JWKS). Subject must correspond to a real local user. Machine-to-machine tokens (`client_credentials`) are rejected here — server-to-server callers continue to use `sk_...` keys. -- RFC 9728 protected-resource metadata is now published at `GET /.well-known/oauth-protected-resource/api`, pointing at the same authorization server as `/mcp`. A single user SSO grants both surfaces. -- `scripts/generate-openapi.ts` now registers the `bearerAuth` and `oauth2` security schemes on the registry (passing them via `generateDocument` options was silently dropped by `OpenApiGeneratorV31`). The checked-in spec had `security: [{ bearerAuth: [] }]` on 24 endpoints referencing a scheme the spec never defined — codegen would have broken. Each protected endpoint now lists both schemes as alternatives. -- `docs/registry/index.mdx` gains an "Option B: User SSO via OAuth 2.1" section covering the discovery endpoints and flow. - -**Incidental fix — MCP OAuth user upsert** - -`handleMCPOAuthCallback` did not upsert the authenticated user into the local `users` table, while the cookie-based `/auth/callback` path did. MCP tool handlers never noticed because they read claims straight off `req.mcpAuth`. REST `requireAuth` verifies the JWT subject corresponds to a real local user, so users who first arrived via MCP were rejected at `/api/*`. The callback now performs the same upsert the cookie path does (`server/src/mcp/oauth-provider.ts`). - -**Security hardening from review** - -- **Application pinning.** `verifyWorkOSJWT` now rejects tokens whose `azp` or `client_id` claim is not our own `WORKOS_CLIENT_ID`. Without this, a sibling application in the same WorkOS tenant could mint tokens that pass signature verification against the shared JWKS. -- **Positive cache on the JWT branch.** `validateWorkOSBearerJWT` caches successful validations keyed on SHA-256 of the token, TTL `min(60s, remaining token exp)`. Removes the per-request DB round-trip that would otherwise amplify DoS surface vs the cookie path (which has an equivalent cache). -- **Trust scoping.** Dropped the auto-`isMember = true` assignment from the JWT's `org_id` claim — the claim reflects the AuthKit-selected org, not AAO membership. Downstream `enrichUserWithMembership` resolves real standing via the DB. -- **Error surface.** MCP `InvalidTokenError` now returns a fixed `"Invalid or expired token"` message instead of echoing `jose` internals. Real error is logged server-side. -- **Test coverage.** `__setJWKSForTesting` hook lets us exercise `verifyWorkOSJWT` with locally-generated key pairs — added coverage for happy path, expired tokens, signature-from-unknown-key, `azp`/`client_id` mismatch, missing application id, and M2M detection. - -**Scope of writes** - -User JWTs get the same access as organization API keys — writes included. MCP tool handlers already allowed OAuth users to mutate registry state; restricting REST to reads would have been arbitrary inconsistency. - -**Files** - -- New: `server/src/auth/workos-jwt.ts` (shared verifier), `server/tests/unit/workos-jwt.test.ts` -- Changed: `server/src/middleware/auth.ts`, `server/src/mcp/oauth-provider.ts`, `server/src/http.ts`, `server/src/mcp/index.ts`, `server/src/routes/registry-api.ts`, `scripts/generate-openapi.ts`, `static/openapi/registry.yaml`, `docs/registry/index.mdx` diff --git a/.changeset/revert-kms-eager-init.md b/.changeset/revert-kms-eager-init.md deleted file mode 100644 index 3bc3e2e04c..0000000000 --- a/.changeset/revert-kms-eager-init.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -fix(boot): drop GCP KMS eager-init at boot - -The eager-init added in #3283 took down the prod deploy: when KMS auth is -misconfigured, the gRPC client retries forever inside `getPublicKey`, the app -never binds port 8080, and Fly's health-check times out without surfacing the -underlying KMS error. - -Lazy init in `getGcpKmsSigningProvider()` is the safer default — a broken -signing path fails per-call (logged + generic message to LLM via the -call-site try/catch in `adcp-tools.ts`) while the rest of the server boots -normally so operators can SSH in and inspect. - -Re-enabling eager init needs either a hard timeout on the `getPublicKey` -round-trip or a deploy-time probe outside the boot critical path. diff --git a/.changeset/revert-tasks-get-result-for-3-0-1.md b/.changeset/revert-tasks-get-result-for-3-0-1.md deleted file mode 100644 index c74af217b4..0000000000 --- a/.changeset/revert-tasks-get-result-for-3-0-1.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Prep main for the 3.0.1 cut: - -- Revert #3126 (`tasks/get` `result` / `include_result` schema fields). The PR's own milestone was 3.1.0 — it landed early and was forcing the changeset bundle to a minor. Re-add it after 3.0.1 ships by reverting commit `f4bce25d5` on a fresh branch (or cherry-picking the original `4136a4a6d`). -- Downgrade five changesets from `minor` → `patch` where the underlying change is annotation-only, source-schema refactor, conformance-harness only, or a clarification of underspecified behavior: `add-seed-creative-format-pagination`, `fix-format-asset-oneof-titles`, `fix-get-signals-max-results-precedence`, `hoist-duplicate-inline-enums`, `hoist-inline-enum-duplicates-tranche-2`. -- Resolve the envelope-prohibition overlap on `protocol-envelope.json`: keep the top-level `not: { anyOf: [{ required: [task_status] }, { required: [response_status] }] }` constraint (from `envelope-forbid-legacy-status-fields.md`) and remove the redundant per-property `not: {}` markers added by the same PR that introduced the v3 envelope integrity storyboard. Storyboard narrative updated to reference the remaining constraint. `v3-envelope-integrity-conformance.md` rewritten as `patch` and scoped to the storyboard contribution only. - -Result: `npx changeset status` reports a single patch bump for `adcontextprotocol`; 3.0.1 cuts cleanly. diff --git a/.changeset/route-invoice-requests-to-billing-channel.md b/.changeset/route-invoice-requests-to-billing-channel.md deleted file mode 100644 index cc42913062..0000000000 --- a/.changeset/route-invoice-requests-to-billing-channel.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Route invoice-request Slack notifications through the configured billing channel (`getBillingChannel`) via `notifyInvoiceSent`, instead of the catch-all `SLACK_WEBHOOK_URL`. The `notifyInvoiceSent` helper now carries the requester name and Stripe invoice ID, and sanitizes user-controlled fields against Slack mrkdwn injection. diff --git a/.changeset/sanitize-storyboard-error-rendering.md b/.changeset/sanitize-storyboard-error-rendering.md deleted file mode 100644 index 0ae41d1c68..0000000000 --- a/.changeset/sanitize-storyboard-error-rendering.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Addie: sanitize agent-controlled error and narrative strings before interpolating them into MCP tool output. Closes #3219. - -The hint fix-plan formatter (adcp#3084 / #3220) carefully sanitized every seller-controlled field at its boundary, but sibling renders on the same `StoryboardStepResult` (`step.error`, `validation.error`, `result.next.narrative`) and adjacent tools (`evaluate_agent_quality` scenario errors, `compare_media_kit` per-brief errors, `test_io_execution` failure messages, `get_storyboard_detail` narratives) emitted runner/agent strings raw — letting a hostile or compromised seller bypass the formatter's prompt-injection protection through a sibling field on the same Claude-bound output. - -This pass runs every such site through `sanitizeAgentField` with a documented 400-char cap (`RUNNER_ERROR_MAX_LEN`) — explicitly framed as a prompt-injection budget, not a UX choice. Defense in depth; no current attack. diff --git a/.changeset/sdk-5-19-and-idempotency-probe.md b/.changeset/sdk-5-19-and-idempotency-probe.md deleted file mode 100644 index 9f1a0e1be2..0000000000 --- a/.changeset/sdk-5-19-and-idempotency-probe.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -chore(server): bump @adcp/client 5.18.0 → 5.19.0 and probe idempotency backend at boot - -Wires the new `IdempotencyStore.probe()` (5.19.0) into the main HTTP boot path right after `runMigrations()`. A misconfigured pool — typo in `DATABASE_URL`, missing migration, deprovisioned DB, wrong credentials — now fails the process at deploy time with a descriptive error naming the table and remediation, rather than silently passing every mutating call to a broken backend until the first 5xx in production. - -`probe?.()` is optional on the store interface — when the lazy backend selection in `training-agent/idempotency.ts` falls back to `memoryBackend` (no DB initialized), the call is a no-op and boot proceeds normally. - -5.19.0 also ships: -- `AgentClient.fromMCPClient()` — in-process MCP transport for compliance fleets (adcp-client#1008) -- Storyboard runner: `$generate:opaque_id` + `context_outputs[generate]` for runner-minted task IDs threaded through multi-step lifecycle storyboards (adcp-client#1006) -- `get_media_buys` extractor guard for mid-walk pagination pages (adcp-client#999) diff --git a/.changeset/security-mdx-production-key-storage.md b/.changeset/security-mdx-production-key-storage.md deleted file mode 100644 index 9beb67ac0b..0000000000 --- a/.changeset/security-mdx-production-key-storage.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -docs(security): add Production key storage subsection to RFC 9421 request-signing guidance - -Adds a brief subsection at `docs/building/implementation/security.mdx` recommending KMS / HSM / Vault for production private-key storage on RFC 9421 transport-layer signing. Includes implementation notes for adapter authors (DER → IEEE P1363 conversion for ECDSA-P256, single-purpose key policy to avoid cross-protocol oracles) and points at the `@adcp/client` reference implementations. - -The spec stays implementation-agnostic about where private keys live — only the bytes on the wire matter — but operator guidance on production key storage is a natural fit for the existing implementation guide. diff --git a/.changeset/seed-multi-package-storyboards.md b/.changeset/seed-multi-package-storyboards.md deleted file mode 100644 index 12f26fb9ae..0000000000 --- a/.changeset/seed-multi-package-storyboards.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Storyboards: add `controller_seeding: true` + `fixtures:` blocks on six -storyboards whose `create_media_buy` steps author multi-package payloads -referencing products that don't exist in any reasonable seller catalog -by default. After `@adcp/client` 5.12 (adcp-client#794), the storyboard -runner emits every authored package instead of silently dropping -`packages[1+]`. Sellers without these products then hit -`PRODUCT_NOT_FOUND: Package 1: Product not found: ` and cascading -step failures in their conformance runs. - -Wires the SDK's fixture-seeding feature (adcp-client#790) on the six -storyboards so `packages[1+].product_id` / `pricing_option_id` resolve -against seeded fixtures regardless of the seller's default catalog: - -- `protocols/media-buy/index.yaml` — seeds `sports_preroll_q2`, - `lifestyle_display_q2` + pricing. -- `protocols/media-buy/scenarios/delivery_reporting.yaml` — seeds - `outdoor_display_q2`, `outdoor_video_q2` + pricing. -- `protocols/media-buy/scenarios/governance_approved.yaml` — same pair. -- `specialisms/creative-generative/generative-seller.yaml` — same pair. -- `specialisms/sales-broadcast-tv/index.yaml` — seeds `primetime_30s_mf`, - `late_fringe_15s_mf` with broadcast-spot formats + unit pricing. -- `specialisms/sales-guaranteed/index.yaml` — seeds - `sports_preroll_q2_guaranteed`, `outdoor_ctv_q2_guaranteed` with - guaranteed-fixed CPM pricing. - -All pricing uses `fixed_price` (rather than `floor_price`) so the -non-auction bid-price requirement doesn't trip storyboards that omit -`bid_price`. - -Validated by overlaying the edited YAMLs into `@adcp/client`'s -compliance cache and re-running via `adcp storyboard run` — seed phase -fires end-to-end and `create_media_buy` no longer throws -`PRODUCT_NOT_FOUND`. diff --git a/.changeset/seller-status-persistence-docs.md b/.changeset/seller-status-persistence-docs.md deleted file mode 100644 index a4e2757fd0..0000000000 --- a/.changeset/seller-status-persistence-docs.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Add explicit seller implementation guidance: media buy `status` must be stored as a persisted database field and updated only via protocol events — not recomputed from flight-date arithmetic. Date logic cannot produce `paused`, `canceled`, or `rejected`; recomputing silently suppresses those states and breaks `valid_actions`. - -Added a normative note to `docs/media-buy/media-buys/index.mdx` (lifecycle states section) and a brief cross-referencing callout in `docs/building/implementation/seller-integration.mdx`. - -Closes #3028 diff --git a/.changeset/slash-command-dispatch.md b/.changeset/slash-command-dispatch.md deleted file mode 100644 index 5b8742449d..0000000000 --- a/.changeset/slash-command-dispatch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Migrate the manual triage nudge to `peter-evans/slash-command-dispatch`. Command is now `/triage [execute|clarify|defer]` (no more `@claude-triage` which collides with the `@claude` GitHub App autocomplete). Benefits: reactions on the triggering comment (👀 on ack, +1 on success, -1 on failure), clean separation between dispatcher and handler, extensible to future commands (`/rebase`, `/retest`, `/autofix`). - -Requires one new repo secret: `TRIAGE_DISPATCH_PAT` (PAT with `contents:write` + `issues:write` + `pull-requests:write` on the repo; GITHUB_TOKEN cannot fire `repository_dispatch`). diff --git a/.changeset/split-agent-context-and-lint.md b/.changeset/split-agent-context-and-lint.md deleted file mode 100644 index 8e78fe849d..0000000000 --- a/.changeset/split-agent-context-and-lint.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Split the agent-context snapshot in two: public `.agents/current-context.md` (factual status, safe to inject into Addie and quote in triage comments) vs. internal `.agents/internal-context.md` (editorial framing, narratives, stakeholder-sensitive phrasing — read by triage routines for richer decisions but never injected or quoted publicly). Add a CI lint at `.github/workflows/validate-agent-context.yml` that fails on prompt-injection markers, level-1 headings, oversized content, or control characters in the public file, and warns when additions look like they belong in the internal file instead. Update the context-refresh routine prompt to write both files and honor the split. This closes the persistent-injection path and the "reads weird to a cold prospect" concern raised in the Addie PR expert review. diff --git a/.changeset/stage-1-membership-invites-schema.md b/.changeset/stage-1-membership-invites-schema.md deleted file mode 100644 index 7803e08f14..0000000000 --- a/.changeset/stage-1-membership-invites-schema.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Add `membership_invites` table + `organizations.billing_address` JSONB column. Foundation for the upcoming "admin sends invitation, prospect self-serves agreement + invoice" flow that replaces the direct admin-invoice endpoint (see `.context/membership-invite-plan.md`). diff --git a/.changeset/stage-2-tighten-invoice-request.md b/.changeset/stage-2-tighten-invoice-request.md deleted file mode 100644 index c6ce53bcca..0000000000 --- a/.changeset/stage-2-tighten-invoice-request.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Tighten `/api/invoice-request` to require auth + org membership + signed agreement. Drops the free-text companyName/contactName/contactEmail inputs (which created orphan Stripe customers that webhooks couldn't link back to an org). Company name now comes from the org record, contact from the session. Billing address is stored on the org for future pre-fill. `createAndSendInvoice` also prefers the org's existing Stripe customer over dedup-by-email, fixing Stefan-style splits where auto-provisioned customers lacked org metadata. diff --git a/.changeset/stage-3-admin-membership-invite.md b/.changeset/stage-3-admin-membership-invite.md deleted file mode 100644 index e652da9ec3..0000000000 --- a/.changeset/stage-3-admin-membership-invite.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Replace the direct admin-invoice endpoint with a membership invitation flow. Admin picks a tier and enters a contact email; system creates an invite token, emails the prospect a link (`/invite/:token`), and no Stripe invoice is issued until the prospect signs in, accepts the membership agreement, and confirms billing. Kills `POST /api/admin/accounts/:orgId/invoice` (returns 410 on legacy prospects path). New endpoints: `POST /api/admin/accounts/:orgId/invite-membership`, `GET /api/admin/accounts/:orgId/invites`, `POST /api/admin/accounts/:orgId/invites/:token/revoke`. Removes the typo-prone free-text form that caused split Stripe customers. diff --git a/.changeset/stage-4-invite-acceptance-flow.md b/.changeset/stage-4-invite-acceptance-flow.md deleted file mode 100644 index 51485e97fa..0000000000 --- a/.changeset/stage-4-invite-acceptance-flow.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Prospect-facing invite acceptance flow. `/invite/:token` landing page + `GET /api/invite/:token` (public metadata) + `POST /api/invite/:token/accept` (authed, issues Stripe invoice, joins WorkOS org, records agreement, stores billing address). Completes the admin-invites-replace-direct-invoices chain. The prospect gets one link, signs in, confirms billing, and the invoice goes out. diff --git a/.changeset/stage-5-hardening.md b/.changeset/stage-5-hardening.md deleted file mode 100644 index b578721d01..0000000000 --- a/.changeset/stage-5-hardening.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Hardening pass on the membership invite chain after expert review. Invite accept never auto-grants `owner` (always `member`); `createAndSendInvoice` no longer overwrites the linked Stripe customer's email on reuse; concurrent-accept clicks converge via Stripe idempotency keys (`invite_`); server validates `agreement_version` against the current published agreement on both the invoice-request and invite-accept paths; two-step org updates merged into a single atomic write; billing address goes through a field-allowlist + length cap sanitizer before hitting the DB or Stripe; deleted-on-Stripe linked customers are cleared from the org row (instead of falling into the email-dedup path that caused the original bug); admin invite lowercases `contact_email` end-to-end; `invite.html` ships a `Referrer-Policy: same-origin` meta and blocks submit until the current agreement version has loaded. diff --git a/.changeset/storyboard-async-create-media-buy.md b/.changeset/storyboard-async-create-media-buy.md deleted file mode 100644 index bd5f3de467..0000000000 --- a/.changeset/storyboard-async-create-media-buy.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -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/.changeset/storyboard-floor-gate-dx.md b/.changeset/storyboard-floor-gate-dx.md deleted file mode 100644 index 42d9c763c0..0000000000 --- a/.changeset/storyboard-floor-gate-dx.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -CI: better DX when the Training Agent Storyboards floor gate trips. The `::error::` line now spells out the exact file + matrix key to edit, the storyboards.log is uploaded as an artifact on failure so you can see *which* storyboards regressed without rerunning locally, and the bash steps gain `set -euo pipefail` plus an explicit empty-check on metric extraction so a future log-format change fails loudly with a name instead of a cryptic `[: unary operator expected`. Adds a defense-in-depth `::warning::` (not a hard fail) when the framework-vs-legacy passing-step asymmetry inverts. Closes #3214. diff --git a/.changeset/storyboards-patch-clarify.md b/.changeset/storyboards-patch-clarify.md deleted file mode 100644 index f87ad9c76f..0000000000 --- a/.changeset/storyboards-patch-clarify.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -patch: clarify that storyboards/conformance-suite changes version independently of spec; deprecate `signed-requests` preview specialism (taxonomy correction) - -Two patches in one PR, both clarifications: - -**versioning.mdx clarification.** Adds a "Spec changes vs. conformance-suite changes" subsection making explicit that conformance-suite changes (storyboards, specialism taxonomy, scenario classifications, runner mechanics) version independently of spec and are patch-level by default. The release-vs-patch rules in the spec apply to wire-level artifacts under `static/schemas/source/` and normative prose in `docs/`. Storyboards under `static/compliance/source/` and the runner machinery are verification artifacts AAO maintains; they're not the spec. Includes a per-change-type table so authors can size correctly. - -**`signed-requests` preview specialism deprecated.** The `signed-requests` specialism YAML at `static/compliance/source/specialisms/signed-requests/index.yaml` is marked `status: deprecated` with an updated narrative explaining the reclassification. The conformance bar is unchanged; only the location moves. Tracked at #3075 (full reclassification to a universal capability-gated storyboard at `static/compliance/source/universal/signed-requests.yaml`, alongside the `request_signing.supported: true` capability advertisement). - -Why patch: -- The versioning.mdx change is policy clarification — no normative wire-level requirement changes. -- The signed-requests reclassification is `preview` status with no graded users today; the on-wire seller obligation (advertise `request_signing.supported: true`, implement the verifier per the security profile) is unchanged. Only the conformance runner taxonomy moves. - -The full reclassification (file move from `specialisms/` to `universal/`, test-kit refactor at `signed-requests-runner.yaml`, doc cross-reference updates in `release-notes.mdx`, `whats-new-in-v3.mdx`, `prerelease-upgrades.mdx`, `compliance-catalog.mdx`, etc.) ships as a follow-up patch under #3075. diff --git a/.changeset/streamline-dashboard-agents-cards.md b/.changeset/streamline-dashboard-agents-cards.md deleted file mode 100644 index ce779a838a..0000000000 --- a/.changeset/streamline-dashboard-agents-cards.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Streamline the dashboard agent cards and the Test-your-agent storyboard list: - -- Visibility is now a single-line dropdown (matching the "Every 12h" select) instead of three stacked card-radios. -- Pause/interval controls are merged with the actions row into one footer line. -- Each storyboard row is now a compact one-liner (title · step-count on top, summary truncated below) with subtle text-button actions and a small "Run" button instead of the oversized "Run step by step" primary button. - -Cuts each card and storyboard row to roughly half their previous vertical footprint without losing any functionality. diff --git a/.changeset/stripe-api-version-from-sdk.md b/.changeset/stripe-api-version-from-sdk.md deleted file mode 100644 index 9411c0c0dd..0000000000 --- a/.changeset/stripe-api-version-from-sdk.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Stripe client now reads its `apiVersion` from `Stripe.API_VERSION` instead of a pinned literal. Each Stripe SDK bump previously required a manual update to the literal in `server/src/billing/stripe-client.ts`, which was missed when the minor-and-patch group bumped Stripe and broke the build. diff --git a/.changeset/substitution-safety-template-pointer.md b/.changeset/substitution-safety-template-pointer.md deleted file mode 100644 index 6a3c420c35..0000000000 --- a/.changeset/substitution-safety-template-pointer.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -docs: point new specialism authors at the substitution-safety phase template (#2654 polish) - -Closes the last acceptance item on #2654 — a short "Adding a catalog- -substitution-safety phase to a new specialism" section in -`docs/contributing/storyboard-authoring.md` that directs authors at the -`phase_template:` block already shipped in -`static/compliance/source/test-kits/substitution-observer-runner.yaml`, -rather than copy-pasting from a sibling specialism. - -The template pattern, the `lint:substitution-vector-names` drift-guard, -and the NFC normalization rule all landed in PR #2656. This doc pointer -is the last polish item so authors reaching for a fourth consumer -(sales-retail-media, sales-broadcast-tv, etc.) find the template before -they clone. diff --git a/.changeset/sweep-grace-period.md b/.changeset/sweep-grace-period.md deleted file mode 100644 index ec380ec43f..0000000000 --- a/.changeset/sweep-grace-period.md +++ /dev/null @@ -1,3 +0,0 @@ ---- ---- - diff --git a/.changeset/sync-backfill-revenue-events.md b/.changeset/sync-backfill-revenue-events.md deleted file mode 100644 index 31e9bc652c..0000000000 --- a/.changeset/sync-backfill-revenue-events.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Extend `POST /api/admin/accounts/:orgId/sync` to backfill `revenue_events` rows for paid Stripe invoices that were never recorded (missed `invoice.paid` webhooks due to orphaned customer metadata). The backfill walks all paid invoices for the customer using paginated auto-iteration, upserts with `ON CONFLICT (stripe_invoice_id) DO NOTHING` so webhook-written rows win, and returns `revenue_events_synced: N` at the top level of the sync response. The admin sync UI now surfaces the count. Fixes silent MRR undercounting when a Stripe customer is re-linked after an orphan scenario. diff --git a/.changeset/tasks-get-include-result-docs-fix.md b/.changeset/tasks-get-include-result-docs-fix.md deleted file mode 100644 index 1a408ee8c2..0000000000 --- a/.changeset/tasks-get-include-result-docs-fix.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -docs(tasks/get): remove non-existent `include_result` flag from polling examples and clarify 3.0 completion-payload retrieval - -The `tasks/get` request schema (`static/schemas/source/core/tasks-get-request.json`) does not define an `include_result` field, and the response schema (`static/schemas/source/core/tasks-get-response.json`) does not define a `result` field. Five doc files were inviting buyers to send `include_result: true` and read a typed completion payload off the polled response — neither of which is supported by the 3.0 spec. - -Removed the spurious parameter from the polling examples in `task-lifecycle.mdx`, `async-operations.mdx` (two call sites), `error-handling.mdx`, and `orchestrator-design.mdx`. Added a note on the canonical `task-lifecycle` polling section stating that in 3.0, `tasks/get` returns task status and the completion payload (e.g. `media_buy_id`, `packages` from `create_media_buy`) is delivered via the seller's push notification to the buyer's webhook URL configured in `push_notification_config`. Buyers that need the completion payload MUST configure a webhook in 3.0; polling alone reports terminal status. - -A typed `include_result` request flag and a documented response projection on completion are tracked for 3.1 in #3123. This patch corrects the docs to match what 3.0 actually ships; the schema-additive fix is out of scope per the patch policy in `docs/reference/versioning.mdx` ("Patches never change schema — no new fields, no renamed fields, no new enum values"). diff --git a/.changeset/team-self-serve-member-admin.md b/.changeset/team-self-serve-member-admin.md deleted file mode 100644 index 4e394df093..0000000000 --- a/.changeset/team-self-serve-member-admin.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -feat(team): self-serve member admin in team.html, admins can change roles - -Closes the self-service gap left after #3235 — an org owner can now do everything from the team UI without filing an escalation. - -## What changed - -- **Team UI consolidates "invite" and "promote" into one "Add member" flow** (`server/public/team.html`). The modal posts to the unified `POST /api/organizations/:orgId/members/by-email` endpoint that walks the four-state machine (invite / create / update / no-op). The same modal handles adding a new teammate and promoting an existing member. -- **Auto-provision toggle in the Verified Domains card** — owners can flip `auto_provision_verified_domain` per org via the existing `PATCH /api/organizations/:orgId/settings`. Hidden when no verified domain exists. Owner-only (admins shouldn't be able to widen org membership unilaterally, especially now that admins can promote auto-joined members to admin). -- **Admins can change member roles** — `PATCH /api/organizations/:orgId/members/:membershipId` and Path 3 of `/members/by-email` now allow org admins to promote `member ↔ admin`. Caps in place: admins can't assign `owner`, can't change a current owner's role, and (matching the existing PATCH endpoint) no caller of either endpoint can change their own role. Owners and AAO super-admins are unrestricted. -- **`/members/by-email` accepts `seat_type`** — the unified endpoint is now a true superset of `/invitations`. seat_type is staged via `invitation_seat_types` for both Path 1 (invite) and Path 2 (direct add) so the `organization_membership.created` webhook hands the right seat_type to the local cache. Path 2 clears any stale staging rows for the same `(org, email)` pair before staging, so a prior failed direct-add can't pollute a later invite. - -## Tests - -- New `server/tests/integration/member-by-email-policy.test.ts` (16 tests) covers all role-cap branches, self-role-change blocking on both endpoints, seat_type propagation, and the owner-only auto-provision toggle. diff --git a/.changeset/test-create-github-issue.md b/.changeset/test-create-github-issue.md deleted file mode 100644 index d6803a0b0e..0000000000 --- a/.changeset/test-create-github-issue.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Let Addie file GitHub issues as the member via WorkOS Pipes instead of a bot token. `create_github_issue` now pulls a per-user GitHub access token from Pipes; if the user hasn't connected yet (or scopes changed) it returns a Connect URL alongside the `draft_github_issue` fallback. Member hub gains a Connections card with a "Connect GitHub" button backed by new `/api/me/connected-accounts/github` routes. diff --git a/.changeset/tmp-experimental-marker.md b/.changeset/tmp-experimental-marker.md deleted file mode 100644 index 5e534540ac..0000000000 --- a/.changeset/tmp-experimental-marker.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -Add `x-status: experimental` to all 9 TMP schemas and `core/seller-agent-ref.json` (exclusively referenced by TMP), completing the experimental-status contract already declared for `trusted_match.core` in `get_adcp_capabilities` and `experimental-status.mdx`. Mirrors the existing pattern on all 11 sponsored-intelligence schemas. Enables validators, doc generators, and tooling to identify TMP as an experimental surface. No wire-format or field changes. diff --git a/.changeset/tmp-seller-agent-on-available-package.md b/.changeset/tmp-seller-agent-on-available-package.md deleted file mode 100644 index 54d28d3cc9..0000000000 --- a/.changeset/tmp-seller-agent-on-available-package.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -**TMP: explicit seller-agent attribution on AvailablePackage.** - -Add `seller_agent: { agent_url, id? }` to the Trusted Match Protocol -AvailablePackage schema, making seller identity explicit on every -package cached by a TMP provider. The canonical identifier is the -seller's agent URL as declared in the property publisher's -`adagents.json` `authorized_agents[].url`; the reserved `id` slot is -forward-compatible with a future registry-assigned opaque identifier. - -- **`/schemas/core/seller-agent-ref.json`** — new shared schema - mirroring the `{agent_url, id?}` shape used by `format-id` and - `ProviderEntry`. -- **`/schemas/tmp/available-package.json`** — `seller_agent` added as - a required field. Lands as a patch under the experimental-surface - contract (`experimental_features: trusted_match.core`, which allows - breaking changes between 3.x releases with advance notice); sellers - syncing `AvailablePackage` payloads need to populate it going - forward. -- **`/schemas/tmp/offer.json`** — optional `seller_agent` echo so - publisher-side log pipelines can attribute offers to sellers - without round-tripping to the media-buy store. Non-authoritative: - the cached package binding remains source of truth; routers MAY - stamp the field on merge when providers omit it. -- **`/schemas/tmp/error.json`** — adds `seller_not_authorized` error - code for sync-time rejection when `seller_agent.agent_url` is not - present in the property publisher's adagents.json - `authorized_agents[].url` list. -- **`docs/trusted-match/specification.mdx`** — new "Package Sync" - section defines the sync contract, the SHOULD-level adagents.json - validation flow, explicit per-actor responsibilities (seller - agent, publisher, router, provider), and the "what this is not" - boundary (not a request-time filter, not a sellers.json bridge, - not a cryptographic attestation). Offer and Error tables updated - accordingly; definitions table gains a **Seller agent** entry. - -Seller identity lives on the cached `AvailablePackage`, not on -`context_match_request` or `identity_match_request`. Providers — -which have no access to a media-buy store — need provenance on the -wire they actually receive; putting it on the request would either -duplicate the sync-time binding or open a path for request-time -seller filtering that re-introduces the identity- and -allocation-leakage failure modes that package-set decorrelation -exists to prevent. Publishers and routers can derive seller identity -from `media_buy_id` against their own stores; providers cannot. - -TMP remains experimental under AdCP 3.x — schema additions here -follow the experimental-surface contract and do not bump the stable -AdCP major. The `SellerAgentRef.id` slot and optional `ext` namespace -leave room to layer signed seller claims or an AAO-assigned opaque -identifier without a rename later. diff --git a/.changeset/training-agent-dogfood-client-5-12-helpers.md b/.changeset/training-agent-dogfood-client-5-12-helpers.md deleted file mode 100644 index b784b74284..0000000000 --- a/.changeset/training-agent-dogfood-client-5-12-helpers.md +++ /dev/null @@ -1,53 +0,0 @@ ---- ---- - -Dogfood `@adcp/client` 5.13 seller helpers in the reference training -agent (closes adcontextprotocol/adcp#2889). - -Pins `@adcp/client` to `5.13.0` (not `^5.13.0`) because 5.14 regressed -the storyboard runner — filed upstream as -adcontextprotocol/adcp-client#866. The adoptions below all exist in -5.11+ / 5.13.0. - -- **`wrapEnvelope`** replaces hand-rolled sibling-field emission - (`replayed` / `context` / `operation_id`) in - `framework-server.ts`'s `toAdaptedResponse` / `serviceUnavailable` / - `versionUnsupported`. -- **Session-scoped `testController.getSeededProducts` callback** wires - `comply_test_controller.seed_product` fixtures through `get_products` - responses on sandbox requests — new behavior that closes a latent - gap where seeded products never surfaced through the buyer-facing - tool. `SEED_PRODUCT_DEFAULTS` provides the schema-minimum baseline - so sparse fixtures still pass response validation. 5.14 would - collapse this to a `bridgeFromSessionStore(...)` one-liner; deferring - that follow-up until the 5.14 storyboard regression resolves. -- **`mergeSeedProduct`** replaces the shallow-spread merge in - `overlaySeededProducts`, gaining permissive-leaf semantics and by-id - `pricing_options` overlay. - -Not adopted — blocked on 5.14 follow-up (adcp-client#866): - -- `bridgeFromSessionStore` (5.14-only) — would replace the hand-rolled - callback with a one-liner. -- `mcpAcceptHeaderMiddleware` from `@adcp/client/express-mcp` — 5.14 - fixed the `rawHeaders` patch gap (adcp-client#825/#830). 5.13's - version only mutates `req.headers.accept`, which doesn't propagate - through `StreamableHTTPServerTransport`'s `@hono/node-server`-backed - Fetch Request. Keeping the inline dual-surface rewrite in - `index.ts` until the bump. -- Deleting `conflict-envelope.ts` + `wrapResponseForConflictRedaction` - — 5.14 ships `sanitizeAdcpErrorEnvelope` in the dispatcher, making - the wire-layer redactor redundant. Kept in 5.13 because - `adcpError()` still auto-injects `recovery` on `IDEMPOTENCY_CONFLICT` - envelopes. - -Also deferred (not blocked on 5.14): migrating `comply_test_controller` -off `customTools` onto `registerTestController` for its auto-emitted -`capabilities.compliance_testing.scenarios` block. Out of scope here. - -Guard rails added as comments (no code changes): the bridge-wiring -security posture (single-gate sandbox with no `resolveAccount`, -fixture data non-sensitive by design) and the custom-tool sanitizer -bypass (`handleComplyTestController` sidesteps the dispatcher's -`sanitizeAdcpErrorEnvelope` — today no `adcp_error` is emitted there, -but a future edit would need to route through `adcpError()`). diff --git a/.changeset/training-agent-drop-signed-requests-specialism.md b/.changeset/training-agent-drop-signed-requests-specialism.md deleted file mode 100644 index a4b1044c3d..0000000000 --- a/.changeset/training-agent-drop-signed-requests-specialism.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -chore(training-agent): drop deprecated `signed-requests` specialism claim - -Follow-up to #3077: the training agent's capability advertisement no longer claims `specialisms: ['signed-requests']`. Per the universal storyboard's narrative, advertising `request_signing.supported: true` is now the sole correct mechanism — the deprecated specialism claim is redundant and the runner emits an informational notice when it sees one. The training agent is the dogfood reference, so it goes first. - -Touches both dispatch paths (`task-handlers.ts` legacy and `framework-server.ts` framework-default) plus the integration tests that previously asserted the deprecated claim was present. Test fixture in `tests/lint-storyboard-test-kits.test.cjs` updated from `applies_to.specialism: 'signed-requests'` to `applies_to.universal_storyboard: 'signed-requests'` to match the post-reclassification syntax. - -No protocol surface change — the spec already deprecated the enum value in #3076 and #3077. diff --git a/.changeset/training-agent-force-create-media-buy-arm.md b/.changeset/training-agent-force-create-media-buy-arm.md deleted file mode 100644 index d500bfb1f3..0000000000 --- a/.changeset/training-agent-force-create-media-buy-arm.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -training-agent: implement `force_create_media_buy_arm` controller scenario + storyboard CI fixes - -Wires the training-agent (this repo's reference seller) to handle the `force_create_media_buy_arm` scenario added in #3104. The `create_media_buy_async` storyboard now grades **passing** instead of `not_applicable` against the training-agent — the conformance suite catches submitted-envelope wire-shape regressions for real, not just on paper. - -**Controller scenario.** `handleComplyTestController` pre-dispatches `force_create_media_buy_arm` before delegating to the SDK's `handleTestControllerRequest` (the SDK's enum is closed; new scenarios from spec PRs land in the wrapper until the SDK adopts them). The handler implements `arm: 'submitted'` only — `arm: 'input-required'` is reserved in the spec but cannot be expressed on a conformant `create-media-buy-response.json` today (`INPUT_REQUIRED` is a task-status, not a value in the canonical error-code enum, and the response schema has no fourth `oneOf` branch for an input-required envelope). The training-agent rejects that arm with `INVALID_PARAMS` until the spec resolves it. The handler validates `task_id` (required for submitted, max 128 chars), `message` (max 2000 chars), and writes to a new single-shot `forcedCreateMediaBuyArm` slot on `session.complyExtensions`. `list_scenarios` is augmented post-SDK-dispatch to include the new scenario name so storyboards detect support. - -**Directive consumption.** `handleCreateMediaBuy` reads-and-clears the directive at the top of the handler — before account-status, governance, and idempotency gates — and returns the submitted task envelope (`status: 'submitted'`, `task_id`, optional `message`). Single-shot — the second `create_media_buy` from the same session resumes default behavior. Buyer-side `idempotency_key` replay still wins because the SDK's request-idempotency cache wraps this handler. - -**Session persistence.** `forcedCreateMediaBuyArm` lands in `ComplyExtensions` (alongside the seed / account-status / etc. fields) so it survives the structuredSerialize/Deserialize round-trip every request does. `state.ts`'s `deserializeSession` carries it through explicitly. - -**CI overlay fix.** `.github/workflows/training-agent-storyboards.yml` now mirrors *new* source files onto the SDK cache, not just edits to existing ones. The previous `if [ -f $DST/file ]` gate skipped any file the cache didn't already have — which silently meant new storyboards never ran in CI until the SDK published a fresh cache. Mirror the source tree fully and let `mkdir -p` create any new subdirectories on the cache side. - -**CI baselines bumped.** -- Legacy: `52 → 53` clean storyboards, `380 → 384` passing steps (the new scenario adds 4 passing steps). -- Framework: `41 → 42` clean storyboards, `370 → 374` passing steps (same delta; framework stays below legacy due to the unrelated 5.17.0 sync_plans / property-list regression tracked at adcontextprotocol/adcp-client#940). - -**Tests.** New `server/tests/unit/training-agent-force-create-media-buy-arm.test.ts` covers: directive registration for the submitted arm, INVALID_PARAMS for `input-required` (with explanatory error_detail), out-of-spec arm values, missing/over-length task_id, round-trip into the submitted envelope, single-shot semantics, overwrite-before-consume, and `list_scenarios` advertisement. - -Why patch: training-agent-only implementation work, no spec changes. Closes the loop on #3081 — the scenario originally graded `not_applicable` until at least one reference seller implemented the controller scenario; this is that implementation. diff --git a/.changeset/training-agent-force-task-completion.md b/.changeset/training-agent-force-task-completion.md deleted file mode 100644 index a5e04f0636..0000000000 --- a/.changeset/training-agent-force-task-completion.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -patch: training-agent implements force_task_completion - -Wires this repo's reference seller to the `force_task_completion` controller scenario from #3138 — the spec primitive that resolves a previously-submitted task to `completed` with a buyer-supplied result payload. Companion to #3115 (`force_create_media_buy_arm`); rebased onto #3191 (the `@adcp/client` 5.18.0 bump that ships `PostgresTaskStore.createTask({ taskId })`). - -**Controller scenario.** `handleComplyTestController` pre-dispatches `force_task_completion` before delegating to the SDK (the SDK's `CONTROLLER_SCENARIOS` enum is closed; new spec scenarios live in the wrapper until adopted upstream). Validates `task_id` (non-empty, ≤128 chars), `result` (plain object), and a soft 256 KB cap on the payload. Records `(task_id, result, ownerKey)` in a process-global Map so cross-account replays return `NOT_FOUND` (per the spec MUST), identical-params replays are idempotent no-ops, and diverging-params replays against a terminal task return `INVALID_TRANSITION`. `list_scenarios` is augmented to advertise the new scenario. - -**Why a local Map and not the SDK task store.** 5.18.0 ships `PostgresTaskStore.createTask({ taskId })` — meaningful progress, but two adjacent gaps still block SDK-backed roundtripping: - -1. `InMemoryTaskStore` (re-exported from `@modelcontextprotocol/sdk`) doesn't yet honor caller-supplied IDs. Training-agent CI without `DATABASE_URL` falls back to InMemory, so an SDK-backed implementation would silently get random IDs there. -2. The SDK auto-registers `tasks/get` with the MCP `Task` shape (`taskId`, status ∈ `working|completed|failed|input_required|cancelled`); the AdCP `tasks-get-response.json` schema requires the AdCP shape (`task_id`, status ∈ `submitted|working|input-required|completed|canceled|...`, plus `task_type`, `protocol`, `result`). A storyboard polling phase asserting AdCP fails against the SDK's auto-registered handler. - -Both tracked in adcp-client#994. The local-Map primitive ships the spec contract sellers must honor (cross-account NOT_FOUND, replay idempotency, terminal INVALID_TRANSITION) without depending on infrastructure that isn't there yet. Swapping the storage layer is mechanical when upstream lands. - -**Storyboard extension still deferred.** `create_media_buy_async.yaml` remains v1.0.0 (submitted-arm only). The polling phase needs Gap 2 to land plus storyboard-runner wiring to thread caller-supplied IDs through tool input. - -**Tests.** New `server/tests/unit/training-agent-force-task-completion.test.ts` (9 tests): directive registration, INVALID_PARAMS for missing/oversized fields, replay idempotency, diverging-replay INVALID_TRANSITION, cross-account NOT_FOUND, list_scenarios advertisement. `comply-test-controller.test.ts` length assertion bumped 8→9. diff --git a/.changeset/training-agent-webhook-principal-scope.md b/.changeset/training-agent-webhook-principal-scope.md deleted file mode 100644 index f794b82d95..0000000000 --- a/.changeset/training-agent-webhook-principal-scope.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Training agent now scopes webhook `operation_id` (and therefore the receiver-facing `idempotency_key`) by the caller's principal — same scoping the request-side idempotency cache already applies. Closes a cross-tenant collision on the shared sandbox token where two callers landing on the same deterministic response entity id would emit webhooks with identical idempotency_keys; receivers that dedupe across tenants on `idempotency_key` now see distinct events for distinct callers. The framework path delegates to `scopedPrincipal` so the partition format is defined in one place and cannot drift from the request-side cache. Fixes #2871. diff --git a/.changeset/triage-already-engaged.md b/.changeset/triage-already-engaged.md deleted file mode 100644 index ed45131b26..0000000000 --- a/.changeset/triage-already-engaged.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Two corrections from the second live v2 run of the AdCP issue-triage routine: - -1. **Already-engaged check.** The bot posted competing flag-for-review comments on issues `#2902` and `#2903`, which the maintainer was actively working on in a Conductor workspace — invisible to GitHub, so the bot thought they were untriaged. Add a check before expert consultation: silent-defer when the issue is assigned to a repo member, has an open PR referencing it, or has a repo-member comment in the last 7 days. - -2. **Tighten "never create labels".** The first v2 run ended with a `compliance-suite` label on two issues that had no description — likely bot-created despite the existing rule. Reinforce: the routine must run `gh label list` first and apply only labels whose names appear in the output. If a bucket doesn't have a matching label, put the bucket name in the comment body and flag the gap in the run summary — don't create the label. diff --git a/.changeset/triage-build-test-gate.md b/.changeset/triage-build-test-gate.md deleted file mode 100644 index 7ce30b081b..0000000000 --- a/.changeset/triage-build-test-gate.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Triage routine now runs a mandatory pre-PR build + test gate (npm run precommit or equivalent) before expert review, with 2 build→fix iterations. Also elevates bullet-label boundary framing ("**X gaps**") from CI warning to hard error in the current-context lint, and adds an explicit rule to the context-refresh prompt. diff --git a/.changeset/triage-bundle-dont-split.md b/.changeset/triage-bundle-dont-split.md deleted file mode 100644 index 4b70d4dd57..0000000000 --- a/.changeset/triage-bundle-dont-split.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Teach the triage routine to bundle ready items from a multi-item issue into one PR rather than splitting the issue into peer child issues. When a maintainer files a follow-up list, the bot opens one cohesive PR for everything that's ready and leaves the parent issue open with a comment about what shipped vs. what remains. Issue-to-issue splits only happen via explicit human "convert to epic" escalation — never autonomously. Reduces GitHub-click proliferation and keeps PR reviews cohesive. diff --git a/.changeset/triage-comment-trigger-and-lifecycle-label.md b/.changeset/triage-comment-trigger-and-lifecycle-label.md deleted file mode 100644 index ec380ec43f..0000000000 --- a/.changeset/triage-comment-trigger-and-lifecycle-label.md +++ /dev/null @@ -1,3 +0,0 @@ ---- ---- - diff --git a/.changeset/triage-defer-flavors.md b/.changeset/triage-defer-flavors.md deleted file mode 100644 index ec380ec43f..0000000000 --- a/.changeset/triage-defer-flavors.md +++ /dev/null @@ -1,3 +0,0 @@ ---- ---- - diff --git a/.changeset/triage-experimental-bump-downgrade.md b/.changeset/triage-experimental-bump-downgrade.md deleted file mode 100644 index 837024f848..0000000000 --- a/.changeset/triage-experimental-bump-downgrade.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Triage routine now applies the experimental-surface bump downgrade per docs/reference/experimental-status.mdx: changes scoped to surfaces marked `x-status: experimental` (or under known-experimental paths like `static/schemas/source/tmp/**`, `sponsored-intelligence/**`, `a2ui/**`) ship one bump level lower — minor → patch, major → minor, patch stays patch. Mixed stable+experimental diffs take the stable bump. diff --git a/.changeset/triage-milestone-branch-routing.md b/.changeset/triage-milestone-branch-routing.md deleted file mode 100644 index 027e78b57e..0000000000 --- a/.changeset/triage-milestone-branch-routing.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Triage routine now routes every drafted PR to the correct milestone and base branch based on the changeset bump level: major → next-major milestone on main; minor → next-minor milestone (e.g., 3.1.0) on main; patch → patch milestone on the X.Y.x branch (flag-for-human if no patch branch is open yet); --empty → no milestone, main. Replaces the conservative "only milestone on explicit signal" rule. diff --git a/.changeset/triage-pr-comments.md b/.changeset/triage-pr-comments.md deleted file mode 100644 index c586f9fdaa..0000000000 --- a/.changeset/triage-pr-comments.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Triage workflow now fires on PR comments as well as issue comments. The previous `github.event.issue.pull_request == null` filter routed PR feedback into a non-existent "auto-fix" workflow, so review comments on PRs sat unactioned until someone manually invoked `/triage`. The routine now receives an `is_pr: true` flag plus a `pr` block (head_ref, base_ref, draft, state) and a MODE directive telling it to treat new PR comments as actionable feedback (apply a follow-up commit on the PR's head branch, or post a reply if the comment is a question). Self-loop guard widened to also skip comments containing "Fixed by Claude Code" so PR-fix replies don't re-trigger. diff --git a/.changeset/triage-pr-ergonomics.md b/.changeset/triage-pr-ergonomics.md deleted file mode 100644 index ec380ec43f..0000000000 --- a/.changeset/triage-pr-ergonomics.md +++ /dev/null @@ -1,3 +0,0 @@ ---- ---- - diff --git a/.changeset/triage-pre-pr-review.md b/.changeset/triage-pre-pr-review.md deleted file mode 100644 index 3952e29df8..0000000000 --- a/.changeset/triage-pre-pr-review.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Triage routine now runs a mandatory pre-PR expert review on the diff (code-reviewer + domain expert in parallel) before opening the PR, capped at 2 review→fix iterations. Sign-offs recorded in the PR body. diff --git a/.changeset/triage-race-and-coverage.md b/.changeset/triage-race-and-coverage.md deleted file mode 100644 index 4299e86dfa..0000000000 --- a/.changeset/triage-race-and-coverage.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Close two gaps from the first live v2 run of the issue-triage routine: - -1. **Concurrency race.** Cron + manual + bridge can all fire near-simultaneously and walk the same `claude-triaged`-less queue, producing duplicate triage comments on the same issue. Add a pre-work check: if any `## Triage` comment was posted on this issue in the last 10 minutes, skip. One-API-call distributed lock. - -2. **Synthesis coverage gaps.** LLM sampling variance + per-expert prompt scope freedom meant single runs missed angles that a second run then surfaced (the two racing comments on #2915 were genuinely complementary — not identical). Add a coverage checklist per bucket: before writing the comment, verify the synthesis touches each applicable dimension (operator reality, codebase coherence, industry precedent, migration cost, governance) and loop back to the relevant expert if a material dimension is missing. For RFC / epic / cross-cutting issues, consider spawning 2× per expert type in parallel. diff --git a/.changeset/triage-recovery-and-local.md b/.changeset/triage-recovery-and-local.md deleted file mode 100644 index ec380ec43f..0000000000 --- a/.changeset/triage-recovery-and-local.md +++ /dev/null @@ -1,3 +0,0 @@ ---- ---- - diff --git a/.changeset/triage-ship-more.md b/.changeset/triage-ship-more.md deleted file mode 100644 index 7cddbb3ee3..0000000000 --- a/.changeset/triage-ship-more.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Flip the triage routine's default from "flag unless obviously a small bug" to "execute unless the change is breaking, ambiguous, or high-risk." Drops the under-150-line scope cap, the classification-only-Bug-or-Doc gate, and the blanket prohibition on `static/schemas/source/**` edits. Adds a crisp non-breaking-vs-breaking definition as the primary Execute/Flag binary, adds evergreen content as a first-class always-PR-able bucket, and guards `infra/agents` bucket as always-Flag (self-modification risk). CODEOWNERS + human review still gate every merge. diff --git a/.changeset/triage-show-json-options.md b/.changeset/triage-show-json-options.md deleted file mode 100644 index 74db759be1..0000000000 --- a/.changeset/triage-show-json-options.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Triage routine's ready-for-human comments on spec/protocol/registry/addie issues must now include a JSON (or diff/yaml/http) wire-format snippet per option path, so a non-expert can decide without reading the spec. Comment char cap lifted when examples are required. diff --git a/.changeset/triage-silent-path-and-prereq.md b/.changeset/triage-silent-path-and-prereq.md deleted file mode 100644 index 899454ac5e..0000000000 --- a/.changeset/triage-silent-path-and-prereq.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Two follow-ups to the AdCP issue-triage routine prompt: - -1. Add a "silent triage" path: when the routine classifies an issue as RFC / Epic / Feature / Discussion AND the author is an established contributor AND the body is well-structured AND the issue already carries an on-target label, apply `claude-triaged` + matching bucket labels silently without posting a comment. A triage comment that restates the issue and says "ready-for-human" is pure noise — the structured label carries the same signal. - -2. Document the `claude-triaged` label as a prerequisite (chicken-and-egg: the prompt requires applying it + forbids creating labels, so the label must pre-exist). diff --git a/.changeset/triage-v2-expert-consultation.md b/.changeset/triage-v2-expert-consultation.md deleted file mode 100644 index e7177e3bb3..0000000000 --- a/.changeset/triage-v2-expert-consultation.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Rewrite the issue-triage routine prompt (v2) around expert consultation. The routine now spawns bucket-specific expert subagents (ad-tech-protocol, adtech-product, code-reviewer, prompt-engineer, user-engagement, education, internal-tools, dx, docs, security) from `.claude/agents/`, synthesizes their input, and lands one of four outcomes: clarify (ask maintainers), flag-for-review (surface to @bokelley), execute PR, or defer (post-cycle work, silent). Drop the silent-triage default and the NONE-author PR gate — drive-by bugs can now become draft PRs when small and correct; CODEOWNERS still gates merge. Relevance check uses milestones + active PRs + recent merges + issue text + current-context snapshot, not a single source. diff --git a/.changeset/triton-promote-hayem.md b/.changeset/triton-promote-hayem.md deleted file mode 100644 index 817616c903..0000000000 --- a/.changeset/triton-promote-hayem.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -fix(team): static admin API key passes the AAO super-admin check on member endpoints - -The new `/members/by-email` and `PATCH /:orgId/settings` endpoints used `isWebUserAAOAdmin(user.id)` for the super-admin override, which checks aao-admin working-group membership. The static admin API key (`ADMIN_API_KEY` env var, used by internal tooling and incident scripts) authenticates with a synthetic user id `admin_api_key` that isn't a real WorkOS user, so the working-group check returns false. - -Both endpoints now also accept `req.isStaticAdminApiKey === true` (set by `requireAuth`) as super-admin equivalent — same posture every other admin-tooling-facing endpoint already takes. The `/members/by-email` path additionally skips the WorkOS `listOrganizationMemberships` lookup for the static-admin-API-key user, since the synthetic id has no memberships to find. - -Adds `scripts/incidents/2026-04-triton-promote-hayem.ts` to resolve escalation #285 — a one-shot script that POSTs to `/api/organizations/org_01KC80TYK2QPPWQ7A8SGGGNHE7/members/by-email` for `raphael.hayem@tritondigital.com` as `admin`, walking whichever state the user is currently in. diff --git a/.changeset/unskip-integration-test-cluster.md b/.changeset/unskip-integration-test-cluster.md deleted file mode 100644 index e312524965..0000000000 --- a/.changeset/unskip-integration-test-cluster.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -ci: un-skip 11 of 13 integration test files tracked in #3289. Auth-mock cluster moves to `vi.mock(path, async (importOriginal) => ({ ...await importOriginal(), }))` so HTTPServer setup finds the real `optionalAuth` and friends; CSRF middleware is mocked alongside auth so POST/PUT/DELETE tests stop hitting `403 forbidden`. The two `--exclude`'d files (`admin-sync-revenue-backfill`, `self-service-delete`) get `vi.hoisted` for shared mock state and class-form WorkOS mocks, so they load and run instead of erroring at module-eval. `health.test.ts` drops `(server as any).registry.initialize()` (the property is gone). `schema-routing.test.ts` re-aligns with `semver.rcompare` ordering. `digest-email-recipients` and `member-db` reconcile drifted assertions. The `--exclude` flags are removed from `test:server-integration`, and `WORKOS_COOKIE_PASSWORD` is set in the test env so the WorkOS client constructs in tests that need it. **+131 tests now run in CI**; remaining skips are documented in #3289 with concrete next steps. diff --git a/.changeset/unskip-mcp-protocol-tests.md b/.changeset/unskip-mcp-protocol-tests.md deleted file mode 100644 index dd1fd90a01..0000000000 --- a/.changeset/unskip-mcp-protocol-tests.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -test(integration): un-skip mcp-protocol — closes #3317. Adds an SSE-parsing `callMcp` helper (the StreamableHTTP transport requires `Accept: text/event-stream` and frames JSON-RPC responses as `event: message\ndata: {...}` even for unary calls). Mocks `express-rate-limit` to passthrough so 22 sequential calls don't trip the inline `mcpRateLimiter` (max: 10/min). Loosens the `toHaveLength(32)` count assertion to `length > 0` so future tool additions don't break the file. Realigns three tests from JSON-RPC `error` envelopes to the spec-correct `result.isError` / structured-content shape — tools/call carries tool-execution errors in `result`, not in `error`. Drops the stale `(server as any)['app']` private-property access; `server.app` is the public getter. Adds `afterAll(() => server.stop())` to close the open HTTP handle. diff --git a/.changeset/url-canonicalization-reference.md b/.changeset/url-canonicalization-reference.md deleted file mode 100644 index 25612e3d57..0000000000 --- a/.changeset/url-canonicalization-reference.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -**URL canonicalization: one authoritative reference for every URL-as-identifier comparison in AdCP.** - -The canonicalization algorithm previously lived only under the request-signing profile in `docs/building/implementation/security.mdx`, but AdCP compares URLs as identifiers in many other places — TMP seller authorization (`seller_agent.agent_url` vs `authorized_agents[].url`), TMP provider resolution (`ProviderEntry.agent_url`), `format-id.agent_url` equivalence, and signal/feature agent lookups in `adagents.json`. Schemas today said "exactly as declared," which reads as byte-equality; two URLs that differ only in case, default port, or percent-encoded unreserved characters would silently miss the match. - -This change moves the algorithm to a first-class reference page and links every consuming surface to it, so the same canonicalization binds everywhere. - -- **New `docs/reference/url-canonicalization.mdx`** — the authoritative home of the 8-step algorithm (RFC 3986 §6.2.2 + §6.2.3, UTS-46 Nontransitional IDN pin, IPv6 zone-identifier rejection, enumerated malformed-authority cases), a "where it applies" table covering signing / TMP seller authorization / TMP provider resolution / `adagents.json` lookups / `format-id` / `authoritative_location` indirection, a "signing profile extensions" note for the transport-only bits, and a common-pitfalls list. -- **`docs/building/implementation/security.mdx`** — `@target-uri` section now cites the reference page instead of restating the eight steps. Keeps only the signing-specific extensions (HTTP/2 `:authority` derivation, dual-header rejection, `request_target_uri_malformed` error, cross-vhost replay gate). Removes the drift risk between two copies. -- **`static/schemas/source/core/seller-agent-ref.json`** — `agent_url` description replaces "exactly as declared" with canonicalization-based comparison. Also drops the "in production" weasel on HTTPS — the scheme requirement is now unconditional. -- **`static/schemas/source/adagents.json`** — all six `url` descriptions updated: the four `authorized_agents[].url` variants, plus the two signals-authorization variants (`signal_ids`, `signal_tags`) and the property-features variant. -- **`static/schemas/source/core/format-id.json`** — `agent_url` description updated to require canonicalization. -- **`static/schemas/source/tmp/provider-registration.json`** — `endpoint` description extends the existing SSRF/DNS-rebinding language with a canonicalization rule for provider-registry de-duplication. -- **`docs/trusted-match/specification.mdx`** — TMP Sync-Time Validation step 2 links canonicalization rules explicitly and adds an explicit `https://`-only rejection (non-HTTPS seller URLs get `seller_not_authorized`, closing the scheme-mismatch bypass). ProviderEntry table row links the canonicalization rules for provider comparison. -- **`docs.json`** — reference page added to both primary and legacy sidebars adjacent to `versioning` (other interop-rules references). - -No schema shape changes. Descriptions only. Schema link style follows the repo convention (`See docs/` bare, no backticks or leading slash). diff --git a/.changeset/v3-envelope-integrity-conformance.md b/.changeset/v3-envelope-integrity-conformance.md deleted file mode 100644 index 7584b87355..0000000000 --- a/.changeset/v3-envelope-integrity-conformance.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"adcontextprotocol": patch ---- - -feat(compliance): v3 envelope integrity universal storyboard - -Adds `static/compliance/source/universal/v3-envelope-integrity.yaml` — a universal storyboard (applies to all agent interaction models) that asserts the v3 `status` field is present on the response envelope and that the legacy v2 `task_status` / `response_status` field names are absent. - -Schema-level enforcement of the prohibition is provided separately by `envelope-forbid-legacy-status-fields.md` (top-level `not: { anyOf: [{ required: [task_status] }, { required: [response_status] }] }` on `protocol-envelope.json`). This changeset is the runtime/storyboard counterpart. - -The explicit envelope-root field-absence assertions are wired as TODO `field_absent` checks pending runner support in `@adcp/client`; the immediate enforcement path remains the schema-level constraint, which any schema-aware validator detects without runner-specific primitives. Closes #3041 at the storyboard layer. diff --git a/.changeset/webhook-side-subscription-dedup.md b/.changeset/webhook-side-subscription-dedup.md deleted file mode 100644 index 34d25b3590..0000000000 --- a/.changeset/webhook-side-subscription-dedup.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Webhook-side dedup on `customer.subscription.created`: cancel a duplicate subscription with proration and skip the org-row UPDATE when the customer already has another live sub. Closes the cross-path race the intake-side guards (active-subscription guard, advisory lock) can't catch — Stripe Checkout creates a session URL, not a subscription, so two concurrent intake paths can both pass guards before either mints a sub. Logs and alerts ops; failures fall through (audit invariant catches misses). diff --git a/.changeset/wg-community-marketing-rename.md b/.changeset/wg-community-marketing-rename.md deleted file mode 100644 index 41069fdc29..0000000000 --- a/.changeset/wg-community-marketing-rename.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Rename the Community & Events WG to "Community & Marketing" with an expanded description covering events, marketing, content, education, and thought leadership — matching the scope of the active `#wg-community-events-mktg-education` channel. Also update Creative's description to avoid word collision with the separate Governance WG: "governance" → "review and approvals". Both are one-line display changes in migration 425. diff --git a/.changeset/wire-server-integration-tests-into-ci.md b/.changeset/wire-server-integration-tests-into-ci.md deleted file mode 100644 index e9a118c575..0000000000 --- a/.changeset/wire-server-integration-tests-into-ci.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -ci: run `server/tests/integration/` in `build-check.yml` against a Postgres service container so PRs that break integration tests fail their checks. Adds a new `test:server-integration` script and skips currently-broken suites under #3289 / #3080 with comments pointing at the umbrella issue. Closes #3094. diff --git a/.changeset/workflow-b-admin-mark-linkedin.md b/.changeset/workflow-b-admin-mark-linkedin.md deleted file mode 100644 index 765194f866..0000000000 --- a/.changeset/workflow-b-admin-mark-linkedin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- ---- - -**Workflow B Stage 3 — admin "Mark posted to LinkedIn" action** - -Closes the Workflow B loop: admins who post to LinkedIn *outside* Slack -can now click "Mark posted to LinkedIn" on the admin account detail page -and record the same `announcement_published` (channel=linkedin) row that -the Slack button writes. - -**Shared critical section.** `markLinkedInPosted(orgId, actor)` is -extracted from the Slack Bolt handler (Stage 2) and called by both the -handler and the new HTTP route. Both paths hold the same advisory lock -keyed on `(orgId, 'mark_linkedin')` so admin double-clicks, Slack -retries, and cross-surface races converge on a single INSERT. - -**Actor identity.** Metadata now discriminates the actor source — Stage -2 rows write `marked_by_slack_user_id` + `marked_via: 'slack'`; Stage 3 -rows write `marked_by_workos_user_id` + `marked_via: 'admin'`. The review -card render path shows a clickable `<@U…>` mention for Slack-originated -marks and a plain-text "an AAO admin" for admin-UI marks (Slack can't -resolve WorkOS ids; leaking internal ids into a shared channel isn't -something we want). Legacy rows (no `marked_via`) are treated as -Slack-originated for back-compat. - -The `announcement_skipped` and `announcement_published (slack)` rows -follow the same shape going forward: `skipper_slack_user_id` / -`approver_slack_user_id` with a matching `_via` field. - -**Changes:** - -- `server/src/addie/jobs/announcement-handlers.ts` — extracts the shared - `markLinkedInPosted`; refactors `AnnouncementState` to carry tagged - `StoredActor` objects instead of raw Slack user ids; adds - `renderActorMention` helper; exports `loadDraftAndState`. -- `server/src/routes/admin/accounts.ts` — adds `POST /api/admin/accounts/:orgId/announcement/linkedin`; extends the account detail GET response with an `announcement` object. -- `server/public/admin-account-detail.html` — new "New-member - announcement" collapsible card. Shows current state and surfaces - the Mark-LI button when `slack_posted && !linkedin_posted && !skipped`. - -6 new shared-function tests cover admin actor, Slack actor, legacy row -back-compat, and the three refuse paths. 86/86 announcement tests pass; -server typecheck clean. diff --git a/.changeset/workflow-b-announcement-handlers.md b/.changeset/workflow-b-announcement-handlers.md deleted file mode 100644 index 1a445b46f9..0000000000 --- a/.changeset/workflow-b-announcement-handlers.md +++ /dev/null @@ -1,67 +0,0 @@ ---- ---- - -**Workflow B Stage 2 — announcement review button handlers** - -Wires the three review-card buttons posted by Stage 1 to action handlers: - -- `Approve & Post to Slack` publishes the approved draft + visual to the - configured public announcement channel, records an - `announcement_published` activity row (channel=slack), and re-renders - the review card to show Slack done + LinkedIn pending. -- `Mark posted to LinkedIn` records `announcement_published` - (channel=linkedin) for the external post; re-renders the card to - show both channels done. -- `Skip` records `announcement_skipped` and removes the action buttons. - -**State-driven rendering.** Each handler loads the current state from -`org_activities` and re-renders the card from scratch on every click. -Re-clicks, lost acks, and out-of-order events converge to the same -result instead of drifting. - -**Idempotency + unwind.** `approve_slack` uses Stage 1's post-then-record -ordering: if the activity write fails after a successful public post, -`chat.delete` unwinds the message so the next retry re-publishes cleanly -instead of leaving an orphan announcement with no idempotency row. An -existing `announcement_published` row short-circuits further posts. - -**Admin gate.** Every action is gated on `isSlackUserAAOAdmin`. Non-admins -get an ephemeral rejection. - -**New config.** Adds `announcement_slack_channel` to `system_settings` -with `PUT /api/admin/settings/announcement-channel`. Unlike the other -channel settings this one requires a *public* channel — a private one -would defeat the point of a broad welcome. The `/slack-channels` picker -accepts `?visibility=public` for public-channel pickers. - -**Hardening (second-round review).** -- Each state-changing handler now runs inside a transaction holding a - Postgres advisory lock keyed on `(orgId, action)`. Two rapid clicks - (admin double-click, or Slack's 3s-no-ack retry) serialize — the - second caller observes the row written by the first and falls - through the idempotent "already done" branch instead of producing a - duplicate public post. -- `buildPublicAnnouncementPayload` re-validates `visual_url` through - `isSafeVisualUrl` before forwarding to the public channel. Stage 1 - validated at write time; a row inserted through a non-drafter path - (manual SQL, future admin tool, migration) can no longer flow an - attacker-chosen URL into Slack. -- Public-post text runs through a URL scrubber that replaces bare - `https://…` URLs not on AAO's host with `[link removed]`. The - drafter prompt forbids non-profile URLs, but adversarial - brand.json/tagline input could still leak a link through the model; - this is the last-mile defense. -- `extractActionContext` now validates `channelId` against - `^[CGD][A-Z0-9]+$` and `messageTs` against `^\d+\.\d+$`, matching - the pattern `settings.ts` already uses on admin channel-id writes. -- `ORG_ID_PATTERN` tightened to case-sensitive (WorkOS org IDs are - fixed-case uppercase). -- Action IDs centralized in `ANNOUNCE_ACTION_IDS` constant. - -**Known follow-ups (not in this PR).** -- Admin UI surface for both `editorial_channel` and `announcement_channel` - — neither is exposed in `admin-settings.html` yet. -- Stage 1's review channel still reads from `SLACK_EDITORIAL_REVIEW_CHANNEL` - env; migrating it to `getEditorialChannel()` is a separate cleanup. -- Manual smoke test in a live Slack workspace (button click flow) still - recommended before this lands in production. diff --git a/.changeset/workflow-b-announcement-trigger.md b/.changeset/workflow-b-announcement-trigger.md deleted file mode 100644 index 0e9bd4d862..0000000000 --- a/.changeset/workflow-b-announcement-trigger.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Workflow B Stage 1 — announcement drafter + editorial review trigger: - -- New `announcement-drafter` service drafts Slack (mrkdwn) and LinkedIn (copy-paste with hashtags) welcome posts from org, profile, and brand.json inputs. Uses the shared `complete()` LLM wrapper on the primary model tier. -- New `announcement-visual` service resolves a draft visual: brand.json `logos[0].url` for companies, approved member portrait for individuals, and an `${APP_URL}/AAo-social.png` fallback. Source is tagged for observability. -- New `announcement-trigger` scheduled job runs hourly during business hours. Picks up orgs with a `profile_published` activity and a brand.json manifest, no prior `announcement_draft_posted`/`announcement_skipped` activity, and `is_public = true`. Drafts copy, resolves a visual, and posts a Block Kit review card to a new `SLACK_EDITORIAL_REVIEW_CHANNEL` with three actions (`announcement_approve_slack`, `announcement_mark_linkedin`, `announcement_skip`). Writes an `announcement_draft_posted` `org_activities` row carrying the drafted texts + visual in metadata so Stage 2 interactivity handlers can publish without re-drafting. -- Job no-ops cleanly when `SLACK_EDITORIAL_REVIEW_CHANNEL` is unset. Per-run draft cap of 5. - -Hardening: - -- Drafter wraps all user-supplied fields in `...` delimiters, caps each field's length, strips control chars, and instructs the model to treat delimited content as data (not instructions). Output drafts are length-clamped after parse. JSON parser recovers from leading/trailing prose via a balanced-brace scan. -- Visual resolver validates every brand.json logo URL before it reaches Slack: https-only, public host only (blocks localhost, RFC1918, `.internal`, `.local`), whitelisted raster extensions (`.png/.jpg/.jpeg/.webp/.gif`) — `.svg` rejected to avoid script risk on downstream surfaces. On any rejection the resolver falls back to the AAO mark rather than posting an attacker-chosen URL. -- Review card sanitizes drafts before embedding: ``/``/`` neutralized to plain text, `<@U…>` user mentions and `<#C…>` channel mentions replaced with generic placeholders, backticks stripped inside the LinkedIn fenced block so the fence cannot be closed early. -- Editorial-channel post uses `requirePrivate: true` so a misconfigured public channel fails closed instead of leaking draft copy. -- Activity write is wrapped in try/catch after the Slack post; on failure the Slack message is deleted via `chat.delete` to preserve draft-row idempotency (no orphan review card without its activity row). - -Follow-up to PR #2246. Stage 2 (Bolt action handlers) and Stage 3 (admin-members "Mark posted to LinkedIn") ship separately. diff --git a/.changeset/workflow-b-backfill.md b/.changeset/workflow-b-backfill.md deleted file mode 100644 index 2e1eae97f3..0000000000 --- a/.changeset/workflow-b-backfill.md +++ /dev/null @@ -1,78 +0,0 @@ ---- ---- - -**Workflow B Stage 4 — retroactive announcement backfill** - -One-shot script that posts retroactive new-member announcement drafts -to the editorial review channel so orgs who became announce-ready -before Workflow A's `profile_published` event emit don't get left out -of the welcome flow. - -Spec: `specs/new-member-announcements.md` → "Backfill". - -**New:** - -- `server/src/scripts/backfill-member-announcements.ts` — CLI entry - point. `--limit N` (default 15) caps the run so a single invocation - can't flood the editorial channel. `--dry-run` reports eligible - candidates without posting or writing activity rows. -- `runBackfillAnnouncements({ reviewChannel, limit, dryRun })` in - `announcement-trigger.ts` — drives the per-candidate pipeline with - the `backfill: true` flag so review cards get a `[BACKFILL]` header - prefix. - -**Refactors:** - -- `findAnnounceCandidates({ requireProfilePublished })` — backfill drops - the `profile_published` EXISTS clause (those orgs predate the event). -- Extracted `processAnnounceCandidate` — shared by the live trigger - and the backfill, keeps the post-then-record unwind semantics. -- `buildReviewBlocks({ backfill })` — header prefix only. -- Recorded `announcement_draft_posted` metadata gains `backfill: true` - so downstream analytics can tell retroactive drafts apart. - -**Safety:** - -- Idempotency via the existing `NOT EXISTS` on `announcement_draft_posted` - / `announcement_skipped` — re-running the script will not re-post - orgs that landed on a previous run. -- Dry-run produces no Slack traffic and no DB writes. -- Unwind (chat.delete) on activity-write failure, same as the live job. - -**Tests:** 19 new tests in `tests/announcement/announcement-backfill.test.ts` -covering SQL shape (profile_published clause presence), header tag -behavior, limit enforcement (default 15 + override), `backfill:true` -metadata, candidate-load failure recovery, per-candidate failure -isolation, and CLI arg parsing. Full announcement suite 122/122 pass. - -**Ops.** Operator picks the right moment, runs -`npx tsx server/src/scripts/backfill-member-announcements.ts --limit 10` -with SLACK_EDITORIAL_REVIEW_CHANNEL + ADDIE_BOT_TOKEN + ANTHROPIC_API_KEY -set. Editorial team approves through the existing Stage 2 Slack buttons. - -**Hardening (expert-review follow-ups).** - -- Hard ceiling: `--limit` defaults to 15, soft-caps at 50 without - `--force`, absolute-capped at 200 with `--force`. Prevents a fat- - fingered `--limit 9999` from flooding the editorial channel or - billing thousands of Anthropic tokens. -- `pg_try_advisory_lock` at run start — two operators (or a duplicate - invocation) running backfill simultaneously would otherwise race the - `NOT EXISTS` idempotency filter. The second caller now refuses with - `lockedOut:true` instead of producing duplicate posts. -- Dry-run preview rows now include `membership_tier`, - `primary_brand_domain`, and `last_published_at` so the operator can - decide from the dry-run output whether the list looks right. -- Live run prints the succeeded-orgs list to stdout and posts a single - summary message (`📦 Backfill wave posted — N retroactive drafts…`) - to the editorial channel so editorial gets a nudge without watching - the CLI. -- Ops pre-flight ritual documented in the script header: dry-run - first, start with `--limit 3`, Ctrl-C is safe, only one run at a - time, hard ceilings explained. - -14 new tests on top of the original 21 (32 total) cover the hardening: -force toggle, soft-cap enforcement, absolute-max ceiling, advisory -lock lockout path, summary message on drafted>0 and skip when 0, -drafted_orgs shape, rich-fixture drafter call-through, and the two -unwind-failure branches. Full announcement suite 135/135 pass. diff --git a/.changeset/workflow-b-final-polish.md b/.changeset/workflow-b-final-polish.md deleted file mode 100644 index 9cfa57733c..0000000000 --- a/.changeset/workflow-b-final-polish.md +++ /dev/null @@ -1,48 +0,0 @@ ---- ---- - -**Workflow B final polish + ops runbook** - -Closes the remaining review-deferred polish on the announcement -backlog view and adds the incident-response runbook flagged during -the #3003 review. - -**Backlog view (`/admin/announcements`):** - -- **Signup-age column** — new "Signed up" column between Tier and - Draft posted. Renders relative ("3d ago", "2mo ago", "1y ago") so - editorial can tell "recent welcome" from "stale backfill" without a - click-through. ISO date in the title attribute for precise reads. - Backend exposes `org_created_at` from `organizations.created_at`; - null-safe for orphan drafts where the org row was deleted. -- **Empty-state copy** — "Pending review" and "LinkedIn pending" now - show actionable copy ("Nothing to review. New drafts are posted - hourly by the trigger job." / "Nothing waiting on LinkedIn right - now.") instead of the generic "No announcements in state X." -- **Arrow-key tab nav** — Left/Right cycle, Home/End jump. Pairs - with the `aria-selected` toggling already in place so the filter - tabs follow the WAI-ARIA tabs pattern. - -**Ops runbook:** - -- New `ops/channel-rotation.md` — quick reference for rotating a - Slack channel wired into an admin setting (all seven — billing, - escalation, admin, prospect, error, editorial, announcement). - Covers happy path, write/send-time failure modes, incident scenario - for archived review channel, and break-glass direct-SQL rotation - with audit-table capture for when the admin UI is down. Referenced - during the #3003 review. - -**Tests:** 2 new backlog query tests (org_created_at happy + orphan), -2 new route tests (ISO serialization + null passthrough). Full -announcement suite 172/172 pass; server typecheck clean. - -**Skipped from the final polish list** (not worth the bytes at -current scale): - -- Sortable column headers — the stuck-first default sort already - answers the question; at 30-50 rows, scanning is fine. -- Vitest pool isolation for `tests/announcement/**` — the - `mockResolvedValue({rows: []})` default works as a band-aid. -- Slack deep-link helper in the admin cannot_verify error state — - UX work outside this thread. diff --git a/.changeset/workflow-b-stale-li-reminders.md b/.changeset/workflow-b-stale-li-reminders.md deleted file mode 100644 index ca4d55c9c9..0000000000 --- a/.changeset/workflow-b-stale-li-reminders.md +++ /dev/null @@ -1,57 +0,0 @@ ---- ---- - -**Workflow B: stale-LinkedIn reminders** - -Closes the Workflow B loop. The backlog view (#3014) already flags -stuck rows with a red badge, but editorial has to open the page to -see it. This job posts a threaded reply on the *original* review -card — the same thread they'd act on — when LinkedIn has been -pending for more than 7 days. - -**Behavior.** Daily job scans for orgs where: - -- Slack was posted more than 7 days ago -- LinkedIn is not yet marked -- Draft was not skipped -- No reminder has been sent in the last 7 days -- Fewer than 3 reminders have ever been sent for this draft - -Posts as a **threaded reply** to the review card (preserves context; -the Mark-LI button is right there), and records an -`announcement_li_reminder_sent` activity for the rate-limit. Max 3 -reminders per org so a permanently-stuck draft doesn't generate -reminders forever. - -**New:** - -- `findStaleLiCandidates()` / `runAnnouncementReminderJob()` in - `announcement-trigger.ts`. -- Constants `REMINDER_STALE_DAYS` (7), `REMINDER_INTERVAL_DAYS` (7), - `MAX_REMINDERS_PER_ORG` (3). -- Registered in `job-definitions.ts` as `announcement-li-reminder` — - 24 h interval, 10–11 am business hours, skip weekends so reminders - land when editorial is actually online. - -**Tests.** 11 new tests in `announcement-reminder.test.ts`: - -- SQL params (rate-limit values pinned) -- SQL exclusion clauses (`li_posts IS NULL`, `skipped IS NULL`, cap) -- Orphan-org fallback + integer coercion on `days_since_slack` -- Happy path: threaded reply + activity write -- Reminder-number increments across runs (reminder_count + 1) -- Slack post failure: no activity row written, failed++ -- Activity-write failure after successful post: counts as reminded, - but logs; next run may re-ping (accepted edge case — unwinding a - thread reply is worse UX than an extra ping) -- Per-candidate failure doesn't stop the batch -- Candidate-load failure returns zeros, doesn't throw -- Empty candidate list short-circuits cleanly -- `buildReminderText` renders all fields in Slack mrkdwn + linkifies - the admin backlog URL - -Full announcement suite 167/167 pass; server typecheck clean. - -**Ops.** First scheduled run lands 22 minutes after server start, -then daily at 10 am local. `shouldLogResult` only logs when -`reminded > 0 || failed > 0` so quiet days stay quiet. diff --git a/.changeset/workos-lazy-init-factory.md b/.changeset/workos-lazy-init-factory.md deleted file mode 100644 index a29eb41fd2..0000000000 --- a/.changeset/workos-lazy-init-factory.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Refactor `workos-client.ts` to a lazy-init factory (`getWorkos()`), removing the module-load throw that broke test suites. Converts all dynamic-import workarounds back to static imports. diff --git a/.changeset/writer-anchor-adopt-promotion.md b/.changeset/writer-anchor-adopt-promotion.md deleted file mode 100644 index 1bfba928f4..0000000000 --- a/.changeset/writer-anchor-adopt-promotion.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Fix writer anchor-adopt promotion — when a publisher's adagents.json claims a -property whose rid was previously created by community/enrichment/contributed -flows AND anchors via a domain identifier under their own host, promote -`source` → `'authoritative'` and rebind `created_by` → -`'adagents_json:'`. Without this, the auth projection's -`WHERE created_by = 'adagents_json:' AND property_id = ANY(...)` returns -zero rows for properties already in the catalog under a different pipeline, -silently dropping the manifest's `authorized_agents[]` entries. - -Found via escalation #287 (wheelrandom.com): valid adagents.json with a -correctly anchored inline property, but registry showed 0 authorizations -because a pre-existing `contributed` catalog row blocked the auth projection. diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc303a47d..ac9ba04fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,212 @@ # Changelog +## 3.0.1 + +See [release notes](docs/reference/release-notes.mdx#version-301) for the curated narrative — 3.0.1 is a stable-surface no-op for 3.0-conformant agents. Skills bundle in `/protocol/3.0.1.tgz`, normative clarifications, additive fields on experimental surfaces (governance, TMP) per the experimental-status contract, and one docs-level deprecation (`get_signals` top-level `max_results`). + +### Patch Changes + +- 10aa2b3: Cut **3.0.1** to ship `skills/` in the protocol tarball and fix path drift in `skills/call-adcp-agent/SKILL.md`. Closes #3116, #3117. + + **Why a patch bump (not a re-cut at 3.0.0):** the protocol tarball is the SDK distribution surface. `3.0.0.tgz` was published 2026-04-22, before #3097 hoisted `skills/`. Re-cutting at the same version would mean a new SHA-256 at the same stable URL — incompatible with content-addressed pipelines, supply-chain attestations, and the cosign signature bound to the original content. Pre-merge expert review (protocol + security) recommended bumping to preserve immutability and produce a fresh signed release through the normal `release.yml` path. + + **What's in 3.0.1:** + + - `skills/` bundled in `/protocol/3.0.1.tgz` (the seven protocol-managed skills: `call-adcp-agent` + the per-protocol `adcp-{brand,creative,governance,media-buy,si,signals}`) + - `manifest.contents.skills` enumerated for SDK sync scripts to detect + - `skills/call-adcp-agent/SKILL.md` — replace four hardcoded `dist/schemas//bundled/...` references with discovery-first phrasing that doesn't assume an SDK layout + - `docs/protocol/calling-an-agent.mdx` — sister content fix + + **What does NOT change:** every schema, every task definition, every wire-format detail in 3.0.0 carries over identically to 3.0.1. The bump is for the bundle/skill axis, not the protocol-spec axis. + + **SDK action:** bump `ADCP_VERSION` from `3.0.0` to `3.0.1` to receive the canonical skills via your existing sync flow. JS-side wiring is in [adcontextprotocol/adcp-client#965](https://github.com/adcontextprotocol/adcp-client/pull/965); Python and Go follow-ups tracked in [adcp-client-python#274](https://github.com/adcontextprotocol/adcp-client-python/issues/274) and [adcp-go#91](https://github.com/adcontextprotocol/adcp-go/issues/91). + +- a7dbe65: docs(brand): specify normative request-validation clauses for `acquire_rights` (closes #2680, #2681) + + Two campaign-field validations on `acquire_rights` were sensible-but-unspecified in 3.0, leaving implementers to disagree on identical requests: + + 1. **Expired campaign window.** Brand agents MUST reject with `INVALID_REQUEST` and `field: "campaign.end_date"` when `campaign.end_date` is in the past at the time of the request. Issuing a zero-duration grant is almost always a buyer-side bug; deterministic rejection is more useful than silent expiry. Unlike `create_media_buy` (where `any_of` supports time-shifting a flight forward), rights grants attach to the requested period and cannot be retroactively shifted, so reject-only is the correct contract. + + 2. **CPM-priced rights under a governed plan.** When the request carries an intent-phase `governance_context` token (the buyer's plan is governed) and the selected pricing option has `model: "cpm"`, brand agents MUST reject with `INVALID_REQUEST` and `field: "campaign.estimated_impressions"` when that field is omitted or `0`. When provided, projected commitment is `(pricing_option.price / 1000) × campaign.estimated_impressions` evaluated in `pricing_option.currency`. If `pricing_option.currency` differs from the plan's budget currency, the agent MUST reject with `field: "pricing_option_id"` — currency conversion is not specified. If the projected commitment exceeds remaining plan budget, the agent MUST reject with `field: "campaign.estimated_impressions"`. Non-CPM pricing options commit the flat amount regardless of volume; agents MUST NOT require `estimated_impressions` for governance projection on those. + + Added a new "Request validation" section to `docs/brand-protocol/tasks/acquire_rights.mdx` and tightened the field descriptions on `static/schemas/source/brand/acquire-rights-request.json` for `campaign.end_date` and `campaign.estimated_impressions` so the validation contract is discoverable from both the task reference and the schema. + + Patch-eligible: docs-only clarification of behavior the spec already implied. No schema shape changes (only description text); no new error codes (`INVALID_REQUEST` is already standard). The `governance_context` anchor and the `(price / 1000) × impressions` projection formula reference fields that exist on the published 3.0 schemas — this PR does not introduce new wire surface, only normative interpretation. + +- 926b079: feat(compliance): add seed_creative_format scenario and list_creative_formats pagination + + Adds `seed_creative_format` to `comply_test_controller` so the compliance harness can pre-populate a deterministic, size-controlled set of creative formats for pagination-integrity storyboards. `comply_test_controller` is a conformance-harness surface, not a core-protocol task — additive enum extensions on it bump at patch level under AdCP semver. + + **Schema changes (comply-test-controller-request.json, comply-test-controller-response.json):** `seed_creative_format` added to the `scenario` enum in both files. The request schema gains a `params.format_id` string field (required when `scenario = seed_creative_format`) and the response schema's `list_scenarios` enum is extended to match. + + **Training-agent implementation:** `seed_creative_format` is handled in `handleComplyTestController` before the SDK dispatcher. Seeded formats are stored in a new `session.complyExtensions.seededCreativeFormats` map and replace the static catalog when non-empty for `list_creative_formats` responses. + + **Pagination:** `handleListCreativeFormats` now applies cursor-based pagination (matching the `list_creatives` pattern) and is session-aware to read seeded formats. Non-compliance callers continue to see the full static catalog with pagination applied. + + **Storyboard:** `pagination-integrity-creative-formats.yaml` exercises the cursor↔has_more invariant on `list_creative_formats` by seeding two formats and walking pages at `max_results=1`. + + Non-breaking: adds a new enum value and optional param. Sellers that don't implement `seed_creative_format` will return `UNKNOWN_SCENARIO`; the storyboard's `controller_seeding: true` signals that support is required for this storyboard to pass. Existing callers of `list_creative_formats` are unaffected — pagination fields are additive to the response. + + Closes #3108. + +- ae7eae2: Add optional `mode` field to `get_plan_audit_logs` audit entries, recording the governance mode (enforce/advisory/audit) active at check time. Surfaces the enforcement posture that produced each decision, closing a gap where audit and enforce modes produced identical-looking trails. +- 46439c4: **Apply the AdCP URL canonicalization rule to brand.json agent URLs.** + + Follow-up to #3067 — the canonicalization reference page now exists, + and `seller-agent-ref`, `adagents.json` `authorized_agents[].url`, + `format-id`, and `provider-registration` all link to it. `brand.json` + declares additional agent URLs that fall in the same identifier- + comparison class but weren't covered: + + - `brand_agent_entry.url` — the brand-declared agent endpoint (MCP or + A2A) used by callers resolving "is this the agent that signed this + artifact?" or matching against a discovery cache. + - `brand_agent.url` — the brand agent MCP endpoint reference. + - `rights_agent.url` — the rights agent MCP endpoint reference. + + All three now reference the AdCP URL canonicalization rules at + `docs/reference/url-canonicalization` so two URLs differing only in + case, default port, or percent-encoded unreserved characters compare + equal during agent resolution. + + `logo.url`, `data_subject_contestation.url`, asset-library `url`, and + the brand's primary `url` are _not_ identifier-comparison keys (they + point at human-facing pages or asset CDN endpoints), so they were + left unchanged. + + `jwks_uri` (line 627) is a fetch target for JWKS download, not an + identifier-comparison key — receivers HTTP-GET the URL as declared + without comparing it to anything. Not in scope for this rule. + + No schema shape changes. Descriptions only. + +- 1cd99c2: Make the `task_status` / `response_status` prohibition from #3021 machine-enforceable at the schema level. Adds a `not: { anyOf: [{ required: [task_status] }, { required: [response_status] }] }` constraint on `protocol-envelope.json` (matching the existing idiom in `catchment.json`) so any JSON Schema validator rejects envelopes that dual-emit legacy status fields — no runner-specific primitive required. The prose MUST NOT in the envelope `status` description remains for human readers; the constraint is what validators act on. Closes #3041 at the spec layer. Runtime conformance (storyboard `field_absent` primitive + `@adcp/client` implementation) is tracked separately. +- ea8e282: Add `title` to all `oneOf` branches in `format.json`'s `assets[]` array so codegen tools (json-schema-to-typescript, datamodel-code-generator, oapi-codegen) produce named, discriminated per-asset-type interfaces instead of collapsing them to an untyped union. Adds titles `IndividualImageAsset` … `IndividualCatalogAsset` and `RepeatableGroupAsset` at the top level, plus `GroupImageAsset` … `GroupWebhookAsset` for the nested branches inside `repeatable_group.assets[]`. Purely annotation-level; no validation or wire-format change. +- cecca44: Deprecates top-level `max_results` on `get_signals` and pins `pagination.max_results` precedence. + + `get-signals-request.json` carried two independent pagination fields — a legacy top-level `max_results` (no cap, no default, predates the pagination envelope) and the standard `pagination` envelope (`pagination.max_results`, max: 100, default: 50). The schema was silent on which wins when both are present. + + This change adds a MUST-level precedence rule: when both fields are present, agents MUST honor `pagination.max_results`. It also deprecates the top-level field with guidance for sellers receiving it without a pagination envelope. The top-level `max_results` will be removed in AdCP 4.0. + + All other paginated read endpoints (`get_products`, `list_creatives`, `list_creative_formats`, `get-collection-list`, `get-property-list`, `get-media-buy-artifacts`, `tasks-list`) carry only `pagination` — this brings `get_signals` into alignment. + + Non-breaking: adds description-level deprecation and normative prose. No type, structure, or required-field changes. Existing callers unaffected; sellers adding the conflict check gain new conformance grounding. + +- 00c1574: Add `mode` to `check_governance` response schema and fix `binding`→`check_type` drift in training agent audit entries. + + `check-governance-response.json` now declares the optional `mode` field (enforce/advisory/audit) that the training agent was already emitting, letting counterparties and regulators distinguish `approved`-with-finding decisions made under `enforce` from those made under `audit`. The training agent audit log handler no longer emits the non-canonical `binding` field (which caused schema-validation failures on the strict `entries[]` schema); it now emits `check_type: "intent"|"execution"` per the existing schema contract. The schema carries `x-status: experimental`. Audit-entry `mode` is added separately by #3160. + +- ff95642: Clarify `policies_evaluated` description in `check-governance-response.json` and `get-plan-audit-logs-response.json`. The previous wording ("Registry policy IDs...") was incomplete and misleading: governance agents also record inline `policy_id`s from `custom_policies` in this field, and a consumer reading the description literally could write a parser that filters them out. The new wording names both sources. Both schemas carry `x-status: experimental`. Description-only clarification; no type, enum, or wire change. +- 20a8310: Mark `governance-mode.json` enum as `x-status: experimental` and clarify the per-check semantics of the audit-entry `mode` field. + + The enum is referenced exclusively from experimental schemas (`check-governance-response.json`, `get-plan-audit-logs-response.json` `entries[]`); annotating it explicitly prevents the enum from being treated as stable while its consumers are still experimental. The `entries[].mode` description is tightened to clarify that the field reflects the mode active for that specific check, distinct from a future `governed_actions[].mode` (which would describe the action's current mode and may differ if the plan has been re-synced since). + +- 3027c39: feat(schema): hoist 4 duplicate inline enum literal sets into shared `enums/` definitions (closes #3144) + + Several inline string-literal unions in the AdCP source schemas had byte-identical value sets across multiple parent schemas but no shared `$ref`, causing the TypeScript SDK to emit per-parent duplicate exports (`Account_PaymentTermsValues`, `GetAccountFinancialsSuccess_PaymentTermsValues`, etc.) when a single canonical `PaymentTermsValues` is what consumers expect. + + **New shared enum files added** (4 new `$id`-bearing schemas in `static/schemas/source/enums/`): + + - `payment-terms.json` — `["net_15","net_30","net_45","net_60","net_90","prepay"]` + - `audio-channel-layout.json` — `["mono","stereo","5.1","7.1"]` + - `media-buy-valid-action.json` — `["pause","resume","cancel","update_budget","update_dates","update_packages","add_packages","sync_creatives"]` + - `rights-billing-period.json` — `["daily","weekly","monthly","quarterly","annual","one_time"]` + + **Schemas updated to use `$ref`** (10 files; wire format unchanged in all cases): + + - `core/account.json`, `account/sync-accounts-request.json`, `account/sync-accounts-response.json`, `account/get-account-financials-response.json` → `payment_terms` now refs `enums/payment-terms.json` + - `core/assets/audio-asset.json`, `core/assets/video-asset.json` → `channels`/`audio_channels` now ref `enums/audio-channel-layout.json` + - `media-buy/create-media-buy-response.json`, `media-buy/update-media-buy-response.json` → `valid_actions` items now ref `enums/media-buy-valid-action.json` + - `brand/rights-terms.json`, `brand/rights-pricing-option.json` → `period` now refs `enums/rights-billing-period.json` + + **Not changed:** `core/insertion-order.json` `payment_terms` (`["net_30","net_60","net_90","prepaid","due_on_receipt"]` — different set, kept inline). + + Non-breaking: replacing inline `{"type":"string","enum":[...]}` with a `$ref` to an equivalent standalone schema produces an identical JSON Schema subgraph; all existing validators behave identically. Source-schema refactor only; bundled wire format is unchanged — patch-eligible. + + After a `npm run sync-schemas` in `adcp-client`, the SDK will emit single canonical exports (`PaymentTermsValues`, `AudioChannelLayoutValues`, etc.) and should ship deprecated re-export aliases for any per-parent names that were in a published release. + +- feed616: Hoist 13 duplicate inline enum sets into shared `enums/` definitions (follow-up to #3148). + + Adds `match-type`, `collection-kind`, `frame-rate-type`, `scan-type`, `gop-type`, `moov-atom-position`, `binary-verdict`, `account-scope`, `governance-decision`, `billing-party`, `feature-check-status`, `snapshot-unavailable-reason`, and `travel-time-unit` as standalone `$id`-bearing enum files. Updates 21 source schemas to `$ref` these files instead of repeating the inline definitions. Source-schema refactor only; bundled wire format is unchanged in all cases. + +- 4614f4d: Clarify that v3 agents MUST NOT emit legacy status fields (`task_status`, `response_status`, or any alias) alongside the v3 `status` field. Adds a migration checklist row, a conformance warning in the task-lifecycle reference, and extends the protocol envelope schema's `status` description with the prohibition. Closes #2987. +- 90ad0dd: Add `x-status: experimental` to all 9 TMP schemas and `core/seller-agent-ref.json` (exclusively referenced by TMP), completing the experimental-status contract already declared for `trusted_match.core` in `get_adcp_capabilities` and `experimental-status.mdx`. Mirrors the existing pattern on all 11 sponsored-intelligence schemas. Enables validators, doc generators, and tooling to identify TMP as an experimental surface. No wire-format or field changes. +- f1e8340: **TMP: explicit seller-agent attribution on AvailablePackage.** + + Add `seller_agent: { agent_url, id? }` to the Trusted Match Protocol + AvailablePackage schema, making seller identity explicit on every + package cached by a TMP provider. The canonical identifier is the + seller's agent URL as declared in the property publisher's + `adagents.json` `authorized_agents[].url`; the reserved `id` slot is + forward-compatible with a future registry-assigned opaque identifier. + + - **`/schemas/core/seller-agent-ref.json`** — new shared schema + mirroring the `{agent_url, id?}` shape used by `format-id` and + `ProviderEntry`. + - **`/schemas/tmp/available-package.json`** — `seller_agent` added as + a required field. Lands as a patch under the experimental-surface + contract (`experimental_features: trusted_match.core`, which allows + breaking changes between 3.x releases with advance notice); sellers + syncing `AvailablePackage` payloads need to populate it going + forward. + - **`/schemas/tmp/offer.json`** — optional `seller_agent` echo so + publisher-side log pipelines can attribute offers to sellers + without round-tripping to the media-buy store. Non-authoritative: + the cached package binding remains source of truth; routers MAY + stamp the field on merge when providers omit it. + - **`/schemas/tmp/error.json`** — adds `seller_not_authorized` error + code for sync-time rejection when `seller_agent.agent_url` is not + present in the property publisher's adagents.json + `authorized_agents[].url` list. + - **`docs/trusted-match/specification.mdx`** — new "Package Sync" + section defines the sync contract, the SHOULD-level adagents.json + validation flow, explicit per-actor responsibilities (seller + agent, publisher, router, provider), and the "what this is not" + boundary (not a request-time filter, not a sellers.json bridge, + not a cryptographic attestation). Offer and Error tables updated + accordingly; definitions table gains a **Seller agent** entry. + + Seller identity lives on the cached `AvailablePackage`, not on + `context_match_request` or `identity_match_request`. Providers — + which have no access to a media-buy store — need provenance on the + wire they actually receive; putting it on the request would either + duplicate the sync-time binding or open a path for request-time + seller filtering that re-introduces the identity- and + allocation-leakage failure modes that package-set decorrelation + exists to prevent. Publishers and routers can derive seller identity + from `media_buy_id` against their own stores; providers cannot. + + TMP remains experimental under AdCP 3.x — schema additions here + follow the experimental-surface contract and do not bump the stable + AdCP major. The `SellerAgentRef.id` slot and optional `ext` namespace + leave room to layer signed seller claims or an AAO-assigned opaque + identifier without a rename later. + +- aa71ebc: **URL canonicalization: one authoritative reference for every URL-as-identifier comparison in AdCP.** + + The canonicalization algorithm previously lived only under the request-signing profile in `docs/building/implementation/security.mdx`, but AdCP compares URLs as identifiers in many other places — TMP seller authorization (`seller_agent.agent_url` vs `authorized_agents[].url`), TMP provider resolution (`ProviderEntry.agent_url`), `format-id.agent_url` equivalence, and signal/feature agent lookups in `adagents.json`. Schemas today said "exactly as declared," which reads as byte-equality; two URLs that differ only in case, default port, or percent-encoded unreserved characters would silently miss the match. + + This change moves the algorithm to a first-class reference page and links every consuming surface to it, so the same canonicalization binds everywhere. + + - **New `docs/reference/url-canonicalization.mdx`** — the authoritative home of the 8-step algorithm (RFC 3986 §6.2.2 + §6.2.3, UTS-46 Nontransitional IDN pin, IPv6 zone-identifier rejection, enumerated malformed-authority cases), a "where it applies" table covering signing / TMP seller authorization / TMP provider resolution / `adagents.json` lookups / `format-id` / `authoritative_location` indirection, a "signing profile extensions" note for the transport-only bits, and a common-pitfalls list. + - **`docs/building/implementation/security.mdx`** — `@target-uri` section now cites the reference page instead of restating the eight steps. Keeps only the signing-specific extensions (HTTP/2 `:authority` derivation, dual-header rejection, `request_target_uri_malformed` error, cross-vhost replay gate). Removes the drift risk between two copies. + - **`static/schemas/source/core/seller-agent-ref.json`** — `agent_url` description replaces "exactly as declared" with canonicalization-based comparison. Also drops the "in production" weasel on HTTPS — the scheme requirement is now unconditional. + - **`static/schemas/source/adagents.json`** — all six `url` descriptions updated: the four `authorized_agents[].url` variants, plus the two signals-authorization variants (`signal_ids`, `signal_tags`) and the property-features variant. + - **`static/schemas/source/core/format-id.json`** — `agent_url` description updated to require canonicalization. + - **`static/schemas/source/tmp/provider-registration.json`** — `endpoint` description extends the existing SSRF/DNS-rebinding language with a canonicalization rule for provider-registry de-duplication. + - **`docs/trusted-match/specification.mdx`** — TMP Sync-Time Validation step 2 links canonicalization rules explicitly and adds an explicit `https://`-only rejection (non-HTTPS seller URLs get `seller_not_authorized`, closing the scheme-mismatch bypass). ProviderEntry table row links the canonicalization rules for provider comparison. + - **`docs.json`** — reference page added to both primary and legacy sidebars adjacent to `versioning` (other interop-rules references). + + No schema shape changes. Descriptions only. Schema link style follows the repo convention (`See docs/` bare, no backticks or leading slash). + +- 9ff83de: feat(compliance): v3 envelope integrity universal storyboard + + Adds `static/compliance/source/universal/v3-envelope-integrity.yaml` — a universal storyboard (applies to all agent interaction models) that asserts the v3 `status` field is present on the response envelope and that the legacy v2 `task_status` / `response_status` field names are absent. + + Schema-level enforcement of the prohibition is provided separately by `envelope-forbid-legacy-status-fields.md` (top-level `not: { anyOf: [{ required: [task_status] }, { required: [response_status] }] }` on `protocol-envelope.json`). This changeset is the runtime/storyboard counterpart. + + The explicit envelope-root field-absence assertions are wired as TODO `field_absent` checks pending runner support in `@adcp/client`; the immediate enforcement path remains the schema-level constraint, which any schema-aware validator detects without runner-specific primitives. Closes #3041 at the storyboard layer. + ## 3.0.0 See [release notes](docs/reference/release-notes.mdx) for migration guidance, or [prerelease upgrade notes](docs/reference/migration/prerelease-upgrades.mdx) for rc.3 adopters. diff --git a/dist/compliance/3.0.1/domains/brand/index.yaml b/dist/compliance/3.0.1/domains/brand/index.yaml new file mode 100644 index 0000000000..9598013fe3 --- /dev/null +++ b/dist/compliance/3.0.1/domains/brand/index.yaml @@ -0,0 +1,163 @@ +id: brand_baseline +version: "1.0.0" +title: "Brand baseline" +protocol: brand +category: brand_baseline +summary: "Baseline protocol storyboard — every brand agent must declare the brand protocol in capabilities and return a schema-valid brand identity." +track: brand +required_tools: + - get_brand_identity + +narrative: | + Brand protocol agents are the identity layer of AdCP. Their job is to hold + brand identity data (names, logos, colors, fonts, tone) and expose it to + other agents — buyer agents, creative agents, DSPs — that need to render + on-brand creative or verify who a campaign is for. + + The baseline tests the minimum contract that every brand agent honors, + regardless of what additional capabilities (rights licensing, creative + approval) it layers on top: + + 1. Declare `brand` in `supported_protocols` on `get_adcp_capabilities`. + 2. Respond to `get_brand_identity` with a schema-valid identity manifest. + 3. Reject unknown `brand_id` values with a structured error. + + Rights licensing (`get_rights`, `acquire_rights`, `update_rights`, + `creative_approval`) ships experimentally in 3.0 and is covered by the + `brand-rights` specialism storyboard, not this baseline. + +agent: + interaction_model: brand_agent + capabilities: [] + examples: + - "Any brand agent (simple identity host or full rights platform)" + - "Brand-owned agents (Acme Outdoor)" + - "Third-party brand identity platforms" + - "Agency-hosted brand agents" + +caller: + role: buyer_agent + example: "Any buyer, creative agent, or DSP needing brand identity" + +prerequisites: + description: | + The test kit provides a sample brand (Nova Motors) that any brand agent + can serve identity for. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls `get_adcp_capabilities` to confirm the agent declares + the brand protocol before issuing any brand-identity call. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares `brand` in `supported_protocols`. + Without this claim the buyer MUST NOT send `get_brand_identity`. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring `brand` in `supported_protocols`. + + sample_request: + context: + correlation_id: "brand_baseline--get_capabilities" + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" + - check: field_present + path: "supported_protocols" + description: "Response declares supported_protocols" + + - id: brand_identity_retrieval + title: "Brand identity retrieval" + narrative: | + The buyer calls `get_brand_identity` to retrieve the brand's identity + manifest. The minimum contract is a schema-valid response that echoes + the requested `brand_id` and carries at least one name. + + steps: + - id: get_brand_identity + title: "Retrieve brand identity" + narrative: | + The buyer calls `get_brand_identity` with a known `brand_id`. The + response MUST match the brand-identity schema and echo the + requested `brand_id`. Rich fields (logos, colors, fonts, tone, + visual_guidelines) are optional at the baseline level — the + minimum bar is that identity resolution works and is schema-valid. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand-protocol/tasks/get_brand_identity" + stateful: false + expected: | + Return a schema-valid brand identity that echoes the requested + brand_id and includes at least one name. + + sample_request: + brand_id: "nova_motors" + context: + correlation_id: "brand_baseline--get_brand_identity" + context_outputs: + - path: "brand_id" + key: "brand_id" + + validations: + - check: response_schema + description: "Response matches get-brand-identity-response.json schema" + - check: field_present + path: "brand_id" + description: "Response includes brand_id" + - check: field_value + path: "brand_id" + value: "nova_motors" + description: "Returned brand_id echoes the requested brand" + - check: field_present + path: "names" + description: "Response includes brand names" + + - id: unknown_brand_rejection + title: "Unknown brand rejection" + narrative: | + Agents MUST reject unknown `brand_id` values with a structured + AdCP error rather than returning an empty or fabricated manifest. + + steps: + - id: get_brand_identity_unknown + title: "Reject unknown brand ID" + narrative: | + The buyer calls `get_brand_identity` with a `brand_id` the agent + does not serve. The response MUST be a structured error with a + recovery classification — not a success response with empty + fields. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand-protocol/tasks/get_brand_identity" + stateful: false + expected: | + Return an AdCP error response indicating the brand is not known + to this agent. + + sample_request: + brand_id: "brand_that_does_not_exist_12345" + context: + correlation_id: "brand_baseline--get_brand_identity_unknown" + + expect_error: true + negative_path: payload_well_formed + validations: + - check: error_code + allowed_values: + - "brand_not_found" + - "BRAND_NOT_FOUND" + - "NOT_FOUND" + description: "Error code indicates brand-not-found" diff --git a/dist/compliance/3.0.1/domains/creative/index.yaml b/dist/compliance/3.0.1/domains/creative/index.yaml new file mode 100644 index 0000000000..cc6efd4f20 --- /dev/null +++ b/dist/compliance/3.0.1/domains/creative/index.yaml @@ -0,0 +1,412 @@ +id: creative_lifecycle +version: "1.0.0" +title: "Creative lifecycle" +category: creative_lifecycle +summary: "Full creative lifecycle on a stateful platform: sync multiple creatives, list with filtering, build and preview across formats, observe status transitions." +track: creative +required_tools: + - list_creative_formats + +narrative: | + You run a creative platform with a persistent library — an ad server, creative management + platform, or publisher that accepts and stores creative assets. A buyer agent pushes + multiple creatives in different formats, queries the library, builds serving tags, previews + renderings, and monitors creative status as assets move through your review pipeline. + + This storyboard covers the complete creative lifecycle from the buyer's perspective: + uploading assets, browsing the library, building deliverables across formats, and + observing status transitions as creatives move from pending_review through to accepted. + + The individual creative storyboards (template, ad server, sales agent) cover specific + interaction models. This storyboard tests the full lifecycle across multiple creatives + and formats on a single platform. + +agent: + interaction_model: stateful_preloaded + capabilities: + - has_creative_library + - supports_transformation + examples: + - "Innovid" + - "Flashtalking" + - "CM360" + - "Creative management platforms" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs creative assets in multiple formats (display, video, native) and + a brand identity. The test kit provides sample assets at standard ad dimensions. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports creative operations before browsing or building creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring creative in supported_protocols, confirming the agent handles creative operations. + sample_request: + context: + correlation_id: "creative_lifecycle--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: "creative_lifecycle--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: discover_formats + title: "Discover accepted formats" + narrative: | + Before pushing any creatives, the buyer discovers what formats your platform accepts. + This determines which assets to prepare and what dimensions and specs to target. + + steps: + - id: list_formats + title: "List creative formats" + narrative: | + The buyer calls list_creative_formats to discover what your platform accepts. + The response defines format specs: dimensions, asset requirements, mime types, + and any platform-specific constraints. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return all creative formats your platform accepts: + - format_id with your agent_url and unique id + - Asset requirements (dimensions, file sizes, mime types) + - Render dimensions + - At least two formats (e.g., display and video) + + sample_request: + context: + correlation_id: "creative_lifecycle--list_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--list_formats" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - id: sync_multiple + title: "Sync multiple creatives" + narrative: | + The buyer pushes three creatives in different formats to your platform: a display + banner, a video spot, and a native card. Your platform validates each creative + against its format specs and returns per-creative status. + + steps: + - id: sync_creatives + title: "Push three creatives in different formats" + narrative: | + The buyer syncs three creatives simultaneously: a 300x250 display banner, a 30s + video spot, and a native content card. Your platform validates each against its + format specs and returns per-creative action and status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Accept and validate all three creatives: + - Per-creative action: created + - Per-creative status: accepted or pending_review + - Validation results for each creative + - Platform-assigned IDs if applicable + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "display_trail_pro_300x250" + name: "Trail Pro 3000 - Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + width: 300 + height: 250 + mime_type: "image/png" + - creative_id: "video_30s_trail_pro" + name: "Trail Pro 3000 - 30s Video" + format_id: + agent_url: "https://your-platform.example.com" + id: "video_30s" + assets: + video: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + width: 1920 + height: 1080 + duration_ms: 30000 + mime_type: "video/mp4" + - creative_id: "native_trail_pro" + name: "Trail Pro 3000 - Native Card" + format_id: + agent_url: "https://your-platform.example.com" + id: "native_content" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-native.png" + width: 1200 + height: 628 + mime_type: "image/png" + headline: + asset_type: "text" + content: "Trail Pro 3000 — Built for the Summit" + + idempotency_key: "$generate:uuid_v4#creative_lifecycle_sync_multiple_sync_creatives" + context: + correlation_id: "creative_lifecycle--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains per-creative results" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: list_and_filter + title: "List creatives with filtering" + narrative: | + The buyer queries the creative library to see their synced creatives. First a broad + list, then filtered by format. This verifies the library correctly stores and indexes + the pushed creatives. + + steps: + - id: list_all + title: "List all creatives in library" + narrative: | + The buyer calls list_creatives with no filters to see all creatives in the + library for their account. The response includes the three creatives synced + in the previous phase. + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Return creatives in the library: + - creatives array containing the synced items + - Each creative includes: creative_id, name, format_id, status + - At least three creatives from the sync phase + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "creative_lifecycle--list_all" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains creatives array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--list_all" + description: "Context correlation_id returned unchanged" + - id: list_filtered + title: "List creatives filtered by format" + narrative: | + The buyer lists creatives filtered to a specific format (display only). The + response should only include creatives matching that format. + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Return only creatives matching the format filter: + - creatives array filtered to display format + - Should include display_trail_pro_300x250 but not video or native + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + filters: + format_ids: + - agent_url: "https://your-platform.example.com" + id: "display_300x250" + + context: + correlation_id: "creative_lifecycle--list_filtered" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains filtered creatives" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--list_filtered" + description: "Context correlation_id returned unchanged" + - id: build_and_preview + title: "Build and preview across formats" + narrative: | + The buyer builds serving tags and previews renderings for the synced creatives. + This tests multi-format output: a display tag, a VAST tag for video, and a + native rendering preview. + + steps: + - id: preview_display + title: "Preview the display creative" + narrative: | + The buyer calls preview_creative for the display banner to see how it renders + in the platform's environment before going live. + task: preview_creative + schema_ref: "creative/preview-creative-request.json" + response_schema_ref: "creative/preview-creative-response.json" + doc_ref: "/creative/task-reference/preview_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a preview of the display creative: + - preview_url: rendered preview the buyer can inspect + - render_dimensions: matches the 300x250 format + - status: preview available + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + request_type: "single" + creative_manifest: + creative_id: "display_trail_pro_300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/banner_300x250.jpg" + width: 300 + height: 250 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/trail-pro" + + context: + correlation_id: "creative_lifecycle--preview_display" + validations: + - check: response_schema + description: "Response matches preview-creative-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--preview_display" + description: "Context correlation_id returned unchanged" + - id: build_video_tag + title: "Build a VAST tag for the video creative" + narrative: | + The buyer builds a serving tag for the video creative. The platform produces + a VAST-compatible tag that the buyer can traffic to ad servers. + task: build_creative + schema_ref: "creative/build-creative-request.json" + response_schema_ref: "creative/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a built serving tag for the video creative: + - tag: VAST-compatible serving tag or URL + - format: matches the video format + - creative_id: matches the requested creative + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creative_id: "video_30s_trail_pro" + target_format_id: + agent_url: "https://your-platform.example.com" + id: "vast_30s" + idempotency_key: "$generate:uuid_v4#creative_lifecycle_build_video_tag" + + context: + correlation_id: "creative_lifecycle--build_video_tag" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--build_video_tag" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/governance/index.yaml b/dist/compliance/3.0.1/domains/governance/index.yaml new file mode 100644 index 0000000000..8839db4847 --- /dev/null +++ b/dist/compliance/3.0.1/domains/governance/index.yaml @@ -0,0 +1,683 @@ +id: media_buy_governance_escalation +version: "1.0.0" +title: "Governance denial and human escalation" +category: media_buy_governance_escalation +summary: "Buyer's governance agent denies a media buy that exceeds spending authority, escalates to a human who approves with conditions." +track: campaign_governance +required_tools: + - sync_plans + - check_governance + +narrative: | + The buyer's governance agent denies a media buy because it exceeds the agent's spending + authority. The governance check escalates to a human reviewer who approves with conditions. + + This storyboard shows the full governance loop: plan registration with spending authority + limits, product discovery, pre-buy governance check that gets denied, human escalation + that results in conditional approval, media buy creation with the approved governance + context, outcome reporting, and a complete audit trail. + + Governance exists to ensure that automated agents operate within defined boundaries. When + those boundaries are exceeded, the system escalates to humans rather than blocking + entirely. The audit trail provides accountability for every decision in the chain. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform integrated with buyer governance" + - "SSP that respects governance checks" + - "Retail media network with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, and a governance agent + URL. The test kit provides a sample brand (Acme Outdoor) with campaign parameters + and a governance configuration with spending authority limits. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "sports_ctv_q2" + delivery_type: "guaranteed" + channels: ["ctv"] + format_ids: + - id: "video_30s" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "sports_ctv_q2" + pricing_option_id: "cpm_guaranteed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 45.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + plans: + - plan_id: "gov_acme_q2_2027" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 outdoor lifestyle campaign — display and video, Adults 25-54, US" + budget: + total: 50000 + currency: "USD" + reallocation_threshold: 20000 + flight: + start: "2027-04-01T00:00:00Z" + end: "2027-06-30T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "weekly_reporting_over_10k" + enforcement: "must" + policy: "Weekly reporting required for buys over $10K." + - policy_id: "seller_concentration_cap" + enforcement: "must" + policy: "No single-seller concentration above 60% of total budget." + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "media_buy_governance_escalation--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: "media_buy_governance_escalation--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account and governance setup" + narrative: | + The buyer establishes an account and registers their governance agent with the + seller. The governance agent will be called before media buys are confirmed to + validate spending authority, brand safety, and compliance. + + steps: + - id: sync_accounts + title: "Establish account relationship" + narrative: | + The buyer registers their brand and operator with your platform. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with: + - account_id: your platform's identifier + - action: created or updated + - status: active or pending_approval + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_account_setup_sync_accounts" + context: + correlation_id: "media_buy_governance_escalation--sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--sync_accounts" + description: "Context correlation_id returned unchanged" + - id: sync_governance + title: "Register governance agent" + narrative: | + The buyer tells your platform: "Before you confirm any media buy for this + account, call this governance agent to validate it." Your platform stores + the governance agent URL and will call it during create_media_buy. + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agents. Your platform should: + - Store the governance agent URLs for the specified accounts + - Return confirmation that agents were registered + - Use these agents during create_media_buy validation + + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "https://governance.pinnacle-agency.example" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_account_setup_sync_governance" + context: + correlation_id: "media_buy_governance_escalation--sync_governance" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--sync_governance" + description: "Context correlation_id returned unchanged" + - id: register_plan + title: "Register governance plan with spending limits" + narrative: | + The buyer registers a governance plan that defines the agent's spending authority. + The plan sets a per-transaction threshold below the intended media buy amount. + This ensures the governance check will trigger an escalation when the buy exceeds + the agent's authority. + + steps: + - id: sync_plans + title: "Register a governance plan with agent-limited authority" + narrative: | + The buyer's governance agent registers a plan with spending authority limits. + The plan sets a per-transaction threshold of $20K via reallocation_threshold + on the budget. Any media buy above this amount requires human approval. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: campaign_governance + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - budget.reallocation_threshold: spending limit before re-evaluation + - human_review_required: set when the plan mandates escalation + + sample_request: + idempotency_key: "media-buy-governance-escalation-sync-plans-v1" + plans: + - plan_id: "gov_acme_q2_2027" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 outdoor lifestyle campaign — display and video, Adults 25-54, US" + budget: + total: 50000 + currency: "USD" + reallocation_threshold: 20000 + flight: + start: "2027-04-01T00:00:00Z" + end: "2027-06-30T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "weekly_reporting_over_10k" + enforcement: "must" + policy: "Weekly reporting required for buys over $10K." + - policy_id: "seller_concentration_cap" + enforcement: "must" + policy: "No single-seller concentration above 60% of total budget." + + context: + correlation_id: "media_buy_governance_escalation--sync_plans" + context_outputs: + - name: plan_id + path: 'plans[0].plan_id' + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--sync_plans" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "plans[0].plan_id" + description: "Governance agent assigns plan_id — must be echoed in check_governance" + - id: discover_products + title: "Product discovery" + narrative: | + The buyer sends a brief to discover products. The products returned will exceed + the governance agent's spending authority when assembled into a media buy. + + steps: + - id: get_products_brief + title: "Send a brief" + narrative: | + The buyer describes what they want. Your platform returns products with + pricing that, when combined, will exceed the $20K per-transaction threshold + set in the governance plan. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief with pricing that totals above $20K: + - product_id, name, description + - delivery_type: guaranteed or non_guaranteed + - pricing_models with CPM and budget recommendations + - forecast: estimated delivery + + sample_request: + buying_mode: "brief" + brief: "Premium CTV and video on sports publishers. Q2 flight, $50K budget. Adults 25-54, US." + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "media_buy_governance_escalation--get_products_brief" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--get_products_brief" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: governance_check_denied + title: "Governance check — denied" + narrative: | + Before creating the media buy, the buyer's governance agent runs a pre-buy check. + The proposed buy totals $50K, which exceeds the agent's $20K per-transaction + authority. The governance agent denies the buy with a must-severity finding and + returns escalation instructions for human review. + + steps: + - id: check_governance_denied + title: "Pre-buy governance check (denied)" + narrative: | + The buyer calls check_governance with the proposed media buy binding. The + governance agent evaluates the buy against the registered plan and finds that + the total exceeds the agent's spending authority. The check returns denied + with a must-severity finding and instructions for escalating to a human. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_spend_authority/denied + stateful: true + expected: | + Return a denied governance decision: + - decision: denied + - findings: array with at least one must-severity finding + - severity: must + - code: SPENDING_AUTHORITY_EXCEEDED + - message: explains the agent's authority limit and how much the buy exceeds it + - escalation: instructions for human review + - governance_context: token/ID the buyer passes to the next check after escalation + - plan_id: the governance plan that triggered the denial + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + tool: "create_media_buy" + payload: + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_check_governance_denied_payload" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + start_time: "2027-01-01T00:00:00Z" + end_time: "2027-03-31T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + + context: + correlation_id: "media_buy_governance_escalation--check_governance_denied" + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains a governance decision" + - check: field_present + path: "findings" + description: "Response contains findings explaining the denial" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--check_governance_denied" + description: "Context correlation_id returned unchanged" + - id: human_escalation + title: "Human escalation — conditional approval" + narrative: | + The governance denial triggers human escalation. A human reviewer at the agency + reviews the proposed buy, the denial reason, and the governance plan. The human + approves the buy with conditions — for example, requiring weekly reporting. The + buyer calls check_governance again with the human's approval, and this time + the check returns approved with conditions. + + steps: + - id: check_governance_approved + title: "Re-check governance after human approval (approved with conditions)" + narrative: | + After the human reviewer approves the buy, the buyer calls check_governance + again with the governance_context from the prior denial and the human's + approval. The governance agent returns approved with conditions that the + buyer must honor during the campaign. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_spend_authority + stateful: true + expected: | + Return an approved governance decision with conditions: + - decision: approved + - conditions: array of requirements the buyer must honor + - e.g., "Weekly delivery reporting required" + - e.g., "Human review required for any budget increase" + - governance_context: updated token the buyer passes to create_media_buy + - approved_by: identifier of the human who approved + - approved_at: timestamp of approval + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + governance_context: "gov_ctx_acme_q2_escalated" + tool: "create_media_buy" + payload: + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_check_governance_approved_payload" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + start_time: "2027-01-01T00:00:00Z" + end_time: "2027-03-31T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + + context: + correlation_id: "media_buy_governance_escalation--check_governance_approved" + context_outputs: + - name: check_id + path: 'check_id' + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains an approved governance decision" + - check: field_present + path: "conditions" + description: "Approval includes conditions" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--check_governance_approved" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "check_id" + description: "Check ID for audit trail — must be echoed in report_plan_outcome" + - id: create_buy_with_governance + title: "Create media buy with governance context" + narrative: | + The buyer creates the media buy, passing the governance_context from the approved + governance check. The seller's platform verifies the governance approval before + confirming the buy. + + steps: + - id: create_media_buy + title: "Create a media buy with governance approval" + narrative: | + The buyer creates the media buy with the governance_context token from the + approved check. The seller's platform validates the governance approval and + confirms the buy. Without a valid governance_context, the platform would + reject the buy because the governance agent is registered for this account. + 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: | + Confirm the media buy with governance approval: + - media_buy_id: your platform's identifier + - status: active + - confirmed_at: timestamp + - governance_context: echoed back confirming governance was validated + - packages: confirmed line items + - valid_actions: creative sync, get_delivery, etc. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + governance_context: "gov_ctx_acme_q2_approved" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + pricing_option_id: "cpm_guaranteed" + creative_assignments: + - creative_id: "video_30s_trail_pro" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + creative_assignments: + - creative_id: "video_15s_trail_pro" + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_create_buy_with_governance_create_media_buy" + context: + correlation_id: "media_buy_governance_escalation--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: report_outcome + title: "Report governance outcome" + narrative: | + After the media buy is created, the buyer reports the outcome back to the + governance agent. This closes the governance loop by linking the actual media + buy to the governance check that authorized it. + + steps: + - id: report_plan_outcome + title: "Report media buy outcome to governance" + narrative: | + The buyer reports the media buy creation back to the governance agent, + linking the media_buy_id to the governance check. This enables the governance + agent to track what was actually purchased against what was approved. + task: report_plan_outcome + schema_ref: "governance/report-plan-outcome-request.json" + response_schema_ref: "governance/report-plan-outcome-response.json" + doc_ref: "/governance/campaign/tasks/report_plan_outcome" + comply_scenario: campaign_governance + stateful: true + expected: | + Acknowledge the outcome report: + - outcome_id: identifier for this outcome record + - plan_id: the governance plan + - media_buy_id: the buy that was created + - governance_context: the approval that authorized it + - status: recorded + + sample_request: + plan_id: "$context.plan_id" + governance_context: "gov_ctx_acme_q2_approved" + outcome: "completed" + seller_response: + seller_reference: "$context.media_buy_id" + committed_budget: 50000 + packages: + - package_id: "pkg_sports_ctv_q2" + committed_budget: 30000 + - package_id: "pkg_outdoor_video_q2" + committed_budget: 20000 + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_report_outcome_report_plan_outcome" + context: + correlation_id: "media_buy_governance_escalation--report_plan_outcome" + validations: + - check: response_schema + description: "Response matches report-plan-outcome-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--report_plan_outcome" + description: "Context correlation_id returned unchanged" + - id: audit_trail + title: "Governance audit trail" + narrative: | + The buyer (or an auditor) retrieves the full audit trail for the governance + plan. This shows every decision in the chain: the initial denial, the human + escalation, the conditional approval, the media buy creation, and the outcome + report. The audit trail provides accountability for automated spending decisions. + + steps: + - id: get_plan_audit_logs + title: "Retrieve the full governance audit trail" + narrative: | + The buyer requests the audit log for the governance plan. The log shows + every governance event: check requests, denials, escalations, approvals, + and outcome reports. Each entry has a timestamp, actor, and decision. + task: get_plan_audit_logs + schema_ref: "governance/get-plan-audit-logs-request.json" + response_schema_ref: "governance/get-plan-audit-logs-response.json" + doc_ref: "/governance/campaign/tasks/get_plan_audit_logs" + comply_scenario: campaign_governance + stateful: false + expected: | + Return the complete audit trail for the governance plan: + - plan_id: the governance plan + - entries: ordered list of governance events, including: + 1. Plan registered with $20K per-transaction threshold + 2. Governance check denied — spending authority exceeded ($50K > $20K) + 3. Human escalation initiated + 4. Human approved with conditions (weekly reporting, budget increase review) + 5. Media buy created with governance approval + 6. Outcome reported linking media_buy_id to governance context + - Each entry includes: timestamp, event_type, actor, decision, details + + sample_request: + plan_ids: + - "$context.plan_id" + + context: + correlation_id: "media_buy_governance_escalation--get_plan_audit_logs" + validations: + - check: response_schema + description: "Response matches get-plan-audit-logs-response.json schema" + - check: field_present + path: "plans" + description: "Response contains audit log entries" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--get_plan_audit_logs" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/media-buy/creative-reception.yaml b/dist/compliance/3.0.1/domains/media-buy/creative-reception.yaml new file mode 100644 index 0000000000..56b4b5fb59 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/creative-reception.yaml @@ -0,0 +1,247 @@ +id: creative_sales_agent +version: "1.0.0" +title: "Sales agent with creative capabilities" +category: creative_sales_agent +summary: "Stateful sales agent that accepts pushed creative assets and renders them in its environment." +track: creative +required_tools: + - sync_creatives + +narrative: | + You run a publisher platform, retail media network, or other sell-side system that + accepts creative assets from buyers. The buyer pushes assets or catalog items to your + platform, and you render them in your environment. + + Your agent is stateful: buyers push creatives to you via sync_creatives, and you + persist them for rendering. This is where catalogs get interesting — the buyer might + push product feeds (flights, hotels, retail products) that your platform renders as + native ads. + + This storyboard walks through the push-and-preview flow from the buyer's perspective. + +agent: + interaction_model: stateful_push + capabilities: + - has_creative_library + examples: + - "Publisher platforms" + - "Retail media networks" + - "Native ad platforms" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The buyer has creative assets (images, catalog feeds, or ad tags) ready to push. + The test kit provides sample assets compatible with common publisher formats. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports creative operations before browsing or building creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring creative in supported_protocols, confirming the agent handles creative operations. + sample_request: + context: + correlation_id: "creative_sales_agent--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: "creative_sales_agent--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: discover_accepted_formats + title: "Discover accepted formats" + narrative: | + The buyer first needs to know what creative formats your platform accepts. + For a publisher, this includes your native ad formats, display placements, + and any custom units. For retail media, this might include product listing + formats or sponsored product cards. + + steps: + - id: list_formats + title: "Discover accepted creative formats" + narrative: | + The buyer asks: "What creative formats does your platform accept?" Your + platform returns the formats you support — native post formats, display + units, video slots, or product listing formats. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_sync + stateful: false + expected: | + Return the creative formats your platform accepts. Each format should define: + - Asset requirements (what the buyer needs to provide) + - Render dimensions + - Any catalog requirements (for product-feed formats) + + - id: push_creatives + title: "Push creative assets" + narrative: | + The buyer pushes their creative assets to your platform. This could be: + - Standard display assets (images, HTML tags) + - Catalog items (product feeds, flight listings, hotel inventory) + - Native ad content (headlines, descriptions, images) + + Your platform validates the assets against your format specs and stores them. + + steps: + - id: sync_creatives + title: "Push creatives to the platform" + narrative: | + The buyer uploads creative assets to your platform. For standard ads, this + is images and copy. For catalog-driven formats, this is a product feed or + set of catalog items. Your platform validates each creative against the + format's asset requirements and returns a per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept the creatives, validate against format specifications, and return: + - Per-creative action (created or updated) + - Per-creative status (accepted, pending_review, rejected) + - Platform-assigned IDs if applicable + - Validation errors for rejected creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "acme_summer_native_001" + name: "Acme Summer Sale — native 2026" + format_id: + agent_url: "https://your-platform.example.com" + id: "native_post" + assets: + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-master.jpg" + width: 1200 + height: 628 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/summer-sale" + + idempotency_key: "$generate:uuid_v4#creative_sales_agent_push_creatives_sync_creatives" + context: + correlation_id: "creative_sales_agent--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_sales_agent--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: preview + title: "Preview pushed creatives" + narrative: | + After pushing assets, the buyer wants to see how their creatives will render + in your platform's environment. For a publisher, this shows the ad in the + publication's native chrome — with engagement buttons, community badges, and + platform-specific styling that the buyer can't preview elsewhere. + + steps: + - id: preview_synced + title: "Preview a pushed creative" + narrative: | + The buyer asks to see how a synced creative will look in your environment. + Your platform renders the creative with its native chrome — the surrounding + UI, engagement buttons, and platform-specific styling. + task: preview_creative + schema_ref: "creative/preview-creative-request.json" + response_schema_ref: "creative/preview-creative-response.json" + doc_ref: "/creative/task-reference/preview_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a preview showing the creative in your platform's environment. + The preview should include your platform's native chrome — not just the + raw assets, but how they'll actually appear to users. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + request_type: "single" + creative_manifest: + creative_id: "acme_summer_native_001" + format_id: + agent_url: "https://your-platform.example.com" + id: "native_post" + assets: + headline: + asset_type: "text" + content: "Summer Sale — 40% Off" + body: + asset_type: "text" + content: "Top-rated outdoor gear. This weekend only." + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero.jpg" + width: 1200 + height: 628 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/summer-sale" + output_format: "url" + quality: "draft" + + context: + correlation_id: "creative_sales_agent--preview_synced" + validations: + - check: response_schema + description: "Response matches preview-creative-response.json schema" + - check: field_present + path: "previews[0].renders[0].preview_url" + description: "Preview includes a renderable URL" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_sales_agent--preview_synced" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/media-buy/index.yaml b/dist/compliance/3.0.1/domains/media-buy/index.yaml new file mode 100644 index 0000000000..38c421a694 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/index.yaml @@ -0,0 +1,769 @@ +id: media_buy_seller +version: "1.0.0" +title: "Media buy seller agent" +category: media_buy_seller +summary: "Seller agent that receives briefs, returns products, accepts media buys, and reports delivery." +track: media_buy +required_tools: + - get_products + - create_media_buy +requires_scenarios: + - media_buy_seller/refine_products + - media_buy_seller/delivery_reporting + - media_buy_seller/measurement_terms_rejected + - media_buy_seller/pending_creatives_to_start + - media_buy_seller/inventory_list_targeting + - 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 + sells advertising inventory. A buyer agent connects to discover your products, create + media buys, sync creatives, and monitor delivery. Your agent handles the full lifecycle + from brief to reporting. + + This storyboard walks through the core media buy flow: account setup, product discovery, + buy creation, creative sync, and delivery monitoring. + + Governance integration, product refinement, and proposal finalization are tested by + required scenarios that run alongside this storyboard. See requires_scenarios for the + full set of seller behaviors validated. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - supports_guaranteed + - supports_non_guaranteed + examples: + - "Yahoo" + - "Retail media networks" + - "Publisher platforms" + - "SSPs" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials for account setup. + The test kit provides a sample brand (Acme Outdoor) with campaign parameters + suitable for testing the full media buy flow. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "sports_preroll_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_30s" + - product_id: "lifestyle_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + pricing_options: + - product_id: "sports_preroll_q2" + pricing_option_id: "cpm_guaranteed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 22.0 + - product_id: "lifestyle_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "media_buy_seller--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: "media_buy_seller--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: account_setup + title: "Account setup" + narrative: | + Before buying anything, the buyer establishes an account relationship with + your platform. This is the handshake: the buyer tells you which brand and + agency (operator) they represent, and you return an account ID, status, and + any setup requirements. + + Some platforms approve accounts instantly. Others require human review — the + buyer gets back a pending_approval status and a URL to complete setup. The + buyer polls or waits for a webhook until the account is active. + + steps: + - id: sync_accounts + title: "Establish account relationship" + narrative: | + The buyer registers their brand and operator with your platform. This is + the first call in any new relationship. Your platform validates the request, + provisions the account, and returns its status. + + If your platform requires manual approval (credit checks, sales team review), + return the account with status pending_approval and account.setup.url populated. + The buyer directs the human to that URL to complete setup, then polls list_accounts + until the account status changes to active. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + # No TestScenario exists for account setup + stateful: true + expected: | + Return the account with: + - account_id: your platform's identifier for this relationship + - action: created or updated + - status: active (instant approval) or pending_approval (requires human review) + - account_scope: operator, brand, operator_brand, or agent + - setup.url and setup.message: populated on the account when status is pending_approval (where the human completes onboarding) + - rate_card: pricing tiers if applicable + - payment_terms: net_30, prepay, etc. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_account_setup_sync_accounts" + context: + correlation_id: "media_buy_seller--sync_accounts" + + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + - check: field_present + path: "accounts[0].status" + description: "Account has a status (active or pending_approval)" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--sync_accounts" + description: "Context correlation_id returned unchanged" + + - id: governance_setup + title: "Governance agent registration" + narrative: | + The buyer registers their governance agent with your platform. This tells your + platform where to call check_governance before confirming media buys. + + steps: + - id: sync_governance + title: "Register governance agents" + narrative: | + The buyer tells your platform: "Before you confirm any media buy for this + account, call this governance agent to validate it." + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + requires_tool: sync_governance + stateful: true + expected: | + Acknowledge the governance agents. Your platform should: + - Store the governance agent URLs for the specified accounts + - Return confirmation that agents were registered + + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "https://governance.pinnacle-agency.example" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_setup_sync_governance" + context: + correlation_id: "media_buy_seller--sync_governance" + ext: + test_platform: + governance_tier: "standard" + + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--sync_governance" + description: "Context correlation_id returned unchanged" + + - id: product_discovery + title: "Product discovery" + narrative: | + The buyer sends a natural-language brief describing what they want to buy. + Your platform interprets the brief against your inventory and returns products — + structured representations of what you can sell, with pricing, delivery forecasts, + targeting options, and creative requirements. + + This is where seller differentiation happens. The same brief sent to three sellers + produces three different product sets. Your AI interprets "premium video on sports + and outdoor lifestyle" against your specific inventory, audiences, and pricing. + + steps: + - id: get_products_brief + title: "Send a brief" + narrative: | + The buyer describes what they want in natural language. Your platform returns + products that match the brief, including pricing options, delivery forecasts, + and creative format requirements. + + This call may take up to 60 seconds — your platform is running AI inference + against your inventory catalog. If the brief is ambiguous, you can return + input-required to ask clarifying questions before producing results. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. Each product should include: + - product_id: unique identifier + - name and description + - delivery_type: guaranteed or non_guaranteed + - pricing_models: available pricing options (CPM, CPC, etc.) + - forecast: estimated impressions, reach + - creative_format_ids: what creative formats this product requires + - targeting: what audiences or contexts this product reaches + + Optionally return proposals — curated media plans that bundle products + with budget allocations the buyer can accept or refine. + + If the brief is unclear, return input-required with clarifying questions. + + sample_request: + buying_mode: "brief" + brief: "Premium video inventory on sports and outdoor lifestyle publishers. Q2 flight, $50K budget. Adults 25-54, US and Canada." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context: + correlation_id: "media_buy_seller--get_products_brief" + + context_outputs: + - name: product_format_id + path: 'products[0].format_ids[0]' + - name: product_id + path: 'products[0].product_id' + - name: pricing_option_id + path: 'products[0].pricing_options[0].pricing_option_id' + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + - check: field_present + path: "products[0].product_id" + description: "Each product has a product_id" + - check: field_present + path: "products[0].delivery_type" + description: "Each product declares guaranteed or non_guaranteed delivery" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--get_products_brief" + description: "Context correlation_id returned unchanged" + + - check: field_present + path: "products[0].publisher_properties" + description: "Products include publisher_properties" + - id: list_formats_integrity + title: "Verify format_ids on products resolve to real formats" + narrative: | + The buyer asks the sales agent to filter `list_creative_formats` by + `products[0].format_ids[0]`. The sales agent MUST return the format + it advertised on its own product — whether it hosts that format + directly or proxies to the creative agent named in + `format_ids[0].agent_url`. An empty `formats[]` means the sales + agent's product catalog references a format that does not resolve — + a stale or typo'd entry that would have failed silently at + `sync_creatives` after the media buy was already committed. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + The sales agent resolves `products[0].format_ids[0]` and returns + the matching format entry: + - formats[] contains at least one entry + - formats[0].format_id matches the id captured from get_products + + An empty formats[] means the sales agent's product catalog references + a format that does not resolve — a common production failure mode + when creative agents deprecate formats without sellers updating + their product catalog. + sample_request: + format_ids: + - "$context.product_format_id" + context: + correlation_id: "media_buy_seller--list_formats_integrity" + # The @adcp/client `list_creative_formats` request builder up + # through 5.10.0 (the currently-published release) returns `{}`, + # and the runner's post-builder merge only forwards envelope + # fields (context / ext / idempotency_key / + # push_notification_config) from sample_request — so `format_ids` + # above never reaches the wire and the seller answers with its + # full format catalog. `context_inputs` is applied after the + # builder runs, so this injects the captured `product_format_id` + # (the `{agent_url, id}` object from `products[0].format_ids[0]`) + # at `format_ids[0]` and lets the round-trip invariant actually + # grade. Drop once we bump past the @adcp/client release that + # ships adcontextprotocol/adcp-client#789. + context_inputs: + - key: product_format_id + inject_at: "format_ids[0]" + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats[0]" + description: "Sales agent resolves the format_id — products[0].format_ids[0] exists in the catalog" + - check: field_value + path: "formats[0].format_id" + value: "$context.product_format_id" + description: "Returned format_id round-trips verbatim — the agent cannot substitute a different format in response to the filter" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--list_formats_integrity" + description: "Context correlation_id returned unchanged" + - id: create_buy + title: "Create the media buy" + narrative: | + The buyer is satisfied with the products and creates a media buy. This is the + equivalent of signing an IO — the buyer commits to specific products, budgets, + and flight dates. + + This operation may be synchronous (completed immediately), short-async (working + while your platform processes), or long-async (task stays submitted while a human + signs the IO internally; task completion delivers the final media_buy_id). There + is no "pending_approval" media buy status — IO review is modelled at the task + layer, not as a MediaBuy.status value. + + If the buyer registered governance agents in Phase 2, your platform calls + check_governance before confirming the buy. The governance agent validates budget + authority, brand safety, and compliance. If governance denies the buy, return the + denial — don't override it. + + steps: + - id: create_media_buy + title: "Create a media buy" + narrative: | + The buyer commits to specific products with budgets and flight dates. Your + platform validates the request, optionally calls governance, and either confirms + the buy or sends it through an approval workflow. + + Two creation modes: + - Manual: buyer specifies packages array with explicit product selections + - Proposal: buyer passes a proposal_id from get_products to execute a proposal + + The response status tells the buyer what happens next: + - completed: buy is active and live + - working: your platform is processing (poll or wait for webhook) + - submitted: long-running async — approval workflow, IO signing, etc. + - input-required: need more information (budget clarification, approval) + 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: | + Process the media buy request and return one of: + + Synchronous (completed): + - media_buy_id: your platform's identifier + - status: active or pending_creatives + - packages: line items with pricing + - confirmed_at: timestamp + - valid_actions: what the buyer can do next + + Asynchronous (working): + - percentage: 0-100 completion + - current_step: what's happening ("Validating inventory", "Checking governance") + + Async with human approval (submitted): + - task_id / taskId: handle the buyer polls or receives webhooks on + - message (optional): explanation of what the seller is waiting on (e.g., "Awaiting IO signature from sales team; typical turnaround 2–4 hours") + - No media_buy_id yet — it is issued on task completion + - Seller-side IO signing is modelled here (task stays submitted until signed). Do not emit a "pending_approval" media buy status — that value is not in MediaBuy.status + + Needs input (input-required): + - reason: APPROVAL_REQUIRED, BUDGET_EXCEEDS_LIMIT, CLARIFICATION_NEEDED + - errors: what needs to be resolved + - Used only when the seller needs the buyer to respond (e.g., confirm a budget). If the blocker is account-level (credit application, funding), the resolution path is list_accounts / sync_accounts, where account.setup.url surfaces on the pending_approval account + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_preroll_q2" + budget: 25000 + pricing_option_id: "cpm_guaranteed" + creative_assignments: + - creative_id: "video_30s_trail_pro" + - product_id: "lifestyle_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + 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_buy_create_media_buy" + context: + correlation_id: "media_buy_seller--create_media_buy" + + context_outputs: + - name: media_buy_id + path: 'media_buy_id' + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--create_media_buy" + description: "Context correlation_id returned unchanged" + + - id: check_buy_status + title: "Check media buy status" + narrative: | + Once create_media_buy has completed and a media_buy_id is issued, the buyer + calls get_media_buys to read current state — pending_creatives, pending_start, + active, paused, completed, rejected, or canceled. + + While the create_media_buy task is still submitted (e.g., waiting on internal + IO signing), the media buy does not exist as a queryable MediaBuy yet. IO + review is tracked at the task layer, not as a MediaBuy.status value. The buyer + polls tasks/get or waits on the webhook until the task completes and a + media_buy_id is delivered. + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Return the current state of the media buy: + - media_buy_id: matches what was returned from create_media_buy + - status: pending_creatives, pending_start, active, paused, completed, rejected, canceled + - packages: line items with current delivery status + - valid_actions: what operations are available in this state + + If pending_creatives: + - Include message explaining what creatives are needed + - valid_actions should include sync_creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + context: + correlation_id: "media_buy_seller--check_buy_status" + + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_present + path: "media_buys[0].status" + description: "Each media buy has a status" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--check_buy_status" + description: "Context correlation_id returned unchanged" + + - id: creative_sync + title: "Creative sync" + narrative: | + With the media buy confirmed, the buyer syncs creative assets to your platform. + Each package in the buy has creative format requirements — the buyer discovered + these during product discovery and now pushes matching assets. + + The format_ids used in sync_creatives must match those returned by your platform + in get_products and list_creative_formats. If your platform returns a format_id + in a product but rejects it when the buyer echoes it back in sync_creatives, the + buyer cannot fulfill the creative requirements. This is a common compliance failure. + + Your platform validates each creative against the format specs and returns + per-creative status. If assets need review or transcoding, the operation may + go async. + + steps: + - id: list_formats + title: "Check creative format requirements" + narrative: | + The buyer confirms what creative formats the confirmed packages require. + Your platform returns format specs with asset requirements, dimensions, + and constraints. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return creative formats your platform accepts. Each format should define: + - format_id with your agent_url and unique id + - Asset requirements (dimensions, file sizes, mime types) + - Render dimensions + + sample_request: + context: + correlation_id: "media_buy_seller--list_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--list_formats" + description: "Context correlation_id returned unchanged" + - check: refs_resolve + description: | + Every format_id returned on products resolves to a format in this + agent's list_creative_formats. Broken references here surface as a + grading failure instead of a silent mismatch that only breaks at + sync_creatives time, after the buy is committed. Third-party + format_ids (agent_url pointing at a different creative agent) + can't be verified without calling that agent and are reported as + observations rather than failures. + source: + from: context + path: "products[*].format_ids[*]" + target: + from: current_step + path: "formats[*].format_id" + match_keys: [agent_url, id] + scope: + key: agent_url + equals: $agent_url + on_out_of_scope: warn + + - id: sync_creatives + title: "Push creative assets (format_id roundtrip)" + narrative: | + The buyer uploads creative assets for the confirmed packages. The first + creative uses $context.product_format_id — the exact format_id object + returned by get_products. This is the roundtrip test: the seller must + accept its own format_ids without modification. If the seller's validation + rejects a format_id that it returned in products, this step fails. + + Your platform validates each creative against the format specs, transcodes + if necessary, and returns per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept and validate creatives: + - Per-creative action: created or updated + - Per-creative status: accepted, pending_review, or rejected + - Validation errors for rejected creatives + - Platform-assigned IDs if applicable + + The first creative uses a format_id extracted from get_products. + If this is rejected, your format_ids do not roundtrip correctly. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "video_30s_trail_pro" + name: "Trail Pro 3000 - 30s CTV Spot" + format_id: "$context.product_format_id" + assets: + video: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + width: 1920 + height: 1080 + duration_ms: 30000 + mime_type: "video/mp4" + - creative_id: "display_trail_pro_300x250" + name: "Trail Pro 3000 - Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + width: 300 + height: 250 + mime_type: "image/png" + idempotency_key: "$generate:uuid_v4#media_buy_seller_creative_sync_sync_creatives" + context: + correlation_id: "media_buy_seller--sync_creatives" + + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--sync_creatives" + description: "Context correlation_id returned unchanged" + + - id: delivery_monitoring + title: "Delivery and reporting" + narrative: | + The campaign is live. The buyer monitors delivery through two tasks: + get_media_buys for status and get_media_buy_delivery for performance metrics. + + Your platform reports in a standard format — impressions, clicks, spend, + completion rates — so the buyer can compare delivery across multiple sellers + in a single view. + + steps: + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data for the active media buy. Your platform + returns performance metrics — impressions, clicks, spend, completion rates — + broken down by package and optionally by day. + + This call may take up to 60 seconds as your platform aggregates reporting + data across delivery systems. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics for the media buy: + - Per-package delivery: impressions, clicks, spend, completion rates + - Daily breakdown if requested (include_package_daily_breakdown) + - Pacing information: on track, ahead, behind + - Budget utilization: spent vs. committed + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + context: + correlation_id: "media_buy_seller--get_delivery" + + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--get_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/create_media_buy_async.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/create_media_buy_async.yaml new file mode 100644 index 0000000000..c2ba4c3ec0 --- /dev/null +++ b/dist/compliance/3.0.1/domains/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/dist/compliance/3.0.1/domains/media-buy/scenarios/creative_fate_after_cancellation.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/creative_fate_after_cancellation.yaml new file mode 100644 index 0000000000..4c9fe67900 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/creative_fate_after_cancellation.yaml @@ -0,0 +1,414 @@ +id: media_buy_seller/creative_fate_after_cancellation +version: "1.0.0" +title: "Creative lifecycle is decoupled from media buy lifecycle" +category: media_buy_seller +summary: "Validates that canceling a media buy releases package-creative assignments but leaves the underlying creatives in the library with their review state intact, and that buyers can reuse released creatives on a new buy." +track: media_buy +required_tools: + - get_products + - create_media_buy + - update_media_buy + - sync_creatives + - list_creatives + +narrative: | + Per the creative library model (docs/creative/creative-libraries#creative-state-and-assignment-state-are-separate) + and the Media Buy State Transitions rule, canceling or rejecting a media buy + releases its package-creative assignments but leaves the creatives themselves + in the library. The creatives remain reusable by `creative_id` in a subsequent + `create_media_buy` or `sync_creatives` call, and a seller MUST NOT implicitly + reject a creative because its containing buy was canceled — a creative + rejection MUST be a deliberate review decision with its own `rejection_reason`. + + This scenario walks the whole flow end-to-end: + + 1. Create a buy, sync a creative, assign it to a package (setup) + 2. Confirm the creative is in the library with a non-terminal review state (pre-cancel) + 3. Cancel the buy + 4. Confirm the creative is STILL in the library with its review state intact — + not archived, not auto-rejected as a side effect of the cancel + 5. Reuse the same `creative_id` on a new buy via `sync_creatives` assignment + + A seller that evaporates library creatives on buy cancellation, or that flips + `status: rejected` on creatives whose only assignment was released by a cancel, + fails this scenario. A seller that correctly decouples the two lifecycles + passes. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + examples: + - "Any media buy seller with a creative library" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports create_media_buy, update_media_buy with cancellation, sync_creatives, + and list_creatives. Catalog-driven sellers and sellers without a creative library + grade this scenario not_applicable (creative lifecycle assumes a library surface). + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Create a buy and assign a creative" + narrative: | + Discover a product, create a media buy, sync one creative with an inline + assignment to the buy's first package. Capture the media_buy_id, package_id, + and creative_id for subsequent phases. + + steps: + - id: get_products_brief + title: "Discover a product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product with a pricing option and at least one format_id. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content for creative-reuse testing." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context: + correlation_id: "creative_fate--get_products" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + - path: "products[0].format_ids[0].agent_url" + key: "format_agent_url" + - path: "products[0].format_ids[0].id" + key: "format_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Product exposes at least one format_id for creative sync" + + - id: create_buy + title: "Create the initial media buy" + 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: | + Media buy created with media_buy_id and at least one package. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "creative-fate-setup-create-buy-v1" + start_time: "2026-09-01T00:00:00Z" + end_time: "2026-09-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 5000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "creative_fate--create_buy" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns media_buy_id" + - check: field_present + path: "packages[0].package_id" + description: "Seller returns package_id for the newly created package" + + - id: sync_creative_with_assignment + title: "Sync a creative and assign to the package" + narrative: | + Sync one creative into the library and assign it to the package in a single + sync_creatives call. The creative enters the library with the library's + review flow; the assignment binds it to the media buy's package. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Creative accepted into the library (action: created), assignment acknowledged. + Creative status is one of: pending_review, approved, processing. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "acme_reuse_banner_001" + name: "Acme Outdoor reuse banner" + format_id: + agent_url: "$context.format_agent_url" + id: "$context.format_id" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/acme-reuse-banner.png" + width: 300 + height: 250 + mime_type: "image/png" + assignments: + - creative_id: "acme_reuse_banner_001" + package_id: "$context.package_id" + idempotency_key: "creative-fate-setup-sync-v1" + context: + correlation_id: "creative_fate--sync_creative_with_assignment" + context_outputs: + - path: "creatives[0].creative_id" + key: "creative_id" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].creative_id" + description: "Response echoes back the buyer-supplied creative_id" + + - id: verify_creative_in_library_pre_cancel + title: "Baseline: creative is in the library with a non-terminal review state" + narrative: | + Before canceling the buy, list the creative via list_creatives and record the + review state. The purpose of this phase is to establish the baseline the + post-cancel phase compares against: the creative exists and has a status + that is NOT `rejected` or `archived`. + + steps: + - id: list_creatives_before_cancel + title: "Look up the creative in the library" + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_library + stateful: true + expected: | + list_creatives returns the creative with a non-terminal review state + (processing, pending_review, or approved). + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + filters: + creative_ids: + - "$context.creative_id" + context: + correlation_id: "creative_fate--list_creatives_before_cancel" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives[0].creative_id" + description: "Creative is present in the library" + - check: field_value + path: "creatives[0].status" + allowed_values: ["processing", "pending_review", "approved"] + description: "Creative status is non-terminal (not rejected or archived) before cancel" + + - id: cancel_buy + title: "Cancel the media buy" + narrative: | + Cancel the media buy with `canceled: true`. The seller MUST transition the buy + to `canceled` and release the creative's package assignment per + docs/media-buy/specification#media-buy-state-transitions. + + steps: + - id: update_media_buy_canceled + title: "update_media_buy with canceled: true" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Seller acknowledges the cancellation and transitions the buy to `canceled`. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + cancellation_reason: "Creative-fate scenario: releasing assignment to verify library persistence." + idempotency_key: "creative-fate-cancel-v1" + context: + correlation_id: "creative_fate--update_media_buy_canceled" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + + - id: verify_creative_persists_post_cancel + title: "Creative remains in the library with review state intact" + narrative: | + After the cancel, the creative's library entry MUST still exist and its review + state MUST NOT be `rejected` (which would indicate implicit-reject-on-cancel, + forbidden by the spec) and SHOULD NOT be `archived` (which would indicate the + seller evaporated library state on buy cancellation, inconsistent with the + decoupled-lifecycle contract). Allowed terminal-adjacent states are + `processing`, `pending_review`, `approved` — whatever the review flow produced. + + steps: + - id: list_creatives_after_cancel + title: "Look up the creative again, post-cancel" + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_library + stateful: true + expected: | + Creative still present with non-rejected, non-archived status. A seller + that returns an empty list, or that has flipped the creative to + `rejected` or `archived` as a side effect of the cancel, fails. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + filters: + creative_ids: + - "$context.creative_id" + context: + correlation_id: "creative_fate--list_creatives_after_cancel" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives[0].creative_id" + description: "Creative still in the library after buy cancellation" + - check: field_value + path: "creatives[0].creative_id" + value: "acme_reuse_banner_001" + description: "Creative ID is unchanged (not re-keyed on cancel)" + - check: field_value + path: "creatives[0].status" + allowed_values: ["processing", "pending_review", "approved"] + description: "Creative status is NOT rejected and NOT archived — no implicit review cascade from the buy cancel" + + - id: reuse_creative_on_new_buy + title: "Reuse the released creative on a new media buy" + narrative: | + With the old buy canceled and the assignment released, the buyer creates a NEW + media buy and references the same creative_id in a fresh assignment. The seller + MUST accept the assignment — the creative_id resolves to the persisted library + entry, demonstrating end-to-end reusability. + + steps: + - id: create_second_buy + title: "Create a second media buy" + 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: | + Second media buy created successfully with a new media_buy_id and package_id. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "creative-fate-second-buy-v1" + start_time: "2026-10-01T00:00:00Z" + end_time: "2026-10-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 5000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "creative_fate--create_second_buy" + context_outputs: + - path: "media_buy_id" + key: "second_media_buy_id" + - path: "packages[0].package_id" + key: "second_package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Second media_buy_id returned" + - check: field_present + path: "packages[0].package_id" + description: "Second package_id returned" + + - id: reassign_creative + title: "Assign the original creative_id to the new package" + narrative: | + Reference the original creative by creative_id only (no assets, no re-upload) + and assign it to the new package. The seller resolves the creative_id from + the library; if the creative was evaporated on cancel, this call fails. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Assignment accepted. No creative-not-found or similar error. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + # Buyer-authoritative id set in sync_creative_with_assignment — + # use it literally instead of round-tripping through + # `$context.creative_id`. That context key is populated from + # the seller's response at `creatives[0].creative_id`; sellers + # whose envelope doesn't surface that exact path resolve to + # undefined and the template engine strips the creative, + # leaving `creatives: undefined` which fails pre-flight zod. + - creative_id: "acme_reuse_banner_001" + name: "Reassigned creative" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/banner_300x250.jpg" + width: 300 + height: 250 + assignments: + - creative_id: "acme_reuse_banner_001" + package_id: "$context.second_package_id" + idempotency_key: "creative-fate-reassign-v1" + context: + correlation_id: "creative_fate--reassign_creative" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/delivery_reporting.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/delivery_reporting.yaml new file mode 100644 index 0000000000..75596a2306 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/delivery_reporting.yaml @@ -0,0 +1,205 @@ +id: media_buy_seller/delivery_reporting +version: "1.0.0" +title: "Seller returns valid delivery reporting" +category: media_buy_seller +summary: "Verifies that get_media_buy_delivery returns schema-compliant delivery data after simulated delivery via the test controller." +track: reporting +required_tools: + - get_products + - create_media_buy + - get_media_buy_delivery + - comply_test_controller + +narrative: | + Delivery reporting is how buyers know if their campaign is working. The seller must + return schema-compliant delivery data from get_media_buy_delivery with per-package + metrics (impressions, spend, pacing). + + This scenario creates a media buy, injects delivery data via the test controller's + simulate_delivery scenario, then calls get_media_buy_delivery and validates the + response against the schema. Without this test, sellers can return arbitrary formats + and still pass certification. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + examples: + - "Any media buy seller" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The seller must implement comply_test_controller with the simulate_delivery + scenario. This allows the test harness to inject delivery metrics into a + media buy so get_media_buy_delivery has data to return. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "outdoor_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "outdoor_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + +phases: + - id: setup + title: "Create a media buy for delivery testing" + steps: + - id: sync_accounts + title: "Establish account" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_delivery_reporting_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Display and video inventory on outdoor lifestyle content. Q2 flight, $25K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy + title: "Create media buy" + 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: | + Create the media buy. We need a media_buy_id to simulate delivery against. + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 10000 + pricing_option_id: "cpm_standard" + idempotency_key: "$generate:uuid_v4#media_buy_seller_delivery_reporting_setup_create_media_buy" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - id: simulate_and_verify + title: "Simulate delivery and validate reporting" + narrative: | + Inject delivery metrics via the test controller, then call get_media_buy_delivery + and validate the response format. This is the core test — does the seller return + schema-compliant delivery data with per-package metrics? + + steps: + - id: simulate_delivery + title: "Inject simulated delivery metrics" + task: comply_test_controller + requires_tool: comply_test_controller + stateful: true + expected: | + The test controller acknowledges the simulated delivery data. + sample_request: + scenario: "simulate_delivery" + params: + media_buy_id: "$context.media_buy_id" + impressions: 5000 + clicks: 150 + reported_spend: + amount: 250.00 + currency: "USD" + validations: + - check: field_value + path: "success" + allowed_values: [true] + description: "Delivery simulation succeeds" + + - id: get_delivery + title: "Get delivery report and validate schema" + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics reflecting the simulated data: + - media_buy_deliveries array with at least one entry + - Per-package breakdown with impressions, spend + - Response matches the get-media-buy-delivery-response.json schema + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains delivery data" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_approved.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_approved.yaml new file mode 100644 index 0000000000..1d7a43b041 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_approved.yaml @@ -0,0 +1,211 @@ +id: media_buy_seller/governance_approved +version: "1.0.0" +title: "Seller creates buy when governance approves" +category: media_buy_seller +summary: "Verifies that the seller creates a media buy when governance approves the transaction." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + This is a multi-agent test. The test harness sets up a permissive governance plan on + a governance agent with a $100K budget, then registers that agent with the seller. + The buyer creates a $25K buy which falls within limits. + + When the seller calls check_governance during create_media_buy, the governance agent + approves. The seller must confirm the buy normally. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "outdoor_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "outdoor_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + +phases: + - id: governance_plan_setup + title: "Set up permissive governance plan" + narrative: | + Create a governance plan on the governance agent with a high budget ($100K). + The subsequent $25K buy will fall within these limits and should be approved. + + steps: + - id: sync_plans + title: "Create permissive governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan with a plan_id. + sample_request: + idempotency_key: "comply-gov-approved-sync-plans-v1" + plans: + - plan_id: "comply-gov-approved-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 outdoor lifestyle campaign — display and video" + budget: + total: 100000 + currency: "USD" + reallocation_threshold: 100000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US", "CA"] + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_approved_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_approved_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_approved + title: "Create buy within governance limits" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Display and video inventory on outdoor lifestyle content. Q2 flight, $25K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy + title: "Create buy (governance approves)" + 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: | + The buy succeeds — governance approved because the $25K buy is within + the plan's $100K budget. + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 10000 + pricing_option_id: "cpm_standard" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_approved_buy_approved_create_media_buy" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_conditions.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_conditions.yaml new file mode 100644 index 0000000000..876db0da4e --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_conditions.yaml @@ -0,0 +1,196 @@ +id: media_buy_seller/governance_conditions +version: "1.0.0" +title: "Seller attaches conditions when governance approves with conditions" +category: media_buy_seller +summary: "Verifies that the seller attaches governance conditions to the buy when governance approves with conditions." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + This is a multi-agent test. The test harness sets up a governance plan on a governance + agent with custom policies that trigger conditions (e.g., "CTV buys require weekly + delivery reporting"). The buyer creates a CTV buy within budget but matching a policy. + + When the seller calls check_governance, the governance agent approves with conditions. + The seller must create the buy and include the governance conditions and context token + in its response. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up conditional governance plan" + narrative: | + Create a governance plan on the governance agent with custom policies that + trigger conditions. The plan has sufficient budget but policies that require + conditions on CTV buys. + + steps: + - id: sync_plans + title: "Create conditional governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan with a plan_id. + sample_request: + idempotency_key: "comply-gov-conditions-sync-plans-v1" + plans: + - plan_id: "comply-gov-conditions-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 CTV campaign with reporting requirements" + budget: + total: 100000 + currency: "USD" + reallocation_threshold: 100000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US", "CA"] + custom_policies: + - policy_id: "ctv_weekly_reporting" + enforcement: "must" + policy: "CTV buys require weekly delivery reporting." + - policy_id: "ugc_brand_safety" + enforcement: "must" + policy: "UGC placements require brand safety review before activation." + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_conditions_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_conditions_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_with_conditions + title: "Create CTV buy triggering governance conditions" + steps: + - id: get_products_brief + title: "Discover CTV products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return CTV/video products matching the brief. + sample_request: + buying_mode: "brief" + brief: "CTV and connected TV inventory on outdoor lifestyle content. Q2 flight, $25K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy_conditions + title: "Create CTV buy (governance approves with conditions)" + 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: | + The buy succeeds with governance conditions attached: + - media_buy_id: present + - status: active or pending_creatives + - governance_context: token from the governance agent + - conditions visible to the buyer + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_ctv_q2" + budget: 25000 + pricing_option_id: "cpm_standard" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_conditions_buy_with_conditions_create_media_buy_conditions" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_denied.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_denied.yaml new file mode 100644 index 0000000000..c93b8fdf46 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_denied.yaml @@ -0,0 +1,192 @@ +id: media_buy_seller/governance_denied +version: "1.0.0" +title: "Seller rejects buy when governance denies" +category: media_buy_seller +summary: "Verifies that the seller rejects a media buy and propagates the denial when governance denies the transaction." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + This is a multi-agent test. The test harness sets up a governance plan on a governance + agent with a strict $10K budget, then registers that agent with the seller. The buyer + attempts a $50K media buy which exceeds the plan's budget. + + When the seller calls check_governance, the governance agent denies because the + requested budget exceeds the plan total. The seller must reject the buy and propagate + the denial back to the buyer. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up strict governance plan" + narrative: | + Create a governance plan on the governance agent with a strict $10K budget + and agent_limited authority. The subsequent $50K buy will exceed the plan + budget and should be denied. + + steps: + - id: sync_plans + title: "Create strict governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan with a plan_id. + sample_request: + idempotency_key: "comply-gov-denied-sync-plans-v1" + plans: + - plan_id: "comply-gov-denied-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Small test campaign — limited budget authority" + budget: + total: 10000 + currency: "USD" + reallocation_threshold: 5000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US"] + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_denied + title: "Create buy exceeding governance limits" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Premium video and display on outdoor lifestyle. Q2 flight, $50K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy_denied + title: "Create buy (governance denies — should fail)" + 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 + expect_error: true + negative_path: payload_well_formed + expected: | + The buy is rejected because governance denied — the $50K buy exceeds + the plan's $10K budget. The seller propagates the denial with findings. + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + idempotency_key: "$generate:uuid_v4#governance_denied_create_media_buy" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_display_q2" + budget: 30000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + validations: + - check: error_code + value: "GOVERNANCE_DENIED" + description: "Error code indicates governance denial" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_denied_recovery.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_denied_recovery.yaml new file mode 100644 index 0000000000..c16e8780f5 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/governance_denied_recovery.yaml @@ -0,0 +1,244 @@ +id: media_buy_seller/governance_denied_recovery +version: "1.0.0" +title: "Seller accepts corrected buy after governance denial" +category: media_buy_seller +summary: "Verifies that a buyer can recover from GOVERNANCE_DENIED by shrinking the buy to within plan limits and retrying." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + GOVERNANCE_DENIED is a correctable error, not a dead end. The buyer must be able to + read the denial findings, adjust the media buy, and retry. This scenario exercises the + full loop: strict governance plan ($10K), failed $50K buy that is denied, then a + corrected $8K buy that fits within the plan and is approved. + + The seller must propagate the governance findings unchanged so the buyer can identify + exactly which constraint was violated (budget authority, brand policy, etc.) and + correct it. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up strict governance plan" + narrative: | + Create a governance plan with a $10K budget. The initial $50K buy will exceed this + limit; the retry at $8K will fit. + + steps: + - id: sync_plans + title: "Create strict governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan. + sample_request: + idempotency_key: "comply-gov-recovery-sync-plans-v1" + plans: + - plan_id: "comply-gov-recovery-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Strict-budget plan for denial + recovery validation" + budget: + total: 10000 + currency: "USD" + reallocation_threshold: 5000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US"] + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_recovery_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_recovery_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_denied + title: "Initial buy exceeds plan — governance denies" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content. Q2 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy_denied + title: "Attempt $50K buy — denied" + 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 + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + The buy is rejected with GOVERNANCE_DENIED. The response includes findings + from the governance agent explaining which constraint was exceeded. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "gov-recovery-initial-denied-v1" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 50000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "governance_denied_recovery--create_media_buy_denied" + validations: + - check: error_code + value: "GOVERNANCE_DENIED" + description: "Error code is GOVERNANCE_DENIED" + - check: field_value + path: "context.correlation_id" + value: "governance_denied_recovery--create_media_buy_denied" + description: "Response echoes context.correlation_id verbatim on error responses (echo contract applies to both success and failure)" + + - id: buy_retried + title: "Corrected buy within plan — governance approves" + narrative: | + The buyer reads the denial findings, shrinks the budget to $8K (within the $10K + plan), and retries with a fresh idempotency_key. The seller consults governance + again, which now approves. + + steps: + - id: create_media_buy_retry + title: "Retry with $8K — approved" + 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: | + The buy succeeds. Governance approves because $8K fits within the $10K plan + budget. The seller returns a media_buy_id. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "gov-recovery-retry-approved-v1" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 8000 + pricing_option_id: "$context.pricing_option_id" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns a media_buy_id after governance approves" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/invalid_transitions.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/invalid_transitions.yaml new file mode 100644 index 0000000000..18d18f1e0e --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/invalid_transitions.yaml @@ -0,0 +1,284 @@ +id: media_buy_seller/invalid_transitions +version: "1.0.0" +title: "Seller rejects illegal state transitions and unknown references" +category: media_buy_seller +summary: "Validates that the seller returns structured AdCP errors (MEDIA_BUY_NOT_FOUND, PACKAGE_NOT_FOUND, NOT_CANCELLABLE) rather than 500s or undefined behavior when the buyer references missing entities or attempts forbidden state transitions." +track: media_buy +required_tools: + - get_products + - create_media_buy + - update_media_buy + +narrative: | + Error-code coverage is the most common compliance gap: sellers accept malformed input + silently or return generic 500s rather than the specific AdCP codes the spec defines. + This scenario forces three of the most-referenced media_buy error codes with + deterministic input: + + - MEDIA_BUY_NOT_FOUND — buyer references a media_buy_id that does not exist. + - PACKAGE_NOT_FOUND — media_buy_id is valid but package_id inside it is not. + - NOT_CANCELLABLE — buyer cancels the same media buy twice; the second cancel must + be rejected because canceled is terminal. + + All three probes use hard `check: error_code` assertions so a seller that returns + a 500 or swallows the error fails the scenario outright. Recovery hints, correlation + echo, and context preservation are also checked on every error response. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + examples: + - "Any media buy seller" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports create_media_buy and update_media_buy with cancellation via + the canceled field. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: unknown_media_buy + title: "Reference an unknown media_buy_id" + narrative: | + Before any state is set up, call update_media_buy with a fabricated media_buy_id. + The seller must return MEDIA_BUY_NOT_FOUND with a correctable recovery hint — not + a 500 and not a silent success. + + steps: + - id: update_unknown_media_buy + title: "update_media_buy with bogus media_buy_id" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + expect_error: true + negative_path: payload_well_formed + stateful: false + expected: | + Reject with: + - code: MEDIA_BUY_NOT_FOUND + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "does-not-exist-invalid-transitions-v1" + paused: true + idempotency_key: "$generate:uuid_v4#update_unknown_media_buy" + context: + correlation_id: "invalid_transitions--update_unknown_media_buy" + validations: + - check: error_code + value: "MEDIA_BUY_NOT_FOUND" + description: "Error code is MEDIA_BUY_NOT_FOUND" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "invalid_transitions--update_unknown_media_buy" + description: "Context correlation_id returned unchanged" + + - id: setup + title: "Create a real media buy for follow-up probes" + narrative: | + The remaining probes need a real media_buy_id and package_id. Discover a product + and create a plain buy — no targeting tricks, no governance — so we have known-good + IDs for the error cases. + + steps: + - id: get_products_brief + title: "Discover a product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product with pricing. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content. Q3 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + + - id: create_buy + title: "Create a buy for the error probes" + 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: | + Media buy created with media_buy_id and at least one package. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "invalid-transitions-setup-v1" + start_time: "2026-08-01T00:00:00Z" + end_time: "2026-08-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 5000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "invalid_transitions--create_buy" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns media_buy_id" + + - id: unknown_package + title: "Reference an unknown package_id on a real buy" + narrative: | + The media_buy_id is real, but the package_id is fabricated. The seller must + return PACKAGE_NOT_FOUND — distinct from MEDIA_BUY_NOT_FOUND — because the + lookup succeeds at the buy level and only fails at the package lookup. + + steps: + - id: update_unknown_package + title: "update_media_buy with bogus package_id" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with: + - code: PACKAGE_NOT_FOUND + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + idempotency_key: "$generate:uuid_v4#update_unknown_package" + packages: + - package_id: "does-not-exist-package-invalid-transitions-v1" + paused: true + context: + correlation_id: "invalid_transitions--update_unknown_package" + validations: + - check: error_code + value: "PACKAGE_NOT_FOUND" + description: "Error code is PACKAGE_NOT_FOUND" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "invalid_transitions--update_unknown_package" + description: "Context correlation_id returned unchanged" + + - id: double_cancel + title: "Cancel twice — second cancel is not valid" + narrative: | + Cancel the buy (success), then try to cancel the same buy again. canceled is + terminal per the AdCP spec, so the second cancel cannot succeed. The seller + must return NOT_CANCELLABLE — the schema specifically reserves this code for + "media buy cannot be canceled in its current state." + + steps: + - id: first_cancel + title: "Cancel the media buy" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Seller acknowledges the cancellation and transitions the buy to canceled. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + cancellation_reason: "Testing NOT_CANCELLABLE on re-cancel" + idempotency_key: "$generate:uuid_v4#media_buy_seller_invalid_transitions_double_cancel_first_cancel" + context: + correlation_id: "invalid_transitions--first_cancel" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + + - id: second_cancel + title: "Re-cancel the canceled buy" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with: + - code: NOT_CANCELLABLE + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + cancellation_reason: "Deliberate re-cancel to force NOT_CANCELLABLE" + idempotency_key: "$generate:uuid_v4#media_buy_seller_invalid_transitions_double_cancel_second_cancel" + context: + correlation_id: "invalid_transitions--second_cancel" + validations: + - check: error_code + value: "NOT_CANCELLABLE" + description: "Error code is NOT_CANCELLABLE on re-cancel of canceled buy" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "invalid_transitions--second_cancel" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/inventory_list_no_match.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/inventory_list_no_match.yaml new file mode 100644 index 0000000000..8f5f4eb301 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/inventory_list_no_match.yaml @@ -0,0 +1,143 @@ +id: media_buy_seller/inventory_list_no_match +version: "1.0.0" +title: "Seller handles property_list / collection_list references that match zero inventory" +category: media_buy_seller +summary: "Verifies a seller returns a zero-forecast product or a clear error — not a crash — when a buyer references inventory lists that resolve to nothing in the seller's catalog." +track: media_buy +required_tools: + - get_products + - create_media_buy + +narrative: | + Not every buyer list matches every seller's inventory. A buyer may point at a + property_list of domains this seller does not carry, or a collection_list of + programs this seller does not syndicate. The seller must handle that gracefully: + + - Return a product with a zero forecast and explain the mismatch, OR + - Return an informative error on create_media_buy (INSUFFICIENT_INVENTORY or + similar) with findings the buyer can act on. + + What the seller must NOT do: crash, return a misleading non-zero forecast, or + silently drop the targeting and deliver against unintended inventory. Each of + those is a real failure mode we've seen in adapter integrations. + + This scenario exercises the no-match path using the test kit's pre-populated + no_match_properties and no_match_collections lists. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - supports_property_list_targeting + - supports_collection_list_targeting + examples: + - "Sellers that validate inventory against buyer-supplied lists" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The seller must resolve PropertyListReference / CollectionListReference against + its own inventory and report a truthful outcome when nothing matches. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: discover + title: "Discover products" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products the seller carries. + sample_request: + buying_mode: "brief" + brief: "Video inventory on outdoor lifestyle programming. Q3 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "inventory_list_no_match--get_products_brief" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + + - id: no_match_attempt + title: "Create buy with lists that match no inventory" + narrative: | + The buyer references the no-match property list and the no-match collection + list. Neither list resolves to anything in the seller's catalog. The seller + must either (a) accept the buy and surface a zero-delivery expectation, or + (b) reject it with an informative error. Silent success with undefined + delivery behaviour is not acceptable. + + steps: + - id: create_buy_no_match + title: "create_media_buy against empty intersections" + 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: | + One of two acceptable outcomes: + + 1. Buy accepted with zero-forecast reporting — status may be + pending_creatives/pending_start/active, but the seller returns + packages with forecast indicating zero deliverable inventory and + a message explaining the list mismatch. + + 2. Buy rejected with an informative error — typically + INSUFFICIENT_INVENTORY or INVALID_TARGETING — including findings + that identify which list(s) matched nothing. + + What is NOT acceptable: a silently-successful buy with normal forecast + numbers, or a crash / non-AdCP error shape. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "inventory-list-no-match-v1" + start_time: "2026-07-01T00:00:00Z" + end_time: "2026-09-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 10000 + pricing_option_id: "$context.pricing_option_id" + targeting_overlay: + property_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_v1" + collection_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_collections_v1" + + context: + correlation_id: "inventory_list_no_match--create_buy_no_match" + validations: + - check: field_present + path: "context" + description: "Response echoes back the context object (success or error)" + - check: field_value + path: "context.correlation_id" + value: "inventory_list_no_match--create_buy_no_match" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/inventory_list_targeting.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/inventory_list_targeting.yaml new file mode 100644 index 0000000000..93e3e32627 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/inventory_list_targeting.yaml @@ -0,0 +1,266 @@ +id: media_buy_seller/inventory_list_targeting +version: "1.0.0" +title: "Seller honors property_list and collection_list targeting on create and update" +category: media_buy_seller +summary: "Verifies that a seller accepts PropertyListReference and CollectionListReference in package targeting on create_media_buy AND update_media_buy, with parity between both paths." +track: media_buy +required_tools: + - get_products + - create_media_buy + - update_media_buy + +narrative: | + AdCP 3.0 targets inventory through agent-hosted reference lists rather than inline + property arrays. Buyers point a package's targeting at a `PropertyListReference` + and/or `CollectionListReference`; the seller resolves the list against its own + inventory at serve time. + + A common integration regression is create/update parity: a seller accepts list + references on create_media_buy but silently drops them on update_media_buy, so a + buyer who edits a live buy loses their list targeting. This scenario writes both + list types on create, then swaps both on update, and finally reads the buy back + to confirm the updated references are what the seller persisted. + + The buyer references pre-populated inventory lists from the test kit — one for + matching properties and one for matching collections — so the seller's test + engine can resolve them without needing a live governance/inventory agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - supports_property_list_targeting + - supports_collection_list_targeting + examples: + - "Sellers that accept PropertyListReference / CollectionListReference in package targeting" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The seller must accept PropertyListReference and CollectionListReference in + package targeting on both create_media_buy and update_media_buy. List contents + come from test-kits/acme-outdoor.yaml → inventory_targets. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: discover_product + title: "Discover a product that supports list targeting" + steps: + - id: get_products_brief + title: "Discover product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product whose targeting supports property_list and + collection_list references. + + sample_request: + buying_mode: "brief" + brief: "Video inventory on outdoor lifestyle programming. Q3 flight, $30K budget." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "inventory_list_targeting--get_products_brief" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].product_id" + description: "Product has a product_id" + + - id: create_with_both_lists + title: "Create media buy with property_list AND collection_list targeting" + narrative: | + The buyer references the matching-property list and the matching-collection + list from the test kit. The seller accepts both references, creates the buy, + and echoes the resolved targeting back on the package. + + steps: + - id: create_buy_with_lists + title: "create_media_buy with both list references" + 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: | + The seller accepts the list references without error. The response + includes a media_buy_id and the package retains both the property_list + and collection_list targeting fields so subsequent get / update calls + can read them back. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "inventory-list-targeting-create-v1" + start_time: "2026-07-01T00:00:00Z" + end_time: "2026-09-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 20000 + pricing_option_id: "$context.pricing_option_id" + targeting_overlay: + property_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_allowlist_v1" + collection_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_collections_v1" + + context: + correlation_id: "inventory_list_targeting--create_buy_with_lists" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller assigns a media_buy_id" + - check: field_present + path: "context" + description: "Response echoes back the context object" + + - id: verify_create_persisted + title: "Verify list targeting persisted after create" + narrative: | + Read the buy back to confirm the seller stored both list references — not just + echoed them back in the create response. This catches sellers that parse list + references on create but never persist them to their internal package model. + + steps: + - id: get_after_create + title: "get_media_buys after create" + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Return the media buy with packages[0].targeting_overlay.property_list + and .collection_list populated with the list_id values sent on create. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "inventory_list_targeting--get_after_create" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.property_list.list_id" + value: "acme_outdoor_allowlist_v1" + description: "property_list.list_id persisted after create" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.collection_list.list_id" + value: "acme_outdoor_collections_v1" + description: "collection_list.list_id persisted after create" + + - id: update_swap_lists + title: "Swap property_list and collection_list via update_media_buy" + narrative: | + The buyer swaps both list references. This is the create/update parity check: + a seller that accepts list references on create but silently drops them on + update would fail to update the persisted targeting. We exercise update + explicitly to catch that class of regression. + + steps: + - id: update_buy_swap_lists + title: "update_media_buy replaces both list references" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The seller acknowledges the updated targeting. Both property_list and + collection_list are replaced (not merged) with the new list_id values. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + packages: + - package_id: "$context.package_id" + targeting_overlay: + property_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_v1" + collection_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_collections_v1" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_inventory_list_targeting_update_swap_lists_update_buy_swap_lists" + context: + correlation_id: "inventory_list_targeting--update_buy_swap_lists" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + + - id: get_after_update + title: "Confirm swap persisted" + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The persisted package targeting reflects the new list_id values. This is + the create/update parity guarantee: edits through update_media_buy land + in persistent state just like fields on create_media_buy. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "inventory_list_targeting--get_after_update" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.property_list.list_id" + value: "acme_outdoor_no_match_v1" + description: "property_list.list_id updated after update_media_buy" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.collection_list.list_id" + value: "acme_outdoor_no_match_collections_v1" + description: "collection_list.list_id updated after update_media_buy" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/measurement_terms_rejected.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/measurement_terms_rejected.yaml new file mode 100644 index 0000000000..3ac9452152 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/measurement_terms_rejected.yaml @@ -0,0 +1,195 @@ +id: media_buy_seller/measurement_terms_rejected +version: "1.0.0" +title: "Seller rejects unworkable measurement_terms" +category: media_buy_seller +summary: "Buyer proposes measurement_terms the seller will not accept; seller returns TERMS_REJECTED; buyer retries with seller-compatible terms." +track: media_buy +required_tools: + - get_products + - create_media_buy + +narrative: | + measurement_terms negotiation is a round-trip. The buyer proposes a measurement vendor, + window, and variance tolerance on each package; the seller either accepts (returning + measurement_terms echoed in the response) or rejects with TERMS_REJECTED and a message + explaining what is unacceptable. The buyer corrects the terms and retries the same + create_media_buy idempotency_key with an adjusted payload. + + This scenario sends an intentionally aggressive first proposal (max_variance_percent: 0 + with a window the seller does not guarantee), verifies the seller rejects with + TERMS_REJECTED, then retries with a relaxed proposal that falls inside the seller's + stated tolerance and expects a successful create_media_buy. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - measurement_terms + examples: + - "Broadcast sellers" + - "CTV sellers with C3/C7 guarantees" + - "Any seller that negotiates measurement terms" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports measurement_terms on create_media_buy packages. Buyer knows the + seller's declared measurement capabilities from get_adcp_capabilities. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: discover_products + title: "Discover products" + steps: + - id: get_products_brief + title: "Find a product that supports measurement_terms" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products with pricing_options. At least one product should advertise + measurement support in the response. + sample_request: + buying_mode: "brief" + brief: "Premium video inventory with measurement guarantees. Q2 flight, $50K." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].product_id" + description: "At least one product returned" + + - id: reject_terms + title: "Seller rejects unworkable terms" + narrative: | + The buyer proposes max_variance_percent: 0 — a tolerance almost no seller can + honor against third-party measurement. The seller must reject with TERMS_REJECTED + and indicate what would be acceptable. + + steps: + - id: create_media_buy_aggressive_terms + title: "Propose aggressive measurement_terms" + 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 + expect_error: true + negative_path: payload_well_formed + stateful: false + expected: | + Reject with: + - code: TERMS_REJECTED + - recovery: correctable + - message indicating which terms are unworkable (vendor, window, or variance) + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "measurement-terms-probe-aggressive-v1" + start_time: "2026-05-01T00:00:00Z" + end_time: "2026-05-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 25000 + pricing_option_id: "$context.pricing_option_id" + measurement_terms: + billing_measurement: + vendor: + domain: "videoamp.example" + measurement_window: "c30" + max_variance_percent: 0 + makegood_policy: + available_remedies: ["credit"] + + context: + correlation_id: "measurement_terms_rejected--aggressive" + validations: + - check: error_code + value: "TERMS_REJECTED" + description: "Error code is TERMS_REJECTED" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "measurement_terms_rejected--aggressive" + description: "Context correlation_id returned unchanged" + + - id: accept_terms + title: "Buyer retries with acceptable terms" + narrative: | + The buyer relaxes measurement_terms to match the seller's stated capability + (c7 window, 10% variance, makegood remedies) and retries. The seller accepts and + returns the confirmed terms echoed in the response. + + steps: + - id: create_media_buy_relaxed_terms + title: "Propose seller-compatible measurement_terms" + 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: | + The buy succeeds. The response echoes the accepted measurement_terms (possibly + normalized by the seller). + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "measurement-terms-probe-relaxed-v1" + start_time: "2026-05-01T00:00:00Z" + end_time: "2026-05-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 25000 + pricing_option_id: "$context.pricing_option_id" + measurement_terms: + billing_measurement: + vendor: + domain: "videoamp.example" + measurement_window: "c7" + max_variance_percent: 10 + makegood_policy: + available_remedies: ["additional_delivery", "credit"] + + context: + correlation_id: "measurement_terms_rejected--relaxed" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns a media_buy_id after accepting terms" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "measurement_terms_rejected--relaxed" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/pending_creatives_to_start.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/pending_creatives_to_start.yaml new file mode 100644 index 0000000000..9bf1a639a0 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/pending_creatives_to_start.yaml @@ -0,0 +1,250 @@ +id: media_buy_seller/pending_creatives_to_start +version: "1.0.0" +title: "Creative sync unblocks pending_creatives → pending_start" +category: media_buy_seller +summary: "Verifies that a media buy created without creatives sits in pending_creatives until sync_creatives completes, then transitions to pending_start." +track: media_buy +required_tools: + - get_products + - create_media_buy + - sync_creatives + - get_media_buys + +narrative: | + When a buyer creates a media buy without creative_assignments, the seller cannot start + delivery until creatives are supplied. The seller must report status: pending_creatives + and, after sync_creatives attaches the required assets, transition the buy to + pending_start (awaiting flight start) or active (if already in flight). + + This scenario walks the transition end-to-end: create the buy, confirm pending_creatives, + sync a creative, and confirm the status advances to pending_start. The transition + proves that the creative pipeline and the buy state machine are wired together — a + common integration gap when creative and media buy systems live on different backends. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_creatives + examples: + - "Any seller that requires creatives before starting delivery" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports create_media_buy without creative_assignments and honors the + pending_creatives → pending_start transition when creatives are synced. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Discover products and formats" + steps: + - id: get_products_brief + title: "Discover a product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product with format_ids and pricing options. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content. Q3 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + - path: "products[0].format_ids[0]" + key: "format_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Product declares a format_id for creative sync" + + - id: create_without_creatives + title: "Create media buy without creative_assignments" + narrative: | + The buyer intentionally omits creative_assignments. The seller must accept the + buy, persist it, and return status: pending_creatives with valid_actions + including sync_creatives. + + steps: + - id: create_buy_no_creatives + title: "Create buy in pending_creatives" + 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: | + Media buy created with: + - media_buy_id assigned + - status: pending_creatives + - valid_actions including sync_creatives + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "pending-creatives-transition-v1" + start_time: "2026-08-01T00:00:00Z" + end_time: "2026-08-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 10000 + pricing_option_id: "$context.pricing_option_id" + + context: + correlation_id: "pending_creatives_to_start--create_buy_no_creatives" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller assigns a media_buy_id" + - check: field_value + path: "status" + value: "pending_creatives" + description: "Status is pending_creatives because no creatives supplied" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pending_creatives_to_start--create_buy_no_creatives" + description: "Context correlation_id returned unchanged" + + - id: supply_creatives + title: "Supply creatives and assign to the package" + narrative: | + The buyer syncs a creative with the format the product requires, then updates the + media buy with creative_assignments so the seller can attach the asset to the + package and advance state. + + steps: + - id: sync_creative + title: "Sync the creative asset" + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + The seller ingests the creative and returns status active (or approved). + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "acme-outdoor-display-q3" + name: "Acme Outdoor Q3 display" + format_id: "$context.format_id" + assets: + image: + asset_type: "image" + url: "https://creative.acmeoutdoor.example/q3/display-300x250.jpg" + width: 300 + height: 250 + mime_type: "image/jpeg" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_pending_creatives_to_start_supply_creatives_sync_creative" + context: + correlation_id: "pending_creatives_to_start--sync_creative" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + + - id: assign_creative_to_package + title: "Assign the creative to the package" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The seller acknowledges the assignment and advances the buy state. The response + status should be pending_start (or active if the flight has started). + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + packages: + - package_id: "$context.package_id" + creative_assignments: + - creative_id: "acme-outdoor-display-q3" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_pending_creatives_to_start_supply_creatives_assign_creative_to_package" + context: + correlation_id: "pending_creatives_to_start--assign_creative_to_package" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + - check: field_value + path: "status" + allowed_values: ["pending_start", "active"] + description: "Status advances out of pending_creatives once creatives attached" + + - id: verify_transition + title: "Verify the status transition" + narrative: | + Independently of the update_media_buy response, call get_media_buys to confirm the + seller's stored state has advanced. This protects against sellers that return the + updated status in response but do not persist it. + + steps: + - id: get_media_buy_after_sync + title: "Confirm pending_start after creative sync" + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The media buy's persisted status is pending_start (or active). valid_actions + no longer includes sync_creatives as a required next step. + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "pending_creatives_to_start--get_media_buy_after_sync" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_value + path: "media_buys[0].status" + allowed_values: ["pending_start", "active"] + description: "Persisted status is past pending_creatives" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/proposal_finalize.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/proposal_finalize.yaml new file mode 100644 index 0000000000..cd30feb805 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/proposal_finalize.yaml @@ -0,0 +1,243 @@ +id: media_buy_seller/proposal_finalize +version: "1.0.0" +title: "Seller handles proposal refinement and finalize" +category: media_buy_seller +summary: "Verifies the full proposal lifecycle: brief with proposals, refine a proposal, finalize to committed, and accept via create_media_buy." +track: media_buy +required_tools: + - get_products + - create_media_buy + +narrative: | + Proposals are curated media plans that the seller generates alongside products. The buyer + reviews proposals, refines them (adjust budget splits, swap products, add constraints), + and then finalizes a proposal to get firm pricing and an inventory hold. Once committed, + the buyer accepts the proposal via create_media_buy. + + The proposal lifecycle is: draft (indicative pricing) -> refine -> finalize (firm pricing, + inventory hold) -> create_media_buy (accept and go live). + + This scenario exercises the complete proposal flow including the finalize action, which + transitions a proposal from draft to committed status with an expires_at hold window. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - generates_proposals + examples: + - "Full-service publisher with proposal engine" + - "Retail media network with curated packages" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials. The seller must support + proposal generation (return proposals alongside products in get_products responses). + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Account setup" + steps: + - id: sync_accounts + title: "Establish account" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_proposal_finalize_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: brief_with_proposals + title: "Brief with proposals" + narrative: | + Send a brief and receive proposals alongside products. Proposals are curated + media plans with budget allocations the buyer can review and refine. + + steps: + - id: get_products_brief + title: "Send a brief and receive proposals" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products and proposals matching the brief: + - products: individual products with pricing and forecasts + - proposals: curated media plans with proposal_id, budget_allocations, rationale + + sample_request: + buying_mode: "brief" + brief: "Premium video and display across outdoor lifestyle and sports. Q2 flight, $50K budget. Adults 25-54, US and Canada." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + - check: field_present + path: "proposals" + description: "Response contains proposals" + - check: field_present + path: "proposals[0].proposal_id" + description: "Proposals have IDs" + + - id: refine_proposal + title: "Refine the proposal" + narrative: | + The buyer reviews the proposals and wants to adjust one. They call get_products in + refine mode targeting a specific proposal_id with changes. The seller applies the + refinements and returns the updated proposal. + + steps: + - id: get_products_refine + title: "Refine a specific proposal" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: true + expected: | + Return the refined proposal: + - proposals: updated proposal reflecting the requested changes + - refinement_applied: how each refinement was handled + - Updated budget allocations, product selections, and forecasts + + sample_request: + buying_mode: "refine" + refine: + - scope: "proposal" + proposal_id: "balanced_reach_q2" + ask: "Shift 60% of budget to CTV. Drop the display product and redistribute that budget to video." + - scope: "request" + ask: "All products must support frequency capping at 3 per day." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "proposals" + description: "Response contains updated proposals" + + - id: finalize_proposal + title: "Finalize the proposal" + narrative: | + The buyer is satisfied with the refined proposal and requests finalization. This + triggers the transition from draft (indicative pricing) to committed (firm pricing + with inventory hold). The seller may need time to process this — the buyer should + not set a time_budget to signal willingness to wait. + + After finalization, the proposal has firm pricing and an expires_at timestamp. + The buyer must create the media buy before the hold expires. + + steps: + - id: get_products_finalize + title: "Finalize the proposal" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: true + expected: | + Return the finalized proposal with committed status: + - proposals[0].proposal_status: committed + - proposals[0].expires_at: timestamp for the inventory hold window + - Firm pricing (not indicative) + - The proposal is ready to accept via create_media_buy + + sample_request: + buying_mode: "refine" + refine: + - scope: "proposal" + proposal_id: "balanced_reach_q2" + action: "finalize" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "proposals" + description: "Response contains the finalized proposal" + + - id: accept_proposal + title: "Accept the committed proposal" + narrative: | + The buyer accepts the committed proposal by creating a media buy with the + proposal_id. The seller converts the proposal's product selections and budget + allocations into active packages. + + steps: + - id: create_media_buy + title: "Create media buy from proposal" + 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: | + Convert the committed proposal into an active media buy: + - media_buy_id: the seller's identifier + - status: active + - confirmed_at: timestamp + - packages: line items derived from the proposal's budget allocations + - proposal_id: echoed back to confirm which proposal was accepted + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + proposal_id: "balanced_reach_q2" + total_budget: + amount: 50000 + currency: "USD" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_proposal_finalize_accept_proposal_create_media_buy" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" diff --git a/dist/compliance/3.0.1/domains/media-buy/scenarios/refine_products.yaml b/dist/compliance/3.0.1/domains/media-buy/scenarios/refine_products.yaml new file mode 100644 index 0000000000..f47c311c2c --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/scenarios/refine_products.yaml @@ -0,0 +1,148 @@ +id: media_buy_seller/refine_products +version: "1.0.0" +title: "Seller handles product refinement" +category: media_buy_seller +summary: "Verifies that a media buy seller supports buying_mode: refine with product-level and request-level changes." +track: media_buy +required_tools: + - get_products + +narrative: | + After a buyer receives products from a brief, they refine the results without starting + over. The buyer calls get_products with buying_mode: refine and a refine array describing + changes at the request level ("only guaranteed") or product level ("increase this product's + budget"). + + The seller must apply each refinement, return updated products, and include + refinement_applied showing how each request was handled. This is the standard negotiation + loop — brief, review, refine, repeat until satisfied. + + Every media buy seller that supports negotiation (not just wholesale) must handle + product-level refinement. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + examples: + - "Any media buy seller that accepts briefs" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Account setup" + steps: + - id: sync_accounts + title: "Establish account" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_refine_products_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: brief + title: "Initial brief" + narrative: | + Send a brief to get the initial product set that the buyer will refine. + + steps: + - id: get_products_brief + title: "Send a brief" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. Each product should include product_id, + delivery_type, pricing_models, and forecast. + + sample_request: + buying_mode: "brief" + brief: "Premium video and display on sports and outdoor lifestyle. Q2 flight, $50K budget. Adults 25-54, US and Canada." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + - check: field_present + path: "products[0].product_id" + description: "Products have IDs" + + - id: refine + title: "Refine products" + narrative: | + The buyer has reviewed the initial products and wants to narrow down. They call + get_products with buying_mode: refine and a refine array with request-level and + product-level changes. The seller applies each refinement and returns the updated + product set. + + steps: + - id: get_products_refine + title: "Refine with request-level and product-level changes" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: true + expected: | + Return updated products reflecting the refinements: + - Apply each refinement to the relevant scope (request or product) + - Include refinement_applied showing how each request was handled + - Preserve products that weren't targeted by refinements + - Update pricing and forecasts to reflect the changes + + sample_request: + buying_mode: "refine" + refine: + - scope: "request" + ask: "Only guaranteed packages. Must include completion rate SLA above 80%." + - scope: "product" + product_id: "sports_preroll_q2" + ask: "Increase budget allocation to $30K" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains updated products" diff --git a/dist/compliance/3.0.1/domains/media-buy/state-machine.yaml b/dist/compliance/3.0.1/domains/media-buy/state-machine.yaml new file mode 100644 index 0000000000..ddc67b9a33 --- /dev/null +++ b/dist/compliance/3.0.1/domains/media-buy/state-machine.yaml @@ -0,0 +1,442 @@ +id: media_buy_state_machine +version: "1.0.0" +title: "Media buy state machine lifecycle" +category: media_buy_state_machine +summary: "Validates media buy state transitions: create, pause, resume, cancel, and terminal state enforcement." +track: media_buy +required_tools: + - create_media_buy + - update_media_buy + +narrative: | + A media buy has a well-defined state machine: pending_creatives, pending_start, active, + paused, completed, rejected, canceled. Transitions between states must follow the spec — you cannot + resume a canceled buy or pause a completed one. + + This storyboard creates a media buy, walks it through pause/resume/cancel transitions, then + verifies that the agent rejects updates to a buy in a terminal state (canceled or completed). + It also tests package-level pause/resume independent of the media buy status. + + The state machine is the backbone of media buy reliability. If an agent allows invalid + transitions, buyers cannot trust the status field and automation breaks down. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + examples: + - "Any AdCP seller with media buy support" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity for account setup. The test creates a media buy + using discovered products, then exercises state transitions. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "media_buy_state_machine--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: "media_buy_state_machine--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: setup + title: "Create a media buy" + narrative: | + Discover products and create a media buy to use for state transition testing. + The media buy ID is captured and passed to subsequent phases. + + steps: + - id: discover_products + title: "Discover products for media buy" + narrative: | + Send a brief to get available products with pricing options. The first product + with a pricing option will be used to create the test media buy. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products with: + - product_id + - pricing_options with pricing_option_id + + sample_request: + buying_mode: "brief" + brief: "Display advertising products for state machine testing" + brand: + domain: "acmeoutdoor.example" + + context: + correlation_id: "media_buy_state_machine--discover_products" + context_outputs: + - name: product_id + path: 'products[0].product_id' + - name: pricing_option_id + path: 'products[0].pricing_options[0].pricing_option_id' + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].product_id" + description: "At least one product with a product_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--discover_products" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: create_buy + title: "Create the test media buy" + narrative: | + Create a media buy using the discovered product. This buy will be used for + all subsequent state transition tests. + 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 an active media buy with: + - media_buy_id + - status: active + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + start_time: "2026-05-01T00:00:00Z" + end_time: "2026-05-31T23:59:59Z" + packages: + - product_id: "test-product" + budget: 10000 + pricing_option_id: "test-pricing" + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_setup_create_buy" + context: + correlation_id: "media_buy_state_machine--create_buy" + context_outputs: + - name: media_buy_id + path: 'media_buy_id' + - name: package_id + path: 'packages[0].package_id' + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Response includes a media_buy_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--create_buy" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "packages[0].package_id" + description: "Seller assigns package_id — must be echoed in update_media_buy" + - id: state_transitions + title: "Valid state transitions" + narrative: | + Exercise the valid state transitions: pause, resume, and cancel. Each transition + must update the status field correctly. + + steps: + - id: pause_buy + title: "Pause the media buy" + narrative: | + Pause the active media buy. The status should transition to paused. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Media buy status transitions to paused. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: true + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_state_transitions_pause_buy" + context: + correlation_id: "media_buy_state_machine--pause_buy" + validations: + - check: field_present + path: "status" + description: "Response includes updated status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--pause_buy" + description: "Context correlation_id returned unchanged" + - id: resume_buy + title: "Resume the media buy" + narrative: | + Resume the paused media buy. The status should transition back to active. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Media buy status transitions back to active. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: false + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_state_transitions_resume_buy" + context: + correlation_id: "media_buy_state_machine--resume_buy" + validations: + - check: field_present + path: "status" + description: "Response includes updated status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--resume_buy" + description: "Context correlation_id returned unchanged" + - id: cancel_buy + title: "Cancel the media buy" + narrative: | + Cancel the media buy. This is a terminal transition — the buy cannot be + resumed or modified after cancellation. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Media buy status transitions to canceled. This is terminal. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_state_transitions_cancel_buy" + context: + correlation_id: "media_buy_state_machine--cancel_buy" + validations: + - check: field_present + path: "status" + description: "Response includes canceled status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--cancel_buy" + description: "Context correlation_id returned unchanged" + - id: terminal_enforcement + title: "Terminal state enforcement" + narrative: | + Verify that the agent rejects state transitions on a canceled media buy. Pausing + or resuming a terminal buy returns INVALID_STATE (generic terminal-state rule). + Re-canceling a terminal buy returns NOT_CANCELLABLE — the cancellation-specific + code takes precedence over the generic INVALID_STATE when the attempted + transition is itself a cancellation. Non-cancellation illegal transitions into + or out of terminal states still return INVALID_STATE. + + steps: + - id: pause_canceled_buy + title: "Reject pause on canceled buy" + narrative: | + Attempt to pause a canceled media buy. The agent must reject this with + INVALID_STATE. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: terminal_state_enforcement + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with INVALID_STATE — cannot pause a canceled buy. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: true + idempotency_key: "$generate:uuid_v4#pause_canceled_buy" + + context: + correlation_id: "media_buy_state_machine--pause_canceled_buy" + validations: + - check: error_code + allowed_values: ["INVALID_STATE"] + description: "Invalid transition rejected with INVALID_STATE" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--pause_canceled_buy" + description: "Context correlation_id returned unchanged" + - id: resume_canceled_buy + title: "Reject resume on canceled buy" + narrative: | + Attempt to resume a canceled media buy. The agent must reject this with + INVALID_STATE. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: terminal_state_enforcement + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with INVALID_STATE — cannot resume a canceled buy. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: false + idempotency_key: "$generate:uuid_v4#resume_canceled_buy" + + context: + correlation_id: "media_buy_state_machine--resume_canceled_buy" + validations: + - check: error_code + allowed_values: ["INVALID_STATE"] + description: "Invalid transition rejected with INVALID_STATE" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--resume_canceled_buy" + description: "Context correlation_id returned unchanged" + - id: recancel_buy + title: "Reject re-cancel of canceled buy with NOT_CANCELLABLE" + narrative: | + Attempt to cancel an already-canceled media buy. The agent MUST reject + this with NOT_CANCELLABLE. The cancellation-specific code takes + precedence over the generic terminal-state INVALID_STATE when the + terminal update is itself a cancellation attempt — see §128/§129 of + the media-buy specification and media_buy_seller/invalid_transitions + for the canonical vector. Idempotent acceptance is NOT conformant for + this case. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: terminal_state_enforcement + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with: + - code: NOT_CANCELLABLE + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_terminal_enforcement_recancel_buy" + context: + correlation_id: "media_buy_state_machine--recancel_buy" + validations: + - check: error_code + value: "NOT_CANCELLABLE" + description: "Error code is NOT_CANCELLABLE on re-cancel of canceled buy" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--recancel_buy" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/signals/index.yaml b/dist/compliance/3.0.1/domains/signals/index.yaml new file mode 100644 index 0000000000..8301cc20f2 --- /dev/null +++ b/dist/compliance/3.0.1/domains/signals/index.yaml @@ -0,0 +1,266 @@ +id: signals_baseline +version: "1.1.0" +title: "Signals baseline" +protocol: signals +category: signals_baseline +summary: "Baseline domain storyboard — every signals agent must discover signals and return an activation, regardless of whether they are owned or marketplace." +track: signals +required_tools: + - get_signals + - activate_signal + +narrative: | + Signals domain agents expose audience and contextual signals to buyers and + activate them on downstream destinations (DSPs, sales agents, clean rooms). + Every signals agent — whether a first-party owned platform or a third-party + marketplace — must support the same three-call flow at the protocol level: + + 1. Declare the signals protocol in get_adcp_capabilities. + 2. Respond to get_signals with a schema-valid signals array. + 3. Respond to activate_signal with a schema-valid deployments array. + + This baseline tests those three calls and nothing beyond them. Specialism + storyboards (signal-owned, signal-marketplace) exercise the richer flows + specific to each model — pricing option selection, source/provenance + discriminators, agent-destination vs. platform-destination activation, + and deactivation for compliance. + + Agents declaring supported_protocols: ["signals"] MUST pass this baseline + even before claiming a specialism. Declaring signals without exposing + get_signals or activate_signal fails the baseline with missing_tool. + +agent: + interaction_model: owned_signals + capabilities: [] + examples: + - "Any signals agent (owned or marketplace)" + - "Retailer CDPs" + - "Publisher contextual platforms" + - "Data provider marketplaces" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The buyer has a campaign brief with broad targeting objectives. The test + kit provides a sample brand (Nova Motors) with a signal description that + any signals agent should be able to interpret. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent serves signals + before issuing discovery or activation calls. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares `signals` in supported_protocols. + Without this claim the buyer will not send get_signals or + activate_signal. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring `signals` in supported_protocols. + + sample_request: + context: + correlation_id: "signals_baseline--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: "signals_baseline--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: discovery + title: "Signal discovery" + narrative: | + The buyer calls get_signals with a natural language signal_spec. Every + signals agent — owned or marketplace — must return a schema-valid + response. The buyer uses the returned signal_agent_segment_id values + to drive activation in the next phase. + + steps: + - id: search_signals + title: "Discover signals matching a spec" + narrative: | + The buyer describes a target audience in natural language. The agent + returns a list of signals from its catalog that match. Each signal + must include the fields the buyer needs to proceed to activation. + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow + stateful: false + expected: | + Return a signals array with at least one entry. Each signal must + carry a signal_agent_segment_id that the buyer can pass to + activate_signal, along with pricing_options and a signal_id that + includes a source discriminator. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_spec: "Adults interested in electric vehicles" + context: + correlation_id: "signals_baseline--search_signals" + context_outputs: + - name: signal_agent_segment_id + path: "signals[0].signal_agent_segment_id" + - name: pricing_option_id + path: "signals[0].pricing_options[0].pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-signals-response.json schema" + - check: field_present + path: "signals[0].signal_agent_segment_id" + description: "First signal carries a signal_agent_segment_id" + - check: field_present + path: "signals[0].signal_id.source" + description: "Signal ID carries a source discriminator (agent_native or data_provider)" + - check: field_present + path: "signals[0].pricing_options" + description: "Signal carries pricing options the buyer can select" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signals_baseline--search_signals" + description: "Context correlation_id returned unchanged" + + - id: activation + title: "Signal activation" + narrative: | + The buyer activates one of the signals returned from discovery against + a destination. The protocol baseline tests only that the agent accepts + the call and returns a schema-valid deployments array — specialisms + exercise owned vs. marketplace activation patterns in depth. + + steps: + - id: activate_on_agent + title: "Activate on a sales agent destination" + narrative: | + Using the signal_agent_segment_id and pricing_option_id captured + from the previous step, the buyer activates the signal on a sales + agent destination. Every signals agent MUST accept `type: agent` + per the signals specification — the SA records the activation + internally and applies targeting in subsequent media-buy calls. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployments array with at least one entry carrying a + `type` discriminator and, for live deployments, an + `activation_key`. Agents MAY return an async deployment with + `is_live: false` and `estimated_activation_duration_minutes`. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "$context.signal_agent_segment_id" + pricing_option_id: "$context.pricing_option_id" + destinations: + - type: "agent" + agent_url: "https://wonderstruck.salesagents.example" + idempotency_key: "$generate:uuid_v4#signals_baseline_activate_agent" + + context: + correlation_id: "signals_baseline--activate_on_agent" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].type" + description: "Deployment carries a type discriminator" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signals_baseline--activate_on_agent" + description: "Context correlation_id returned unchanged" + + - id: activate_on_platform + title: "Activate on a platform destination" + narrative: | + The buyer re-activates the same signal against a platform + destination (a DSP). Signal agents MUST accept `type: platform` + per the signals specification — the agent pushes the segment to + the platform and returns a deployment record. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployments array with at least one entry whose + `type: "platform"` and, for live deployments, an + `activation_key` with `type: "segment_id"`. Async deployments + MAY report `is_live: false` with + `estimated_activation_duration_minutes`. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "$context.signal_agent_segment_id" + pricing_option_id: "$context.pricing_option_id" + destinations: + - type: "platform" + platform: "the-trade-desk" + account: "agency-123-ttd" + idempotency_key: "$generate:uuid_v4#signals_baseline_activate_platform" + + context: + correlation_id: "signals_baseline--activate_on_platform" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].type" + description: "Deployment carries a type discriminator" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signals_baseline--activate_on_platform" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/domains/sponsored-intelligence/index.yaml b/dist/compliance/3.0.1/domains/sponsored-intelligence/index.yaml new file mode 100644 index 0000000000..9846ad7bcc --- /dev/null +++ b/dist/compliance/3.0.1/domains/sponsored-intelligence/index.yaml @@ -0,0 +1,256 @@ +id: si_baseline +version: "1.0.0" +title: "Sponsored intelligence baseline" +protocol: sponsored-intelligence +category: si_baseline +summary: "Baseline domain storyboard — every SI agent must discover offerings, initiate a session, exchange messages, and terminate cleanly." +track: si +required_tools: + - si_initiate_session + +narrative: | + You run an AI platform that supports sponsored intelligence — conversational ad experiences + embedded in AI-powered search, chat, or assistant products. A buyer agent connects to + discover what SI offerings are available, initiate a session, send messages within the + conversation, and cleanly terminate when done. + + Sponsored intelligence is fundamentally different from display or video advertising. The + ad experience is conversational — the user asks a question, the AI responds, and sponsored + content is woven into the response in a way that is transparent and relevant. + + This storyboard covers the SI session lifecycle from the buyer's perspective: discovering + what the platform offers, starting a conversation, exchanging messages, and ending the + session. + +agent: + interaction_model: si_platform + capabilities: + - sponsored_intelligence + examples: + - "Perplexity" + - "ChatGPT Search" + - "Arc Browser" + - "AI assistants with ad support" + +caller: + role: buyer_agent + example: "Nova Motors (advertiser)" + +prerequisites: + description: | + The caller needs brand context and campaign parameters for SI. The test kit provides + a sample brand (Nova Motors) with signal definitions suitable for conversational + ad experiences. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports sponsored intelligence before initiating sessions. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring sponsored_intelligence in supported_protocols, confirming the agent supports conversational ad experiences. + sample_request: + context: + correlation_id: "si_session--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: "si_session--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: offering_discovery + title: "Discover SI offerings" + narrative: | + Before initiating any session, the buyer discovers what sponsored intelligence + offerings the platform has available. This determines what kinds of conversational + experiences can be sponsored and at what pricing. + + steps: + - id: si_get_offering + title: "Get available SI offerings" + narrative: | + The buyer calls si_get_offering to learn what conversational ad experiences + the platform supports. The response describes available offerings with pricing, + targeting options, and format specifications. + task: si_get_offering + schema_ref: "sponsored-intelligence/si-get-offering-request.json" + response_schema_ref: "sponsored-intelligence/si-get-offering-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_get_offering" + comply_scenario: si_availability + stateful: false + expected: | + Return available SI offerings: + - Offering descriptions with pricing + - Supported conversation types + - Targeting and context options + - Format specifications for sponsored content + + sample_request: + offering_id: "novamotors_conversational_v1" + context: + correlation_id: "si_session--si_get_offering" + + context_outputs: + - name: offering_id + path: 'offering_id' + validations: + - check: response_schema + description: "Response matches si-get-offering-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_get_offering" + description: "Context correlation_id returned unchanged" + - id: session_lifecycle + title: "Session lifecycle" + narrative: | + The buyer initiates a session, exchanges messages within it, and terminates + cleanly. Each session represents a single conversational ad experience — the + buyer provides context and the platform weaves sponsored content into the + conversation. + + steps: + - id: si_initiate_session + title: "Start a conversation session" + narrative: | + The buyer initiates a new SI session with campaign context. The platform + creates a session and returns a session ID that the buyer uses for subsequent + messages. + task: si_initiate_session + schema_ref: "sponsored-intelligence/si-initiate-session-request.json" + response_schema_ref: "sponsored-intelligence/si-initiate-session-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_initiate_session" + comply_scenario: si_session_lifecycle + stateful: true + expected: | + Return a new session: + - session_id: platform-assigned session identifier + - status: active + - Initial context acknowledgment + - Available interaction modes + + sample_request: + intent: "User is researching electric vehicles for long road trips and wants to talk to Nova Motors" + identity: + consent_granted: true + consent_timestamp: "2026-04-22T14:00:00Z" + user: + locale: "en-US" + + idempotency_key: "$generate:uuid_v4#si_baseline_session_lifecycle_si_initiate_session" + context: + correlation_id: "si_session--si_initiate_session" + context_outputs: + - name: session_id + path: 'session_id' + validations: + - check: response_schema + description: "Response matches si-initiate-session-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_initiate_session" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "session_id" + description: "Platform assigns session_id — must be echoed in si_send_message and si_terminate_session" + - id: si_send_message + title: "Exchange messages" + narrative: | + The buyer sends a message within the active session. The platform processes + the message and returns a response that may include sponsored content woven + into the conversational experience. + task: si_send_message + schema_ref: "sponsored-intelligence/si-send-message-request.json" + response_schema_ref: "sponsored-intelligence/si-send-message-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_send_message" + comply_scenario: si_session_lifecycle + stateful: true + expected: | + Process the message and return a response: + - Message acknowledgment + - Response content (may include sponsored elements) + - Session state (active, waiting, etc.) + + sample_request: + session_id: "$context.session_id" + message: "What are the best electric vehicles for long road trips?" + + idempotency_key: "$generate:uuid_v4#si_baseline_session_lifecycle_si_send_message" + context: + correlation_id: "si_session--si_send_message" + validations: + - check: response_schema + description: "Response matches si-send-message-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_send_message" + description: "Context correlation_id returned unchanged" + - id: si_terminate_session + title: "End the session" + narrative: | + The buyer terminates the SI session. The platform records session metrics + and returns a summary of the conversation including any sponsored content + that was delivered. + task: si_terminate_session + schema_ref: "sponsored-intelligence/si-terminate-session-request.json" + response_schema_ref: "sponsored-intelligence/si-terminate-session-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_terminate_session" + comply_scenario: si_handoff + stateful: true + expected: | + Terminate the session and return a summary: + - session_id: confirms which session was terminated + - status: terminated + - Session metrics (duration, messages exchanged) + - Sponsored content delivery summary + + sample_request: + session_id: "$context.session_id" + reason: "handoff_complete" + + context: + correlation_id: "si_session--si_terminate_session" + validations: + - check: response_schema + description: "Response matches si-terminate-session-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_terminate_session" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/index.json b/dist/compliance/3.0.1/index.json new file mode 100644 index 0000000000..4a4316a6a4 --- /dev/null +++ b/dist/compliance/3.0.1/index.json @@ -0,0 +1,324 @@ +{ + "adcp_version": "3.0.1", + "generated_at": "2026-04-28T12:32:24.878Z", + "universal": [ + "capability-discovery", + "collection-lists-pagination-integrity", + "content-standards-pagination-integrity", + "deterministic-testing", + "error-compliance", + "fictional-entities", + "get-media-buys-pagination-integrity", + "get-signals-pagination-integrity", + "idempotency", + "pagination-integrity", + "pagination-integrity-creative-formats", + "pagination-integrity-list-accounts", + "property-lists-pagination-integrity", + "runner-output-contract", + "schema-validation", + "security", + "signed-requests", + "storyboard-schema", + "v3-envelope-integrity", + "webhook-emission" + ], + "protocols": [ + { + "id": "brand", + "title": "Brand baseline", + "has_baseline": true, + "path": "protocols/brand/" + }, + { + "id": "creative", + "title": "Creative lifecycle", + "has_baseline": true, + "path": "protocols/creative/" + }, + { + "id": "governance", + "title": "Governance denial and human escalation", + "has_baseline": true, + "path": "protocols/governance/" + }, + { + "id": "media-buy", + "title": "Media buy seller agent", + "has_baseline": true, + "path": "protocols/media-buy/" + }, + { + "id": "signals", + "title": "Signals baseline", + "has_baseline": true, + "path": "protocols/signals/" + }, + { + "id": "sponsored-intelligence", + "title": "Sponsored intelligence baseline", + "has_baseline": true, + "path": "protocols/sponsored-intelligence/" + } + ], + "domains": [ + { + "id": "brand", + "title": "Brand baseline", + "has_baseline": true, + "path": "domains/brand/" + }, + { + "id": "creative", + "title": "Creative lifecycle", + "has_baseline": true, + "path": "domains/creative/" + }, + { + "id": "governance", + "title": "Governance denial and human escalation", + "has_baseline": true, + "path": "domains/governance/" + }, + { + "id": "media-buy", + "title": "Media buy seller agent", + "has_baseline": true, + "path": "domains/media-buy/" + }, + { + "id": "signals", + "title": "Signals baseline", + "has_baseline": true, + "path": "domains/signals/" + }, + { + "id": "sponsored-intelligence", + "title": "Sponsored intelligence baseline", + "has_baseline": true, + "path": "domains/sponsored-intelligence/" + } + ], + "specialisms": [ + { + "id": "audience-sync", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Audience sync", + "status": "stable", + "required_tools": [ + "list_accounts", + "sync_audiences" + ], + "path": "specialisms/audience-sync/" + }, + { + "id": "brand-rights", + "protocol": "brand", + "domain": "brand", + "title": "Brand identity and rights licensing", + "status": "stable", + "required_tools": [ + "get_brand_identity" + ], + "path": "specialisms/brand-rights/" + }, + { + "id": "collection-lists", + "protocol": "governance", + "domain": "governance", + "title": "Collection lists", + "status": "stable", + "required_tools": [ + "create_collection_list" + ], + "path": "specialisms/collection-lists/" + }, + { + "id": "content-standards", + "protocol": "governance", + "domain": "governance", + "title": "Content standards", + "status": "stable", + "required_tools": [ + "list_content_standards" + ], + "path": "specialisms/content-standards/" + }, + { + "id": "creative-ad-server", + "protocol": "creative", + "domain": "creative", + "title": "Creative ad server", + "status": "stable", + "required_tools": [ + "build_creative" + ], + "path": "specialisms/creative-ad-server/" + }, + { + "id": "creative-generative", + "protocol": "creative", + "domain": "creative", + "title": "Generative creative agent", + "status": "stable", + "required_tools": [ + "build_creative" + ], + "path": "specialisms/creative-generative/" + }, + { + "id": "creative-template", + "protocol": "creative", + "domain": "creative", + "title": "Creative template and transformation agent", + "status": "stable", + "required_tools": [ + "build_creative" + ], + "path": "specialisms/creative-template/" + }, + { + "id": "governance-aware-seller", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Governance-aware seller", + "status": "stable", + "required_tools": [ + "sync_governance", + "create_media_buy" + ], + "path": "specialisms/governance-aware-seller/" + }, + { + "id": "governance-delivery-monitor", + "protocol": "governance", + "domain": "governance", + "title": "Campaign governance — delivery monitoring with drift detection", + "status": "stable", + "required_tools": [ + "check_governance" + ], + "path": "specialisms/governance-delivery-monitor/" + }, + { + "id": "governance-spend-authority", + "protocol": "governance", + "domain": "governance", + "title": "Campaign governance — conditional approval", + "status": "stable", + "required_tools": [ + "sync_plans", + "check_governance" + ], + "path": "specialisms/governance-spend-authority/" + }, + { + "id": "property-lists", + "protocol": "governance", + "domain": "governance", + "title": "Property lists", + "status": "stable", + "required_tools": [ + "create_property_list" + ], + "path": "specialisms/property-lists/" + }, + { + "id": "sales-broadcast-tv", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Broadcast linear TV seller agent", + "status": "stable", + "required_tools": [ + "get_products", + "create_media_buy" + ], + "path": "specialisms/sales-broadcast-tv/" + }, + { + "id": "sales-catalog-driven", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Catalog-driven creative and conversion tracking", + "status": "stable", + "required_tools": [ + "get_products", + "create_media_buy" + ], + "path": "specialisms/sales-catalog-driven/" + }, + { + "id": "sales-guaranteed", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Guaranteed media buy with human IO approval", + "status": "stable", + "required_tools": [ + "get_products", + "create_media_buy" + ], + "path": "specialisms/sales-guaranteed/" + }, + { + "id": "sales-non-guaranteed", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Non-guaranteed auction-based media buy", + "status": "stable", + "required_tools": [ + "get_products", + "create_media_buy" + ], + "path": "specialisms/sales-non-guaranteed/" + }, + { + "id": "sales-proposal-mode", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Media buy via proposal acceptance", + "status": "stable", + "required_tools": [ + "get_products", + "create_media_buy" + ], + "path": "specialisms/sales-proposal-mode/" + }, + { + "id": "sales-social", + "protocol": "media-buy", + "domain": "media-buy", + "title": "Social platform", + "status": "stable", + "required_tools": [ + "sync_audiences", + "sync_catalogs", + "sync_creatives", + "sync_event_sources" + ], + "path": "specialisms/sales-social/" + }, + { + "id": "signal-marketplace", + "protocol": "signals", + "domain": "signals", + "title": "Marketplace signal agent", + "status": "stable", + "required_tools": [ + "get_signals" + ], + "path": "specialisms/signal-marketplace/" + }, + { + "id": "signal-owned", + "protocol": "signals", + "domain": "signals", + "title": "Owned signal agent", + "status": "stable", + "required_tools": [ + "get_signals" + ], + "path": "specialisms/signal-owned/" + } + ] +} diff --git a/dist/compliance/3.0.1/protocols/brand/index.yaml b/dist/compliance/3.0.1/protocols/brand/index.yaml new file mode 100644 index 0000000000..9598013fe3 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/brand/index.yaml @@ -0,0 +1,163 @@ +id: brand_baseline +version: "1.0.0" +title: "Brand baseline" +protocol: brand +category: brand_baseline +summary: "Baseline protocol storyboard — every brand agent must declare the brand protocol in capabilities and return a schema-valid brand identity." +track: brand +required_tools: + - get_brand_identity + +narrative: | + Brand protocol agents are the identity layer of AdCP. Their job is to hold + brand identity data (names, logos, colors, fonts, tone) and expose it to + other agents — buyer agents, creative agents, DSPs — that need to render + on-brand creative or verify who a campaign is for. + + The baseline tests the minimum contract that every brand agent honors, + regardless of what additional capabilities (rights licensing, creative + approval) it layers on top: + + 1. Declare `brand` in `supported_protocols` on `get_adcp_capabilities`. + 2. Respond to `get_brand_identity` with a schema-valid identity manifest. + 3. Reject unknown `brand_id` values with a structured error. + + Rights licensing (`get_rights`, `acquire_rights`, `update_rights`, + `creative_approval`) ships experimentally in 3.0 and is covered by the + `brand-rights` specialism storyboard, not this baseline. + +agent: + interaction_model: brand_agent + capabilities: [] + examples: + - "Any brand agent (simple identity host or full rights platform)" + - "Brand-owned agents (Acme Outdoor)" + - "Third-party brand identity platforms" + - "Agency-hosted brand agents" + +caller: + role: buyer_agent + example: "Any buyer, creative agent, or DSP needing brand identity" + +prerequisites: + description: | + The test kit provides a sample brand (Nova Motors) that any brand agent + can serve identity for. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls `get_adcp_capabilities` to confirm the agent declares + the brand protocol before issuing any brand-identity call. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares `brand` in `supported_protocols`. + Without this claim the buyer MUST NOT send `get_brand_identity`. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring `brand` in `supported_protocols`. + + sample_request: + context: + correlation_id: "brand_baseline--get_capabilities" + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" + - check: field_present + path: "supported_protocols" + description: "Response declares supported_protocols" + + - id: brand_identity_retrieval + title: "Brand identity retrieval" + narrative: | + The buyer calls `get_brand_identity` to retrieve the brand's identity + manifest. The minimum contract is a schema-valid response that echoes + the requested `brand_id` and carries at least one name. + + steps: + - id: get_brand_identity + title: "Retrieve brand identity" + narrative: | + The buyer calls `get_brand_identity` with a known `brand_id`. The + response MUST match the brand-identity schema and echo the + requested `brand_id`. Rich fields (logos, colors, fonts, tone, + visual_guidelines) are optional at the baseline level — the + minimum bar is that identity resolution works and is schema-valid. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand-protocol/tasks/get_brand_identity" + stateful: false + expected: | + Return a schema-valid brand identity that echoes the requested + brand_id and includes at least one name. + + sample_request: + brand_id: "nova_motors" + context: + correlation_id: "brand_baseline--get_brand_identity" + context_outputs: + - path: "brand_id" + key: "brand_id" + + validations: + - check: response_schema + description: "Response matches get-brand-identity-response.json schema" + - check: field_present + path: "brand_id" + description: "Response includes brand_id" + - check: field_value + path: "brand_id" + value: "nova_motors" + description: "Returned brand_id echoes the requested brand" + - check: field_present + path: "names" + description: "Response includes brand names" + + - id: unknown_brand_rejection + title: "Unknown brand rejection" + narrative: | + Agents MUST reject unknown `brand_id` values with a structured + AdCP error rather than returning an empty or fabricated manifest. + + steps: + - id: get_brand_identity_unknown + title: "Reject unknown brand ID" + narrative: | + The buyer calls `get_brand_identity` with a `brand_id` the agent + does not serve. The response MUST be a structured error with a + recovery classification — not a success response with empty + fields. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand-protocol/tasks/get_brand_identity" + stateful: false + expected: | + Return an AdCP error response indicating the brand is not known + to this agent. + + sample_request: + brand_id: "brand_that_does_not_exist_12345" + context: + correlation_id: "brand_baseline--get_brand_identity_unknown" + + expect_error: true + negative_path: payload_well_formed + validations: + - check: error_code + allowed_values: + - "brand_not_found" + - "BRAND_NOT_FOUND" + - "NOT_FOUND" + description: "Error code indicates brand-not-found" diff --git a/dist/compliance/3.0.1/protocols/creative/index.yaml b/dist/compliance/3.0.1/protocols/creative/index.yaml new file mode 100644 index 0000000000..cc6efd4f20 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/creative/index.yaml @@ -0,0 +1,412 @@ +id: creative_lifecycle +version: "1.0.0" +title: "Creative lifecycle" +category: creative_lifecycle +summary: "Full creative lifecycle on a stateful platform: sync multiple creatives, list with filtering, build and preview across formats, observe status transitions." +track: creative +required_tools: + - list_creative_formats + +narrative: | + You run a creative platform with a persistent library — an ad server, creative management + platform, or publisher that accepts and stores creative assets. A buyer agent pushes + multiple creatives in different formats, queries the library, builds serving tags, previews + renderings, and monitors creative status as assets move through your review pipeline. + + This storyboard covers the complete creative lifecycle from the buyer's perspective: + uploading assets, browsing the library, building deliverables across formats, and + observing status transitions as creatives move from pending_review through to accepted. + + The individual creative storyboards (template, ad server, sales agent) cover specific + interaction models. This storyboard tests the full lifecycle across multiple creatives + and formats on a single platform. + +agent: + interaction_model: stateful_preloaded + capabilities: + - has_creative_library + - supports_transformation + examples: + - "Innovid" + - "Flashtalking" + - "CM360" + - "Creative management platforms" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs creative assets in multiple formats (display, video, native) and + a brand identity. The test kit provides sample assets at standard ad dimensions. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports creative operations before browsing or building creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring creative in supported_protocols, confirming the agent handles creative operations. + sample_request: + context: + correlation_id: "creative_lifecycle--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: "creative_lifecycle--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: discover_formats + title: "Discover accepted formats" + narrative: | + Before pushing any creatives, the buyer discovers what formats your platform accepts. + This determines which assets to prepare and what dimensions and specs to target. + + steps: + - id: list_formats + title: "List creative formats" + narrative: | + The buyer calls list_creative_formats to discover what your platform accepts. + The response defines format specs: dimensions, asset requirements, mime types, + and any platform-specific constraints. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return all creative formats your platform accepts: + - format_id with your agent_url and unique id + - Asset requirements (dimensions, file sizes, mime types) + - Render dimensions + - At least two formats (e.g., display and video) + + sample_request: + context: + correlation_id: "creative_lifecycle--list_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--list_formats" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - id: sync_multiple + title: "Sync multiple creatives" + narrative: | + The buyer pushes three creatives in different formats to your platform: a display + banner, a video spot, and a native card. Your platform validates each creative + against its format specs and returns per-creative status. + + steps: + - id: sync_creatives + title: "Push three creatives in different formats" + narrative: | + The buyer syncs three creatives simultaneously: a 300x250 display banner, a 30s + video spot, and a native content card. Your platform validates each against its + format specs and returns per-creative action and status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Accept and validate all three creatives: + - Per-creative action: created + - Per-creative status: accepted or pending_review + - Validation results for each creative + - Platform-assigned IDs if applicable + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "display_trail_pro_300x250" + name: "Trail Pro 3000 - Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + width: 300 + height: 250 + mime_type: "image/png" + - creative_id: "video_30s_trail_pro" + name: "Trail Pro 3000 - 30s Video" + format_id: + agent_url: "https://your-platform.example.com" + id: "video_30s" + assets: + video: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + width: 1920 + height: 1080 + duration_ms: 30000 + mime_type: "video/mp4" + - creative_id: "native_trail_pro" + name: "Trail Pro 3000 - Native Card" + format_id: + agent_url: "https://your-platform.example.com" + id: "native_content" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-native.png" + width: 1200 + height: 628 + mime_type: "image/png" + headline: + asset_type: "text" + content: "Trail Pro 3000 — Built for the Summit" + + idempotency_key: "$generate:uuid_v4#creative_lifecycle_sync_multiple_sync_creatives" + context: + correlation_id: "creative_lifecycle--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains per-creative results" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: list_and_filter + title: "List creatives with filtering" + narrative: | + The buyer queries the creative library to see their synced creatives. First a broad + list, then filtered by format. This verifies the library correctly stores and indexes + the pushed creatives. + + steps: + - id: list_all + title: "List all creatives in library" + narrative: | + The buyer calls list_creatives with no filters to see all creatives in the + library for their account. The response includes the three creatives synced + in the previous phase. + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Return creatives in the library: + - creatives array containing the synced items + - Each creative includes: creative_id, name, format_id, status + - At least three creatives from the sync phase + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "creative_lifecycle--list_all" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains creatives array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--list_all" + description: "Context correlation_id returned unchanged" + - id: list_filtered + title: "List creatives filtered by format" + narrative: | + The buyer lists creatives filtered to a specific format (display only). The + response should only include creatives matching that format. + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Return only creatives matching the format filter: + - creatives array filtered to display format + - Should include display_trail_pro_300x250 but not video or native + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + filters: + format_ids: + - agent_url: "https://your-platform.example.com" + id: "display_300x250" + + context: + correlation_id: "creative_lifecycle--list_filtered" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains filtered creatives" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--list_filtered" + description: "Context correlation_id returned unchanged" + - id: build_and_preview + title: "Build and preview across formats" + narrative: | + The buyer builds serving tags and previews renderings for the synced creatives. + This tests multi-format output: a display tag, a VAST tag for video, and a + native rendering preview. + + steps: + - id: preview_display + title: "Preview the display creative" + narrative: | + The buyer calls preview_creative for the display banner to see how it renders + in the platform's environment before going live. + task: preview_creative + schema_ref: "creative/preview-creative-request.json" + response_schema_ref: "creative/preview-creative-response.json" + doc_ref: "/creative/task-reference/preview_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a preview of the display creative: + - preview_url: rendered preview the buyer can inspect + - render_dimensions: matches the 300x250 format + - status: preview available + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + request_type: "single" + creative_manifest: + creative_id: "display_trail_pro_300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/banner_300x250.jpg" + width: 300 + height: 250 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/trail-pro" + + context: + correlation_id: "creative_lifecycle--preview_display" + validations: + - check: response_schema + description: "Response matches preview-creative-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--preview_display" + description: "Context correlation_id returned unchanged" + - id: build_video_tag + title: "Build a VAST tag for the video creative" + narrative: | + The buyer builds a serving tag for the video creative. The platform produces + a VAST-compatible tag that the buyer can traffic to ad servers. + task: build_creative + schema_ref: "creative/build-creative-request.json" + response_schema_ref: "creative/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a built serving tag for the video creative: + - tag: VAST-compatible serving tag or URL + - format: matches the video format + - creative_id: matches the requested creative + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creative_id: "video_30s_trail_pro" + target_format_id: + agent_url: "https://your-platform.example.com" + id: "vast_30s" + idempotency_key: "$generate:uuid_v4#creative_lifecycle_build_video_tag" + + context: + correlation_id: "creative_lifecycle--build_video_tag" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_lifecycle--build_video_tag" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/governance/index.yaml b/dist/compliance/3.0.1/protocols/governance/index.yaml new file mode 100644 index 0000000000..8839db4847 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/governance/index.yaml @@ -0,0 +1,683 @@ +id: media_buy_governance_escalation +version: "1.0.0" +title: "Governance denial and human escalation" +category: media_buy_governance_escalation +summary: "Buyer's governance agent denies a media buy that exceeds spending authority, escalates to a human who approves with conditions." +track: campaign_governance +required_tools: + - sync_plans + - check_governance + +narrative: | + The buyer's governance agent denies a media buy because it exceeds the agent's spending + authority. The governance check escalates to a human reviewer who approves with conditions. + + This storyboard shows the full governance loop: plan registration with spending authority + limits, product discovery, pre-buy governance check that gets denied, human escalation + that results in conditional approval, media buy creation with the approved governance + context, outcome reporting, and a complete audit trail. + + Governance exists to ensure that automated agents operate within defined boundaries. When + those boundaries are exceeded, the system escalates to humans rather than blocking + entirely. The audit trail provides accountability for every decision in the chain. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform integrated with buyer governance" + - "SSP that respects governance checks" + - "Retail media network with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, and a governance agent + URL. The test kit provides a sample brand (Acme Outdoor) with campaign parameters + and a governance configuration with spending authority limits. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "sports_ctv_q2" + delivery_type: "guaranteed" + channels: ["ctv"] + format_ids: + - id: "video_30s" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "sports_ctv_q2" + pricing_option_id: "cpm_guaranteed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 45.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + plans: + - plan_id: "gov_acme_q2_2027" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 outdoor lifestyle campaign — display and video, Adults 25-54, US" + budget: + total: 50000 + currency: "USD" + reallocation_threshold: 20000 + flight: + start: "2027-04-01T00:00:00Z" + end: "2027-06-30T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "weekly_reporting_over_10k" + enforcement: "must" + policy: "Weekly reporting required for buys over $10K." + - policy_id: "seller_concentration_cap" + enforcement: "must" + policy: "No single-seller concentration above 60% of total budget." + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "media_buy_governance_escalation--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: "media_buy_governance_escalation--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account and governance setup" + narrative: | + The buyer establishes an account and registers their governance agent with the + seller. The governance agent will be called before media buys are confirmed to + validate spending authority, brand safety, and compliance. + + steps: + - id: sync_accounts + title: "Establish account relationship" + narrative: | + The buyer registers their brand and operator with your platform. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with: + - account_id: your platform's identifier + - action: created or updated + - status: active or pending_approval + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_account_setup_sync_accounts" + context: + correlation_id: "media_buy_governance_escalation--sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--sync_accounts" + description: "Context correlation_id returned unchanged" + - id: sync_governance + title: "Register governance agent" + narrative: | + The buyer tells your platform: "Before you confirm any media buy for this + account, call this governance agent to validate it." Your platform stores + the governance agent URL and will call it during create_media_buy. + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agents. Your platform should: + - Store the governance agent URLs for the specified accounts + - Return confirmation that agents were registered + - Use these agents during create_media_buy validation + + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "https://governance.pinnacle-agency.example" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_account_setup_sync_governance" + context: + correlation_id: "media_buy_governance_escalation--sync_governance" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--sync_governance" + description: "Context correlation_id returned unchanged" + - id: register_plan + title: "Register governance plan with spending limits" + narrative: | + The buyer registers a governance plan that defines the agent's spending authority. + The plan sets a per-transaction threshold below the intended media buy amount. + This ensures the governance check will trigger an escalation when the buy exceeds + the agent's authority. + + steps: + - id: sync_plans + title: "Register a governance plan with agent-limited authority" + narrative: | + The buyer's governance agent registers a plan with spending authority limits. + The plan sets a per-transaction threshold of $20K via reallocation_threshold + on the budget. Any media buy above this amount requires human approval. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: campaign_governance + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - budget.reallocation_threshold: spending limit before re-evaluation + - human_review_required: set when the plan mandates escalation + + sample_request: + idempotency_key: "media-buy-governance-escalation-sync-plans-v1" + plans: + - plan_id: "gov_acme_q2_2027" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 outdoor lifestyle campaign — display and video, Adults 25-54, US" + budget: + total: 50000 + currency: "USD" + reallocation_threshold: 20000 + flight: + start: "2027-04-01T00:00:00Z" + end: "2027-06-30T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "weekly_reporting_over_10k" + enforcement: "must" + policy: "Weekly reporting required for buys over $10K." + - policy_id: "seller_concentration_cap" + enforcement: "must" + policy: "No single-seller concentration above 60% of total budget." + + context: + correlation_id: "media_buy_governance_escalation--sync_plans" + context_outputs: + - name: plan_id + path: 'plans[0].plan_id' + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--sync_plans" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "plans[0].plan_id" + description: "Governance agent assigns plan_id — must be echoed in check_governance" + - id: discover_products + title: "Product discovery" + narrative: | + The buyer sends a brief to discover products. The products returned will exceed + the governance agent's spending authority when assembled into a media buy. + + steps: + - id: get_products_brief + title: "Send a brief" + narrative: | + The buyer describes what they want. Your platform returns products with + pricing that, when combined, will exceed the $20K per-transaction threshold + set in the governance plan. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief with pricing that totals above $20K: + - product_id, name, description + - delivery_type: guaranteed or non_guaranteed + - pricing_models with CPM and budget recommendations + - forecast: estimated delivery + + sample_request: + buying_mode: "brief" + brief: "Premium CTV and video on sports publishers. Q2 flight, $50K budget. Adults 25-54, US." + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "media_buy_governance_escalation--get_products_brief" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--get_products_brief" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: governance_check_denied + title: "Governance check — denied" + narrative: | + Before creating the media buy, the buyer's governance agent runs a pre-buy check. + The proposed buy totals $50K, which exceeds the agent's $20K per-transaction + authority. The governance agent denies the buy with a must-severity finding and + returns escalation instructions for human review. + + steps: + - id: check_governance_denied + title: "Pre-buy governance check (denied)" + narrative: | + The buyer calls check_governance with the proposed media buy binding. The + governance agent evaluates the buy against the registered plan and finds that + the total exceeds the agent's spending authority. The check returns denied + with a must-severity finding and instructions for escalating to a human. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_spend_authority/denied + stateful: true + expected: | + Return a denied governance decision: + - decision: denied + - findings: array with at least one must-severity finding + - severity: must + - code: SPENDING_AUTHORITY_EXCEEDED + - message: explains the agent's authority limit and how much the buy exceeds it + - escalation: instructions for human review + - governance_context: token/ID the buyer passes to the next check after escalation + - plan_id: the governance plan that triggered the denial + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + tool: "create_media_buy" + payload: + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_check_governance_denied_payload" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + start_time: "2027-01-01T00:00:00Z" + end_time: "2027-03-31T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + + context: + correlation_id: "media_buy_governance_escalation--check_governance_denied" + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains a governance decision" + - check: field_present + path: "findings" + description: "Response contains findings explaining the denial" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--check_governance_denied" + description: "Context correlation_id returned unchanged" + - id: human_escalation + title: "Human escalation — conditional approval" + narrative: | + The governance denial triggers human escalation. A human reviewer at the agency + reviews the proposed buy, the denial reason, and the governance plan. The human + approves the buy with conditions — for example, requiring weekly reporting. The + buyer calls check_governance again with the human's approval, and this time + the check returns approved with conditions. + + steps: + - id: check_governance_approved + title: "Re-check governance after human approval (approved with conditions)" + narrative: | + After the human reviewer approves the buy, the buyer calls check_governance + again with the governance_context from the prior denial and the human's + approval. The governance agent returns approved with conditions that the + buyer must honor during the campaign. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_spend_authority + stateful: true + expected: | + Return an approved governance decision with conditions: + - decision: approved + - conditions: array of requirements the buyer must honor + - e.g., "Weekly delivery reporting required" + - e.g., "Human review required for any budget increase" + - governance_context: updated token the buyer passes to create_media_buy + - approved_by: identifier of the human who approved + - approved_at: timestamp of approval + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + governance_context: "gov_ctx_acme_q2_escalated" + tool: "create_media_buy" + payload: + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_check_governance_approved_payload" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + start_time: "2027-01-01T00:00:00Z" + end_time: "2027-03-31T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + + context: + correlation_id: "media_buy_governance_escalation--check_governance_approved" + context_outputs: + - name: check_id + path: 'check_id' + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains an approved governance decision" + - check: field_present + path: "conditions" + description: "Approval includes conditions" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--check_governance_approved" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "check_id" + description: "Check ID for audit trail — must be echoed in report_plan_outcome" + - id: create_buy_with_governance + title: "Create media buy with governance context" + narrative: | + The buyer creates the media buy, passing the governance_context from the approved + governance check. The seller's platform verifies the governance approval before + confirming the buy. + + steps: + - id: create_media_buy + title: "Create a media buy with governance approval" + narrative: | + The buyer creates the media buy with the governance_context token from the + approved check. The seller's platform validates the governance approval and + confirms the buy. Without a valid governance_context, the platform would + reject the buy because the governance agent is registered for this account. + 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: | + Confirm the media buy with governance approval: + - media_buy_id: your platform's identifier + - status: active + - confirmed_at: timestamp + - governance_context: echoed back confirming governance was validated + - packages: confirmed line items + - valid_actions: creative sync, get_delivery, etc. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + governance_context: "gov_ctx_acme_q2_approved" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + pricing_option_id: "cpm_guaranteed" + creative_assignments: + - creative_id: "video_30s_trail_pro" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + creative_assignments: + - creative_id: "video_15s_trail_pro" + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_create_buy_with_governance_create_media_buy" + context: + correlation_id: "media_buy_governance_escalation--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: report_outcome + title: "Report governance outcome" + narrative: | + After the media buy is created, the buyer reports the outcome back to the + governance agent. This closes the governance loop by linking the actual media + buy to the governance check that authorized it. + + steps: + - id: report_plan_outcome + title: "Report media buy outcome to governance" + narrative: | + The buyer reports the media buy creation back to the governance agent, + linking the media_buy_id to the governance check. This enables the governance + agent to track what was actually purchased against what was approved. + task: report_plan_outcome + schema_ref: "governance/report-plan-outcome-request.json" + response_schema_ref: "governance/report-plan-outcome-response.json" + doc_ref: "/governance/campaign/tasks/report_plan_outcome" + comply_scenario: campaign_governance + stateful: true + expected: | + Acknowledge the outcome report: + - outcome_id: identifier for this outcome record + - plan_id: the governance plan + - media_buy_id: the buy that was created + - governance_context: the approval that authorized it + - status: recorded + + sample_request: + plan_id: "$context.plan_id" + governance_context: "gov_ctx_acme_q2_approved" + outcome: "completed" + seller_response: + seller_reference: "$context.media_buy_id" + committed_budget: 50000 + packages: + - package_id: "pkg_sports_ctv_q2" + committed_budget: 30000 + - package_id: "pkg_outdoor_video_q2" + committed_budget: 20000 + + idempotency_key: "$generate:uuid_v4#media_buy_governance_escalation_report_outcome_report_plan_outcome" + context: + correlation_id: "media_buy_governance_escalation--report_plan_outcome" + validations: + - check: response_schema + description: "Response matches report-plan-outcome-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--report_plan_outcome" + description: "Context correlation_id returned unchanged" + - id: audit_trail + title: "Governance audit trail" + narrative: | + The buyer (or an auditor) retrieves the full audit trail for the governance + plan. This shows every decision in the chain: the initial denial, the human + escalation, the conditional approval, the media buy creation, and the outcome + report. The audit trail provides accountability for automated spending decisions. + + steps: + - id: get_plan_audit_logs + title: "Retrieve the full governance audit trail" + narrative: | + The buyer requests the audit log for the governance plan. The log shows + every governance event: check requests, denials, escalations, approvals, + and outcome reports. Each entry has a timestamp, actor, and decision. + task: get_plan_audit_logs + schema_ref: "governance/get-plan-audit-logs-request.json" + response_schema_ref: "governance/get-plan-audit-logs-response.json" + doc_ref: "/governance/campaign/tasks/get_plan_audit_logs" + comply_scenario: campaign_governance + stateful: false + expected: | + Return the complete audit trail for the governance plan: + - plan_id: the governance plan + - entries: ordered list of governance events, including: + 1. Plan registered with $20K per-transaction threshold + 2. Governance check denied — spending authority exceeded ($50K > $20K) + 3. Human escalation initiated + 4. Human approved with conditions (weekly reporting, budget increase review) + 5. Media buy created with governance approval + 6. Outcome reported linking media_buy_id to governance context + - Each entry includes: timestamp, event_type, actor, decision, details + + sample_request: + plan_ids: + - "$context.plan_id" + + context: + correlation_id: "media_buy_governance_escalation--get_plan_audit_logs" + validations: + - check: response_schema + description: "Response matches get-plan-audit-logs-response.json schema" + - check: field_present + path: "plans" + description: "Response contains audit log entries" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_governance_escalation--get_plan_audit_logs" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/media-buy/creative-reception.yaml b/dist/compliance/3.0.1/protocols/media-buy/creative-reception.yaml new file mode 100644 index 0000000000..56b4b5fb59 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/creative-reception.yaml @@ -0,0 +1,247 @@ +id: creative_sales_agent +version: "1.0.0" +title: "Sales agent with creative capabilities" +category: creative_sales_agent +summary: "Stateful sales agent that accepts pushed creative assets and renders them in its environment." +track: creative +required_tools: + - sync_creatives + +narrative: | + You run a publisher platform, retail media network, or other sell-side system that + accepts creative assets from buyers. The buyer pushes assets or catalog items to your + platform, and you render them in your environment. + + Your agent is stateful: buyers push creatives to you via sync_creatives, and you + persist them for rendering. This is where catalogs get interesting — the buyer might + push product feeds (flights, hotels, retail products) that your platform renders as + native ads. + + This storyboard walks through the push-and-preview flow from the buyer's perspective. + +agent: + interaction_model: stateful_push + capabilities: + - has_creative_library + examples: + - "Publisher platforms" + - "Retail media networks" + - "Native ad platforms" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The buyer has creative assets (images, catalog feeds, or ad tags) ready to push. + The test kit provides sample assets compatible with common publisher formats. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports creative operations before browsing or building creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring creative in supported_protocols, confirming the agent handles creative operations. + sample_request: + context: + correlation_id: "creative_sales_agent--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: "creative_sales_agent--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: discover_accepted_formats + title: "Discover accepted formats" + narrative: | + The buyer first needs to know what creative formats your platform accepts. + For a publisher, this includes your native ad formats, display placements, + and any custom units. For retail media, this might include product listing + formats or sponsored product cards. + + steps: + - id: list_formats + title: "Discover accepted creative formats" + narrative: | + The buyer asks: "What creative formats does your platform accept?" Your + platform returns the formats you support — native post formats, display + units, video slots, or product listing formats. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_sync + stateful: false + expected: | + Return the creative formats your platform accepts. Each format should define: + - Asset requirements (what the buyer needs to provide) + - Render dimensions + - Any catalog requirements (for product-feed formats) + + - id: push_creatives + title: "Push creative assets" + narrative: | + The buyer pushes their creative assets to your platform. This could be: + - Standard display assets (images, HTML tags) + - Catalog items (product feeds, flight listings, hotel inventory) + - Native ad content (headlines, descriptions, images) + + Your platform validates the assets against your format specs and stores them. + + steps: + - id: sync_creatives + title: "Push creatives to the platform" + narrative: | + The buyer uploads creative assets to your platform. For standard ads, this + is images and copy. For catalog-driven formats, this is a product feed or + set of catalog items. Your platform validates each creative against the + format's asset requirements and returns a per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept the creatives, validate against format specifications, and return: + - Per-creative action (created or updated) + - Per-creative status (accepted, pending_review, rejected) + - Platform-assigned IDs if applicable + - Validation errors for rejected creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "acme_summer_native_001" + name: "Acme Summer Sale — native 2026" + format_id: + agent_url: "https://your-platform.example.com" + id: "native_post" + assets: + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-master.jpg" + width: 1200 + height: 628 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/summer-sale" + + idempotency_key: "$generate:uuid_v4#creative_sales_agent_push_creatives_sync_creatives" + context: + correlation_id: "creative_sales_agent--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_sales_agent--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: preview + title: "Preview pushed creatives" + narrative: | + After pushing assets, the buyer wants to see how their creatives will render + in your platform's environment. For a publisher, this shows the ad in the + publication's native chrome — with engagement buttons, community badges, and + platform-specific styling that the buyer can't preview elsewhere. + + steps: + - id: preview_synced + title: "Preview a pushed creative" + narrative: | + The buyer asks to see how a synced creative will look in your environment. + Your platform renders the creative with its native chrome — the surrounding + UI, engagement buttons, and platform-specific styling. + task: preview_creative + schema_ref: "creative/preview-creative-request.json" + response_schema_ref: "creative/preview-creative-response.json" + doc_ref: "/creative/task-reference/preview_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a preview showing the creative in your platform's environment. + The preview should include your platform's native chrome — not just the + raw assets, but how they'll actually appear to users. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + request_type: "single" + creative_manifest: + creative_id: "acme_summer_native_001" + format_id: + agent_url: "https://your-platform.example.com" + id: "native_post" + assets: + headline: + asset_type: "text" + content: "Summer Sale — 40% Off" + body: + asset_type: "text" + content: "Top-rated outdoor gear. This weekend only." + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero.jpg" + width: 1200 + height: 628 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/summer-sale" + output_format: "url" + quality: "draft" + + context: + correlation_id: "creative_sales_agent--preview_synced" + validations: + - check: response_schema + description: "Response matches preview-creative-response.json schema" + - check: field_present + path: "previews[0].renders[0].preview_url" + description: "Preview includes a renderable URL" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_sales_agent--preview_synced" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/media-buy/index.yaml b/dist/compliance/3.0.1/protocols/media-buy/index.yaml new file mode 100644 index 0000000000..38c421a694 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/index.yaml @@ -0,0 +1,769 @@ +id: media_buy_seller +version: "1.0.0" +title: "Media buy seller agent" +category: media_buy_seller +summary: "Seller agent that receives briefs, returns products, accepts media buys, and reports delivery." +track: media_buy +required_tools: + - get_products + - create_media_buy +requires_scenarios: + - media_buy_seller/refine_products + - media_buy_seller/delivery_reporting + - media_buy_seller/measurement_terms_rejected + - media_buy_seller/pending_creatives_to_start + - media_buy_seller/inventory_list_targeting + - 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 + sells advertising inventory. A buyer agent connects to discover your products, create + media buys, sync creatives, and monitor delivery. Your agent handles the full lifecycle + from brief to reporting. + + This storyboard walks through the core media buy flow: account setup, product discovery, + buy creation, creative sync, and delivery monitoring. + + Governance integration, product refinement, and proposal finalization are tested by + required scenarios that run alongside this storyboard. See requires_scenarios for the + full set of seller behaviors validated. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - supports_guaranteed + - supports_non_guaranteed + examples: + - "Yahoo" + - "Retail media networks" + - "Publisher platforms" + - "SSPs" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials for account setup. + The test kit provides a sample brand (Acme Outdoor) with campaign parameters + suitable for testing the full media buy flow. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "sports_preroll_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_30s" + - product_id: "lifestyle_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + pricing_options: + - product_id: "sports_preroll_q2" + pricing_option_id: "cpm_guaranteed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 22.0 + - product_id: "lifestyle_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "media_buy_seller--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: "media_buy_seller--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: account_setup + title: "Account setup" + narrative: | + Before buying anything, the buyer establishes an account relationship with + your platform. This is the handshake: the buyer tells you which brand and + agency (operator) they represent, and you return an account ID, status, and + any setup requirements. + + Some platforms approve accounts instantly. Others require human review — the + buyer gets back a pending_approval status and a URL to complete setup. The + buyer polls or waits for a webhook until the account is active. + + steps: + - id: sync_accounts + title: "Establish account relationship" + narrative: | + The buyer registers their brand and operator with your platform. This is + the first call in any new relationship. Your platform validates the request, + provisions the account, and returns its status. + + If your platform requires manual approval (credit checks, sales team review), + return the account with status pending_approval and account.setup.url populated. + The buyer directs the human to that URL to complete setup, then polls list_accounts + until the account status changes to active. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + # No TestScenario exists for account setup + stateful: true + expected: | + Return the account with: + - account_id: your platform's identifier for this relationship + - action: created or updated + - status: active (instant approval) or pending_approval (requires human review) + - account_scope: operator, brand, operator_brand, or agent + - setup.url and setup.message: populated on the account when status is pending_approval (where the human completes onboarding) + - rate_card: pricing tiers if applicable + - payment_terms: net_30, prepay, etc. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_account_setup_sync_accounts" + context: + correlation_id: "media_buy_seller--sync_accounts" + + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + - check: field_present + path: "accounts[0].status" + description: "Account has a status (active or pending_approval)" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--sync_accounts" + description: "Context correlation_id returned unchanged" + + - id: governance_setup + title: "Governance agent registration" + narrative: | + The buyer registers their governance agent with your platform. This tells your + platform where to call check_governance before confirming media buys. + + steps: + - id: sync_governance + title: "Register governance agents" + narrative: | + The buyer tells your platform: "Before you confirm any media buy for this + account, call this governance agent to validate it." + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + requires_tool: sync_governance + stateful: true + expected: | + Acknowledge the governance agents. Your platform should: + - Store the governance agent URLs for the specified accounts + - Return confirmation that agents were registered + + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "https://governance.pinnacle-agency.example" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_setup_sync_governance" + context: + correlation_id: "media_buy_seller--sync_governance" + ext: + test_platform: + governance_tier: "standard" + + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--sync_governance" + description: "Context correlation_id returned unchanged" + + - id: product_discovery + title: "Product discovery" + narrative: | + The buyer sends a natural-language brief describing what they want to buy. + Your platform interprets the brief against your inventory and returns products — + structured representations of what you can sell, with pricing, delivery forecasts, + targeting options, and creative requirements. + + This is where seller differentiation happens. The same brief sent to three sellers + produces three different product sets. Your AI interprets "premium video on sports + and outdoor lifestyle" against your specific inventory, audiences, and pricing. + + steps: + - id: get_products_brief + title: "Send a brief" + narrative: | + The buyer describes what they want in natural language. Your platform returns + products that match the brief, including pricing options, delivery forecasts, + and creative format requirements. + + This call may take up to 60 seconds — your platform is running AI inference + against your inventory catalog. If the brief is ambiguous, you can return + input-required to ask clarifying questions before producing results. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. Each product should include: + - product_id: unique identifier + - name and description + - delivery_type: guaranteed or non_guaranteed + - pricing_models: available pricing options (CPM, CPC, etc.) + - forecast: estimated impressions, reach + - creative_format_ids: what creative formats this product requires + - targeting: what audiences or contexts this product reaches + + Optionally return proposals — curated media plans that bundle products + with budget allocations the buyer can accept or refine. + + If the brief is unclear, return input-required with clarifying questions. + + sample_request: + buying_mode: "brief" + brief: "Premium video inventory on sports and outdoor lifestyle publishers. Q2 flight, $50K budget. Adults 25-54, US and Canada." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context: + correlation_id: "media_buy_seller--get_products_brief" + + context_outputs: + - name: product_format_id + path: 'products[0].format_ids[0]' + - name: product_id + path: 'products[0].product_id' + - name: pricing_option_id + path: 'products[0].pricing_options[0].pricing_option_id' + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + - check: field_present + path: "products[0].product_id" + description: "Each product has a product_id" + - check: field_present + path: "products[0].delivery_type" + description: "Each product declares guaranteed or non_guaranteed delivery" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--get_products_brief" + description: "Context correlation_id returned unchanged" + + - check: field_present + path: "products[0].publisher_properties" + description: "Products include publisher_properties" + - id: list_formats_integrity + title: "Verify format_ids on products resolve to real formats" + narrative: | + The buyer asks the sales agent to filter `list_creative_formats` by + `products[0].format_ids[0]`. The sales agent MUST return the format + it advertised on its own product — whether it hosts that format + directly or proxies to the creative agent named in + `format_ids[0].agent_url`. An empty `formats[]` means the sales + agent's product catalog references a format that does not resolve — + a stale or typo'd entry that would have failed silently at + `sync_creatives` after the media buy was already committed. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + The sales agent resolves `products[0].format_ids[0]` and returns + the matching format entry: + - formats[] contains at least one entry + - formats[0].format_id matches the id captured from get_products + + An empty formats[] means the sales agent's product catalog references + a format that does not resolve — a common production failure mode + when creative agents deprecate formats without sellers updating + their product catalog. + sample_request: + format_ids: + - "$context.product_format_id" + context: + correlation_id: "media_buy_seller--list_formats_integrity" + # The @adcp/client `list_creative_formats` request builder up + # through 5.10.0 (the currently-published release) returns `{}`, + # and the runner's post-builder merge only forwards envelope + # fields (context / ext / idempotency_key / + # push_notification_config) from sample_request — so `format_ids` + # above never reaches the wire and the seller answers with its + # full format catalog. `context_inputs` is applied after the + # builder runs, so this injects the captured `product_format_id` + # (the `{agent_url, id}` object from `products[0].format_ids[0]`) + # at `format_ids[0]` and lets the round-trip invariant actually + # grade. Drop once we bump past the @adcp/client release that + # ships adcontextprotocol/adcp-client#789. + context_inputs: + - key: product_format_id + inject_at: "format_ids[0]" + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats[0]" + description: "Sales agent resolves the format_id — products[0].format_ids[0] exists in the catalog" + - check: field_value + path: "formats[0].format_id" + value: "$context.product_format_id" + description: "Returned format_id round-trips verbatim — the agent cannot substitute a different format in response to the filter" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--list_formats_integrity" + description: "Context correlation_id returned unchanged" + - id: create_buy + title: "Create the media buy" + narrative: | + The buyer is satisfied with the products and creates a media buy. This is the + equivalent of signing an IO — the buyer commits to specific products, budgets, + and flight dates. + + This operation may be synchronous (completed immediately), short-async (working + while your platform processes), or long-async (task stays submitted while a human + signs the IO internally; task completion delivers the final media_buy_id). There + is no "pending_approval" media buy status — IO review is modelled at the task + layer, not as a MediaBuy.status value. + + If the buyer registered governance agents in Phase 2, your platform calls + check_governance before confirming the buy. The governance agent validates budget + authority, brand safety, and compliance. If governance denies the buy, return the + denial — don't override it. + + steps: + - id: create_media_buy + title: "Create a media buy" + narrative: | + The buyer commits to specific products with budgets and flight dates. Your + platform validates the request, optionally calls governance, and either confirms + the buy or sends it through an approval workflow. + + Two creation modes: + - Manual: buyer specifies packages array with explicit product selections + - Proposal: buyer passes a proposal_id from get_products to execute a proposal + + The response status tells the buyer what happens next: + - completed: buy is active and live + - working: your platform is processing (poll or wait for webhook) + - submitted: long-running async — approval workflow, IO signing, etc. + - input-required: need more information (budget clarification, approval) + 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: | + Process the media buy request and return one of: + + Synchronous (completed): + - media_buy_id: your platform's identifier + - status: active or pending_creatives + - packages: line items with pricing + - confirmed_at: timestamp + - valid_actions: what the buyer can do next + + Asynchronous (working): + - percentage: 0-100 completion + - current_step: what's happening ("Validating inventory", "Checking governance") + + Async with human approval (submitted): + - task_id / taskId: handle the buyer polls or receives webhooks on + - message (optional): explanation of what the seller is waiting on (e.g., "Awaiting IO signature from sales team; typical turnaround 2–4 hours") + - No media_buy_id yet — it is issued on task completion + - Seller-side IO signing is modelled here (task stays submitted until signed). Do not emit a "pending_approval" media buy status — that value is not in MediaBuy.status + + Needs input (input-required): + - reason: APPROVAL_REQUIRED, BUDGET_EXCEEDS_LIMIT, CLARIFICATION_NEEDED + - errors: what needs to be resolved + - Used only when the seller needs the buyer to respond (e.g., confirm a budget). If the blocker is account-level (credit application, funding), the resolution path is list_accounts / sync_accounts, where account.setup.url surfaces on the pending_approval account + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_preroll_q2" + budget: 25000 + pricing_option_id: "cpm_guaranteed" + creative_assignments: + - creative_id: "video_30s_trail_pro" + - product_id: "lifestyle_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + 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_buy_create_media_buy" + context: + correlation_id: "media_buy_seller--create_media_buy" + + context_outputs: + - name: media_buy_id + path: 'media_buy_id' + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--create_media_buy" + description: "Context correlation_id returned unchanged" + + - id: check_buy_status + title: "Check media buy status" + narrative: | + Once create_media_buy has completed and a media_buy_id is issued, the buyer + calls get_media_buys to read current state — pending_creatives, pending_start, + active, paused, completed, rejected, or canceled. + + While the create_media_buy task is still submitted (e.g., waiting on internal + IO signing), the media buy does not exist as a queryable MediaBuy yet. IO + review is tracked at the task layer, not as a MediaBuy.status value. The buyer + polls tasks/get or waits on the webhook until the task completes and a + media_buy_id is delivered. + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Return the current state of the media buy: + - media_buy_id: matches what was returned from create_media_buy + - status: pending_creatives, pending_start, active, paused, completed, rejected, canceled + - packages: line items with current delivery status + - valid_actions: what operations are available in this state + + If pending_creatives: + - Include message explaining what creatives are needed + - valid_actions should include sync_creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + context: + correlation_id: "media_buy_seller--check_buy_status" + + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_present + path: "media_buys[0].status" + description: "Each media buy has a status" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--check_buy_status" + description: "Context correlation_id returned unchanged" + + - id: creative_sync + title: "Creative sync" + narrative: | + With the media buy confirmed, the buyer syncs creative assets to your platform. + Each package in the buy has creative format requirements — the buyer discovered + these during product discovery and now pushes matching assets. + + The format_ids used in sync_creatives must match those returned by your platform + in get_products and list_creative_formats. If your platform returns a format_id + in a product but rejects it when the buyer echoes it back in sync_creatives, the + buyer cannot fulfill the creative requirements. This is a common compliance failure. + + Your platform validates each creative against the format specs and returns + per-creative status. If assets need review or transcoding, the operation may + go async. + + steps: + - id: list_formats + title: "Check creative format requirements" + narrative: | + The buyer confirms what creative formats the confirmed packages require. + Your platform returns format specs with asset requirements, dimensions, + and constraints. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return creative formats your platform accepts. Each format should define: + - format_id with your agent_url and unique id + - Asset requirements (dimensions, file sizes, mime types) + - Render dimensions + + sample_request: + context: + correlation_id: "media_buy_seller--list_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--list_formats" + description: "Context correlation_id returned unchanged" + - check: refs_resolve + description: | + Every format_id returned on products resolves to a format in this + agent's list_creative_formats. Broken references here surface as a + grading failure instead of a silent mismatch that only breaks at + sync_creatives time, after the buy is committed. Third-party + format_ids (agent_url pointing at a different creative agent) + can't be verified without calling that agent and are reported as + observations rather than failures. + source: + from: context + path: "products[*].format_ids[*]" + target: + from: current_step + path: "formats[*].format_id" + match_keys: [agent_url, id] + scope: + key: agent_url + equals: $agent_url + on_out_of_scope: warn + + - id: sync_creatives + title: "Push creative assets (format_id roundtrip)" + narrative: | + The buyer uploads creative assets for the confirmed packages. The first + creative uses $context.product_format_id — the exact format_id object + returned by get_products. This is the roundtrip test: the seller must + accept its own format_ids without modification. If the seller's validation + rejects a format_id that it returned in products, this step fails. + + Your platform validates each creative against the format specs, transcodes + if necessary, and returns per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept and validate creatives: + - Per-creative action: created or updated + - Per-creative status: accepted, pending_review, or rejected + - Validation errors for rejected creatives + - Platform-assigned IDs if applicable + + The first creative uses a format_id extracted from get_products. + If this is rejected, your format_ids do not roundtrip correctly. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "video_30s_trail_pro" + name: "Trail Pro 3000 - 30s CTV Spot" + format_id: "$context.product_format_id" + assets: + video: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + width: 1920 + height: 1080 + duration_ms: 30000 + mime_type: "video/mp4" + - creative_id: "display_trail_pro_300x250" + name: "Trail Pro 3000 - Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + width: 300 + height: 250 + mime_type: "image/png" + idempotency_key: "$generate:uuid_v4#media_buy_seller_creative_sync_sync_creatives" + context: + correlation_id: "media_buy_seller--sync_creatives" + + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--sync_creatives" + description: "Context correlation_id returned unchanged" + + - id: delivery_monitoring + title: "Delivery and reporting" + narrative: | + The campaign is live. The buyer monitors delivery through two tasks: + get_media_buys for status and get_media_buy_delivery for performance metrics. + + Your platform reports in a standard format — impressions, clicks, spend, + completion rates — so the buyer can compare delivery across multiple sellers + in a single view. + + steps: + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data for the active media buy. Your platform + returns performance metrics — impressions, clicks, spend, completion rates — + broken down by package and optionally by day. + + This call may take up to 60 seconds as your platform aggregates reporting + data across delivery systems. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics for the media buy: + - Per-package delivery: impressions, clicks, spend, completion rates + - Daily breakdown if requested (include_package_daily_breakdown) + - Pacing information: on track, ahead, behind + - Budget utilization: spent vs. committed + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + context: + correlation_id: "media_buy_seller--get_delivery" + + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_seller--get_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/create_media_buy_async.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/create_media_buy_async.yaml new file mode 100644 index 0000000000..c2ba4c3ec0 --- /dev/null +++ b/dist/compliance/3.0.1/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/dist/compliance/3.0.1/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml new file mode 100644 index 0000000000..4c9fe67900 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml @@ -0,0 +1,414 @@ +id: media_buy_seller/creative_fate_after_cancellation +version: "1.0.0" +title: "Creative lifecycle is decoupled from media buy lifecycle" +category: media_buy_seller +summary: "Validates that canceling a media buy releases package-creative assignments but leaves the underlying creatives in the library with their review state intact, and that buyers can reuse released creatives on a new buy." +track: media_buy +required_tools: + - get_products + - create_media_buy + - update_media_buy + - sync_creatives + - list_creatives + +narrative: | + Per the creative library model (docs/creative/creative-libraries#creative-state-and-assignment-state-are-separate) + and the Media Buy State Transitions rule, canceling or rejecting a media buy + releases its package-creative assignments but leaves the creatives themselves + in the library. The creatives remain reusable by `creative_id` in a subsequent + `create_media_buy` or `sync_creatives` call, and a seller MUST NOT implicitly + reject a creative because its containing buy was canceled — a creative + rejection MUST be a deliberate review decision with its own `rejection_reason`. + + This scenario walks the whole flow end-to-end: + + 1. Create a buy, sync a creative, assign it to a package (setup) + 2. Confirm the creative is in the library with a non-terminal review state (pre-cancel) + 3. Cancel the buy + 4. Confirm the creative is STILL in the library with its review state intact — + not archived, not auto-rejected as a side effect of the cancel + 5. Reuse the same `creative_id` on a new buy via `sync_creatives` assignment + + A seller that evaporates library creatives on buy cancellation, or that flips + `status: rejected` on creatives whose only assignment was released by a cancel, + fails this scenario. A seller that correctly decouples the two lifecycles + passes. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + examples: + - "Any media buy seller with a creative library" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports create_media_buy, update_media_buy with cancellation, sync_creatives, + and list_creatives. Catalog-driven sellers and sellers without a creative library + grade this scenario not_applicable (creative lifecycle assumes a library surface). + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Create a buy and assign a creative" + narrative: | + Discover a product, create a media buy, sync one creative with an inline + assignment to the buy's first package. Capture the media_buy_id, package_id, + and creative_id for subsequent phases. + + steps: + - id: get_products_brief + title: "Discover a product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product with a pricing option and at least one format_id. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content for creative-reuse testing." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context: + correlation_id: "creative_fate--get_products" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + - path: "products[0].format_ids[0].agent_url" + key: "format_agent_url" + - path: "products[0].format_ids[0].id" + key: "format_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Product exposes at least one format_id for creative sync" + + - id: create_buy + title: "Create the initial media buy" + 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: | + Media buy created with media_buy_id and at least one package. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "creative-fate-setup-create-buy-v1" + start_time: "2026-09-01T00:00:00Z" + end_time: "2026-09-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 5000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "creative_fate--create_buy" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns media_buy_id" + - check: field_present + path: "packages[0].package_id" + description: "Seller returns package_id for the newly created package" + + - id: sync_creative_with_assignment + title: "Sync a creative and assign to the package" + narrative: | + Sync one creative into the library and assign it to the package in a single + sync_creatives call. The creative enters the library with the library's + review flow; the assignment binds it to the media buy's package. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Creative accepted into the library (action: created), assignment acknowledged. + Creative status is one of: pending_review, approved, processing. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "acme_reuse_banner_001" + name: "Acme Outdoor reuse banner" + format_id: + agent_url: "$context.format_agent_url" + id: "$context.format_id" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/acme-reuse-banner.png" + width: 300 + height: 250 + mime_type: "image/png" + assignments: + - creative_id: "acme_reuse_banner_001" + package_id: "$context.package_id" + idempotency_key: "creative-fate-setup-sync-v1" + context: + correlation_id: "creative_fate--sync_creative_with_assignment" + context_outputs: + - path: "creatives[0].creative_id" + key: "creative_id" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].creative_id" + description: "Response echoes back the buyer-supplied creative_id" + + - id: verify_creative_in_library_pre_cancel + title: "Baseline: creative is in the library with a non-terminal review state" + narrative: | + Before canceling the buy, list the creative via list_creatives and record the + review state. The purpose of this phase is to establish the baseline the + post-cancel phase compares against: the creative exists and has a status + that is NOT `rejected` or `archived`. + + steps: + - id: list_creatives_before_cancel + title: "Look up the creative in the library" + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_library + stateful: true + expected: | + list_creatives returns the creative with a non-terminal review state + (processing, pending_review, or approved). + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + filters: + creative_ids: + - "$context.creative_id" + context: + correlation_id: "creative_fate--list_creatives_before_cancel" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives[0].creative_id" + description: "Creative is present in the library" + - check: field_value + path: "creatives[0].status" + allowed_values: ["processing", "pending_review", "approved"] + description: "Creative status is non-terminal (not rejected or archived) before cancel" + + - id: cancel_buy + title: "Cancel the media buy" + narrative: | + Cancel the media buy with `canceled: true`. The seller MUST transition the buy + to `canceled` and release the creative's package assignment per + docs/media-buy/specification#media-buy-state-transitions. + + steps: + - id: update_media_buy_canceled + title: "update_media_buy with canceled: true" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Seller acknowledges the cancellation and transitions the buy to `canceled`. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + cancellation_reason: "Creative-fate scenario: releasing assignment to verify library persistence." + idempotency_key: "creative-fate-cancel-v1" + context: + correlation_id: "creative_fate--update_media_buy_canceled" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + + - id: verify_creative_persists_post_cancel + title: "Creative remains in the library with review state intact" + narrative: | + After the cancel, the creative's library entry MUST still exist and its review + state MUST NOT be `rejected` (which would indicate implicit-reject-on-cancel, + forbidden by the spec) and SHOULD NOT be `archived` (which would indicate the + seller evaporated library state on buy cancellation, inconsistent with the + decoupled-lifecycle contract). Allowed terminal-adjacent states are + `processing`, `pending_review`, `approved` — whatever the review flow produced. + + steps: + - id: list_creatives_after_cancel + title: "Look up the creative again, post-cancel" + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_library + stateful: true + expected: | + Creative still present with non-rejected, non-archived status. A seller + that returns an empty list, or that has flipped the creative to + `rejected` or `archived` as a side effect of the cancel, fails. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + filters: + creative_ids: + - "$context.creative_id" + context: + correlation_id: "creative_fate--list_creatives_after_cancel" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives[0].creative_id" + description: "Creative still in the library after buy cancellation" + - check: field_value + path: "creatives[0].creative_id" + value: "acme_reuse_banner_001" + description: "Creative ID is unchanged (not re-keyed on cancel)" + - check: field_value + path: "creatives[0].status" + allowed_values: ["processing", "pending_review", "approved"] + description: "Creative status is NOT rejected and NOT archived — no implicit review cascade from the buy cancel" + + - id: reuse_creative_on_new_buy + title: "Reuse the released creative on a new media buy" + narrative: | + With the old buy canceled and the assignment released, the buyer creates a NEW + media buy and references the same creative_id in a fresh assignment. The seller + MUST accept the assignment — the creative_id resolves to the persisted library + entry, demonstrating end-to-end reusability. + + steps: + - id: create_second_buy + title: "Create a second media buy" + 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: | + Second media buy created successfully with a new media_buy_id and package_id. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "creative-fate-second-buy-v1" + start_time: "2026-10-01T00:00:00Z" + end_time: "2026-10-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 5000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "creative_fate--create_second_buy" + context_outputs: + - path: "media_buy_id" + key: "second_media_buy_id" + - path: "packages[0].package_id" + key: "second_package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Second media_buy_id returned" + - check: field_present + path: "packages[0].package_id" + description: "Second package_id returned" + + - id: reassign_creative + title: "Assign the original creative_id to the new package" + narrative: | + Reference the original creative by creative_id only (no assets, no re-upload) + and assign it to the new package. The seller resolves the creative_id from + the library; if the creative was evaporated on cancel, this call fails. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Assignment accepted. No creative-not-found or similar error. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + # Buyer-authoritative id set in sync_creative_with_assignment — + # use it literally instead of round-tripping through + # `$context.creative_id`. That context key is populated from + # the seller's response at `creatives[0].creative_id`; sellers + # whose envelope doesn't surface that exact path resolve to + # undefined and the template engine strips the creative, + # leaving `creatives: undefined` which fails pre-flight zod. + - creative_id: "acme_reuse_banner_001" + name: "Reassigned creative" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/banner_300x250.jpg" + width: 300 + height: 250 + assignments: + - creative_id: "acme_reuse_banner_001" + package_id: "$context.second_package_id" + idempotency_key: "creative-fate-reassign-v1" + context: + correlation_id: "creative_fate--reassign_creative" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/delivery_reporting.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/delivery_reporting.yaml new file mode 100644 index 0000000000..75596a2306 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/delivery_reporting.yaml @@ -0,0 +1,205 @@ +id: media_buy_seller/delivery_reporting +version: "1.0.0" +title: "Seller returns valid delivery reporting" +category: media_buy_seller +summary: "Verifies that get_media_buy_delivery returns schema-compliant delivery data after simulated delivery via the test controller." +track: reporting +required_tools: + - get_products + - create_media_buy + - get_media_buy_delivery + - comply_test_controller + +narrative: | + Delivery reporting is how buyers know if their campaign is working. The seller must + return schema-compliant delivery data from get_media_buy_delivery with per-package + metrics (impressions, spend, pacing). + + This scenario creates a media buy, injects delivery data via the test controller's + simulate_delivery scenario, then calls get_media_buy_delivery and validates the + response against the schema. Without this test, sellers can return arbitrary formats + and still pass certification. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + examples: + - "Any media buy seller" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The seller must implement comply_test_controller with the simulate_delivery + scenario. This allows the test harness to inject delivery metrics into a + media buy so get_media_buy_delivery has data to return. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "outdoor_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "outdoor_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + +phases: + - id: setup + title: "Create a media buy for delivery testing" + steps: + - id: sync_accounts + title: "Establish account" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_delivery_reporting_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Display and video inventory on outdoor lifestyle content. Q2 flight, $25K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy + title: "Create media buy" + 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: | + Create the media buy. We need a media_buy_id to simulate delivery against. + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 10000 + pricing_option_id: "cpm_standard" + idempotency_key: "$generate:uuid_v4#media_buy_seller_delivery_reporting_setup_create_media_buy" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - id: simulate_and_verify + title: "Simulate delivery and validate reporting" + narrative: | + Inject delivery metrics via the test controller, then call get_media_buy_delivery + and validate the response format. This is the core test — does the seller return + schema-compliant delivery data with per-package metrics? + + steps: + - id: simulate_delivery + title: "Inject simulated delivery metrics" + task: comply_test_controller + requires_tool: comply_test_controller + stateful: true + expected: | + The test controller acknowledges the simulated delivery data. + sample_request: + scenario: "simulate_delivery" + params: + media_buy_id: "$context.media_buy_id" + impressions: 5000 + clicks: 150 + reported_spend: + amount: 250.00 + currency: "USD" + validations: + - check: field_value + path: "success" + allowed_values: [true] + description: "Delivery simulation succeeds" + + - id: get_delivery + title: "Get delivery report and validate schema" + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics reflecting the simulated data: + - media_buy_deliveries array with at least one entry + - Per-package breakdown with impressions, spend + - Response matches the get-media-buy-delivery-response.json schema + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains delivery data" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_approved.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_approved.yaml new file mode 100644 index 0000000000..1d7a43b041 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_approved.yaml @@ -0,0 +1,211 @@ +id: media_buy_seller/governance_approved +version: "1.0.0" +title: "Seller creates buy when governance approves" +category: media_buy_seller +summary: "Verifies that the seller creates a media buy when governance approves the transaction." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + This is a multi-agent test. The test harness sets up a permissive governance plan on + a governance agent with a $100K budget, then registers that agent with the seller. + The buyer creates a $25K buy which falls within limits. + + When the seller calls check_governance during create_media_buy, the governance agent + approves. The seller must confirm the buy normally. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "outdoor_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "outdoor_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + +phases: + - id: governance_plan_setup + title: "Set up permissive governance plan" + narrative: | + Create a governance plan on the governance agent with a high budget ($100K). + The subsequent $25K buy will fall within these limits and should be approved. + + steps: + - id: sync_plans + title: "Create permissive governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan with a plan_id. + sample_request: + idempotency_key: "comply-gov-approved-sync-plans-v1" + plans: + - plan_id: "comply-gov-approved-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 outdoor lifestyle campaign — display and video" + budget: + total: 100000 + currency: "USD" + reallocation_threshold: 100000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US", "CA"] + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_approved_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_approved_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_approved + title: "Create buy within governance limits" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Display and video inventory on outdoor lifestyle content. Q2 flight, $25K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy + title: "Create buy (governance approves)" + 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: | + The buy succeeds — governance approved because the $25K buy is within + the plan's $100K budget. + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 10000 + pricing_option_id: "cpm_standard" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_approved_buy_approved_create_media_buy" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_conditions.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_conditions.yaml new file mode 100644 index 0000000000..876db0da4e --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_conditions.yaml @@ -0,0 +1,196 @@ +id: media_buy_seller/governance_conditions +version: "1.0.0" +title: "Seller attaches conditions when governance approves with conditions" +category: media_buy_seller +summary: "Verifies that the seller attaches governance conditions to the buy when governance approves with conditions." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + This is a multi-agent test. The test harness sets up a governance plan on a governance + agent with custom policies that trigger conditions (e.g., "CTV buys require weekly + delivery reporting"). The buyer creates a CTV buy within budget but matching a policy. + + When the seller calls check_governance, the governance agent approves with conditions. + The seller must create the buy and include the governance conditions and context token + in its response. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up conditional governance plan" + narrative: | + Create a governance plan on the governance agent with custom policies that + trigger conditions. The plan has sufficient budget but policies that require + conditions on CTV buys. + + steps: + - id: sync_plans + title: "Create conditional governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan with a plan_id. + sample_request: + idempotency_key: "comply-gov-conditions-sync-plans-v1" + plans: + - plan_id: "comply-gov-conditions-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Q2 CTV campaign with reporting requirements" + budget: + total: 100000 + currency: "USD" + reallocation_threshold: 100000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US", "CA"] + custom_policies: + - policy_id: "ctv_weekly_reporting" + enforcement: "must" + policy: "CTV buys require weekly delivery reporting." + - policy_id: "ugc_brand_safety" + enforcement: "must" + policy: "UGC placements require brand safety review before activation." + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_conditions_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_conditions_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_with_conditions + title: "Create CTV buy triggering governance conditions" + steps: + - id: get_products_brief + title: "Discover CTV products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return CTV/video products matching the brief. + sample_request: + buying_mode: "brief" + brief: "CTV and connected TV inventory on outdoor lifestyle content. Q2 flight, $25K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy_conditions + title: "Create CTV buy (governance approves with conditions)" + 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: | + The buy succeeds with governance conditions attached: + - media_buy_id: present + - status: active or pending_creatives + - governance_context: token from the governance agent + - conditions visible to the buyer + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_ctv_q2" + budget: 25000 + pricing_option_id: "cpm_standard" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_conditions_buy_with_conditions_create_media_buy_conditions" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_denied.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_denied.yaml new file mode 100644 index 0000000000..c93b8fdf46 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_denied.yaml @@ -0,0 +1,192 @@ +id: media_buy_seller/governance_denied +version: "1.0.0" +title: "Seller rejects buy when governance denies" +category: media_buy_seller +summary: "Verifies that the seller rejects a media buy and propagates the denial when governance denies the transaction." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + This is a multi-agent test. The test harness sets up a governance plan on a governance + agent with a strict $10K budget, then registers that agent with the seller. The buyer + attempts a $50K media buy which exceeds the plan's budget. + + When the seller calls check_governance, the governance agent denies because the + requested budget exceeds the plan total. The seller must reject the buy and propagate + the denial back to the buyer. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up strict governance plan" + narrative: | + Create a governance plan on the governance agent with a strict $10K budget + and agent_limited authority. The subsequent $50K buy will exceed the plan + budget and should be denied. + + steps: + - id: sync_plans + title: "Create strict governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan with a plan_id. + sample_request: + idempotency_key: "comply-gov-denied-sync-plans-v1" + plans: + - plan_id: "comply-gov-denied-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Small test campaign — limited budget authority" + budget: + total: 10000 + currency: "USD" + reallocation_threshold: 5000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US"] + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority", "brand_policy"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_denied + title: "Create buy exceeding governance limits" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Premium video and display on outdoor lifestyle. Q2 flight, $50K budget. Adults 25-54, US." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy_denied + title: "Create buy (governance denies — should fail)" + 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 + expect_error: true + negative_path: payload_well_formed + expected: | + The buy is rejected because governance denied — the $50K buy exceeds + the plan's $10K budget. The seller propagates the denial with findings. + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + idempotency_key: "$generate:uuid_v4#governance_denied_create_media_buy" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_display_q2" + budget: 30000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + validations: + - check: error_code + value: "GOVERNANCE_DENIED" + description: "Error code indicates governance denial" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_denied_recovery.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_denied_recovery.yaml new file mode 100644 index 0000000000..c16e8780f5 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/governance_denied_recovery.yaml @@ -0,0 +1,244 @@ +id: media_buy_seller/governance_denied_recovery +version: "1.0.0" +title: "Seller accepts corrected buy after governance denial" +category: media_buy_seller +summary: "Verifies that a buyer can recover from GOVERNANCE_DENIED by shrinking the buy to within plan limits and retrying." +track: media_buy +required_tools: + - sync_governance + - get_products + - create_media_buy + +narrative: | + GOVERNANCE_DENIED is a correctable error, not a dead end. The buyer must be able to + read the denial findings, adjust the media buy, and retry. This scenario exercises the + full loop: strict governance plan ($10K), failed $50K buy that is denied, then a + corrected $8K buy that fits within the plan and is approved. + + The seller must propagate the governance findings unchanged so the buyer can identify + exactly which constraint was violated (budget authority, brand policy, etc.) and + correct it. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Any media buy seller with governance support" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up strict governance plan" + narrative: | + Create a governance plan with a $10K budget. The initial $50K buy will exceed this + limit; the retry at $8K will fit. + + steps: + - id: sync_plans + title: "Create strict governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan. + sample_request: + idempotency_key: "comply-gov-recovery-sync-plans-v1" + plans: + - plan_id: "comply-gov-recovery-plan" + brand: + domain: "acmeoutdoor.example" + objectives: "Strict-budget plan for denial + recovery validation" + budget: + total: 10000 + currency: "USD" + reallocation_threshold: 5000 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US"] + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: seller_setup + title: "Account and governance registration on seller" + steps: + - id: sync_accounts + title: "Establish account with seller" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_recovery_seller_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with seller" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Acknowledge the governance agent registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority"] + idempotency_key: "$generate:uuid_v4#media_buy_seller_governance_denied_recovery_seller_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: buy_denied + title: "Initial buy exceeds plan — governance denies" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content. Q2 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - id: create_media_buy_denied + title: "Attempt $50K buy — denied" + 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 + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + The buy is rejected with GOVERNANCE_DENIED. The response includes findings + from the governance agent explaining which constraint was exceeded. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "gov-recovery-initial-denied-v1" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 50000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "governance_denied_recovery--create_media_buy_denied" + validations: + - check: error_code + value: "GOVERNANCE_DENIED" + description: "Error code is GOVERNANCE_DENIED" + - check: field_value + path: "context.correlation_id" + value: "governance_denied_recovery--create_media_buy_denied" + description: "Response echoes context.correlation_id verbatim on error responses (echo contract applies to both success and failure)" + + - id: buy_retried + title: "Corrected buy within plan — governance approves" + narrative: | + The buyer reads the denial findings, shrinks the budget to $8K (within the $10K + plan), and retries with a fresh idempotency_key. The seller consults governance + again, which now approves. + + steps: + - id: create_media_buy_retry + title: "Retry with $8K — approved" + 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: | + The buy succeeds. Governance approves because $8K fits within the $10K plan + budget. The seller returns a media_buy_id. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "gov-recovery-retry-approved-v1" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 8000 + pricing_option_id: "$context.pricing_option_id" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns a media_buy_id after governance approves" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/invalid_transitions.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/invalid_transitions.yaml new file mode 100644 index 0000000000..18d18f1e0e --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/invalid_transitions.yaml @@ -0,0 +1,284 @@ +id: media_buy_seller/invalid_transitions +version: "1.0.0" +title: "Seller rejects illegal state transitions and unknown references" +category: media_buy_seller +summary: "Validates that the seller returns structured AdCP errors (MEDIA_BUY_NOT_FOUND, PACKAGE_NOT_FOUND, NOT_CANCELLABLE) rather than 500s or undefined behavior when the buyer references missing entities or attempts forbidden state transitions." +track: media_buy +required_tools: + - get_products + - create_media_buy + - update_media_buy + +narrative: | + Error-code coverage is the most common compliance gap: sellers accept malformed input + silently or return generic 500s rather than the specific AdCP codes the spec defines. + This scenario forces three of the most-referenced media_buy error codes with + deterministic input: + + - MEDIA_BUY_NOT_FOUND — buyer references a media_buy_id that does not exist. + - PACKAGE_NOT_FOUND — media_buy_id is valid but package_id inside it is not. + - NOT_CANCELLABLE — buyer cancels the same media buy twice; the second cancel must + be rejected because canceled is terminal. + + All three probes use hard `check: error_code` assertions so a seller that returns + a 500 or swallows the error fails the scenario outright. Recovery hints, correlation + echo, and context preservation are also checked on every error response. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + examples: + - "Any media buy seller" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports create_media_buy and update_media_buy with cancellation via + the canceled field. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: unknown_media_buy + title: "Reference an unknown media_buy_id" + narrative: | + Before any state is set up, call update_media_buy with a fabricated media_buy_id. + The seller must return MEDIA_BUY_NOT_FOUND with a correctable recovery hint — not + a 500 and not a silent success. + + steps: + - id: update_unknown_media_buy + title: "update_media_buy with bogus media_buy_id" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + expect_error: true + negative_path: payload_well_formed + stateful: false + expected: | + Reject with: + - code: MEDIA_BUY_NOT_FOUND + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "does-not-exist-invalid-transitions-v1" + paused: true + idempotency_key: "$generate:uuid_v4#update_unknown_media_buy" + context: + correlation_id: "invalid_transitions--update_unknown_media_buy" + validations: + - check: error_code + value: "MEDIA_BUY_NOT_FOUND" + description: "Error code is MEDIA_BUY_NOT_FOUND" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "invalid_transitions--update_unknown_media_buy" + description: "Context correlation_id returned unchanged" + + - id: setup + title: "Create a real media buy for follow-up probes" + narrative: | + The remaining probes need a real media_buy_id and package_id. Discover a product + and create a plain buy — no targeting tricks, no governance — so we have known-good + IDs for the error cases. + + steps: + - id: get_products_brief + title: "Discover a product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product with pricing. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content. Q3 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + + - id: create_buy + title: "Create a buy for the error probes" + 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: | + Media buy created with media_buy_id and at least one package. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "invalid-transitions-setup-v1" + start_time: "2026-08-01T00:00:00Z" + end_time: "2026-08-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 5000 + pricing_option_id: "$context.pricing_option_id" + context: + correlation_id: "invalid_transitions--create_buy" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns media_buy_id" + + - id: unknown_package + title: "Reference an unknown package_id on a real buy" + narrative: | + The media_buy_id is real, but the package_id is fabricated. The seller must + return PACKAGE_NOT_FOUND — distinct from MEDIA_BUY_NOT_FOUND — because the + lookup succeeds at the buy level and only fails at the package lookup. + + steps: + - id: update_unknown_package + title: "update_media_buy with bogus package_id" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with: + - code: PACKAGE_NOT_FOUND + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + idempotency_key: "$generate:uuid_v4#update_unknown_package" + packages: + - package_id: "does-not-exist-package-invalid-transitions-v1" + paused: true + context: + correlation_id: "invalid_transitions--update_unknown_package" + validations: + - check: error_code + value: "PACKAGE_NOT_FOUND" + description: "Error code is PACKAGE_NOT_FOUND" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "invalid_transitions--update_unknown_package" + description: "Context correlation_id returned unchanged" + + - id: double_cancel + title: "Cancel twice — second cancel is not valid" + narrative: | + Cancel the buy (success), then try to cancel the same buy again. canceled is + terminal per the AdCP spec, so the second cancel cannot succeed. The seller + must return NOT_CANCELLABLE — the schema specifically reserves this code for + "media buy cannot be canceled in its current state." + + steps: + - id: first_cancel + title: "Cancel the media buy" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Seller acknowledges the cancellation and transitions the buy to canceled. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + cancellation_reason: "Testing NOT_CANCELLABLE on re-cancel" + idempotency_key: "$generate:uuid_v4#media_buy_seller_invalid_transitions_double_cancel_first_cancel" + context: + correlation_id: "invalid_transitions--first_cancel" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + + - id: second_cancel + title: "Re-cancel the canceled buy" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with: + - code: NOT_CANCELLABLE + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + cancellation_reason: "Deliberate re-cancel to force NOT_CANCELLABLE" + idempotency_key: "$generate:uuid_v4#media_buy_seller_invalid_transitions_double_cancel_second_cancel" + context: + correlation_id: "invalid_transitions--second_cancel" + validations: + - check: error_code + value: "NOT_CANCELLABLE" + description: "Error code is NOT_CANCELLABLE on re-cancel of canceled buy" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "invalid_transitions--second_cancel" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/inventory_list_no_match.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/inventory_list_no_match.yaml new file mode 100644 index 0000000000..8f5f4eb301 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/inventory_list_no_match.yaml @@ -0,0 +1,143 @@ +id: media_buy_seller/inventory_list_no_match +version: "1.0.0" +title: "Seller handles property_list / collection_list references that match zero inventory" +category: media_buy_seller +summary: "Verifies a seller returns a zero-forecast product or a clear error — not a crash — when a buyer references inventory lists that resolve to nothing in the seller's catalog." +track: media_buy +required_tools: + - get_products + - create_media_buy + +narrative: | + Not every buyer list matches every seller's inventory. A buyer may point at a + property_list of domains this seller does not carry, or a collection_list of + programs this seller does not syndicate. The seller must handle that gracefully: + + - Return a product with a zero forecast and explain the mismatch, OR + - Return an informative error on create_media_buy (INSUFFICIENT_INVENTORY or + similar) with findings the buyer can act on. + + What the seller must NOT do: crash, return a misleading non-zero forecast, or + silently drop the targeting and deliver against unintended inventory. Each of + those is a real failure mode we've seen in adapter integrations. + + This scenario exercises the no-match path using the test kit's pre-populated + no_match_properties and no_match_collections lists. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - supports_property_list_targeting + - supports_collection_list_targeting + examples: + - "Sellers that validate inventory against buyer-supplied lists" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The seller must resolve PropertyListReference / CollectionListReference against + its own inventory and report a truthful outcome when nothing matches. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: discover + title: "Discover products" + steps: + - id: get_products_brief + title: "Discover products" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products the seller carries. + sample_request: + buying_mode: "brief" + brief: "Video inventory on outdoor lifestyle programming. Q3 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "inventory_list_no_match--get_products_brief" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + + - id: no_match_attempt + title: "Create buy with lists that match no inventory" + narrative: | + The buyer references the no-match property list and the no-match collection + list. Neither list resolves to anything in the seller's catalog. The seller + must either (a) accept the buy and surface a zero-delivery expectation, or + (b) reject it with an informative error. Silent success with undefined + delivery behaviour is not acceptable. + + steps: + - id: create_buy_no_match + title: "create_media_buy against empty intersections" + 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: | + One of two acceptable outcomes: + + 1. Buy accepted with zero-forecast reporting — status may be + pending_creatives/pending_start/active, but the seller returns + packages with forecast indicating zero deliverable inventory and + a message explaining the list mismatch. + + 2. Buy rejected with an informative error — typically + INSUFFICIENT_INVENTORY or INVALID_TARGETING — including findings + that identify which list(s) matched nothing. + + What is NOT acceptable: a silently-successful buy with normal forecast + numbers, or a crash / non-AdCP error shape. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "inventory-list-no-match-v1" + start_time: "2026-07-01T00:00:00Z" + end_time: "2026-09-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 10000 + pricing_option_id: "$context.pricing_option_id" + targeting_overlay: + property_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_v1" + collection_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_collections_v1" + + context: + correlation_id: "inventory_list_no_match--create_buy_no_match" + validations: + - check: field_present + path: "context" + description: "Response echoes back the context object (success or error)" + - check: field_value + path: "context.correlation_id" + value: "inventory_list_no_match--create_buy_no_match" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/inventory_list_targeting.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/inventory_list_targeting.yaml new file mode 100644 index 0000000000..93e3e32627 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/inventory_list_targeting.yaml @@ -0,0 +1,266 @@ +id: media_buy_seller/inventory_list_targeting +version: "1.0.0" +title: "Seller honors property_list and collection_list targeting on create and update" +category: media_buy_seller +summary: "Verifies that a seller accepts PropertyListReference and CollectionListReference in package targeting on create_media_buy AND update_media_buy, with parity between both paths." +track: media_buy +required_tools: + - get_products + - create_media_buy + - update_media_buy + +narrative: | + AdCP 3.0 targets inventory through agent-hosted reference lists rather than inline + property arrays. Buyers point a package's targeting at a `PropertyListReference` + and/or `CollectionListReference`; the seller resolves the list against its own + inventory at serve time. + + A common integration regression is create/update parity: a seller accepts list + references on create_media_buy but silently drops them on update_media_buy, so a + buyer who edits a live buy loses their list targeting. This scenario writes both + list types on create, then swaps both on update, and finally reads the buy back + to confirm the updated references are what the seller persisted. + + The buyer references pre-populated inventory lists from the test kit — one for + matching properties and one for matching collections — so the seller's test + engine can resolve them without needing a live governance/inventory agent. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - supports_property_list_targeting + - supports_collection_list_targeting + examples: + - "Sellers that accept PropertyListReference / CollectionListReference in package targeting" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The seller must accept PropertyListReference and CollectionListReference in + package targeting on both create_media_buy and update_media_buy. List contents + come from test-kits/acme-outdoor.yaml → inventory_targets. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: discover_product + title: "Discover a product that supports list targeting" + steps: + - id: get_products_brief + title: "Discover product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product whose targeting supports property_list and + collection_list references. + + sample_request: + buying_mode: "brief" + brief: "Video inventory on outdoor lifestyle programming. Q3 flight, $30K budget." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "inventory_list_targeting--get_products_brief" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].product_id" + description: "Product has a product_id" + + - id: create_with_both_lists + title: "Create media buy with property_list AND collection_list targeting" + narrative: | + The buyer references the matching-property list and the matching-collection + list from the test kit. The seller accepts both references, creates the buy, + and echoes the resolved targeting back on the package. + + steps: + - id: create_buy_with_lists + title: "create_media_buy with both list references" + 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: | + The seller accepts the list references without error. The response + includes a media_buy_id and the package retains both the property_list + and collection_list targeting fields so subsequent get / update calls + can read them back. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "inventory-list-targeting-create-v1" + start_time: "2026-07-01T00:00:00Z" + end_time: "2026-09-30T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 20000 + pricing_option_id: "$context.pricing_option_id" + targeting_overlay: + property_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_allowlist_v1" + collection_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_collections_v1" + + context: + correlation_id: "inventory_list_targeting--create_buy_with_lists" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller assigns a media_buy_id" + - check: field_present + path: "context" + description: "Response echoes back the context object" + + - id: verify_create_persisted + title: "Verify list targeting persisted after create" + narrative: | + Read the buy back to confirm the seller stored both list references — not just + echoed them back in the create response. This catches sellers that parse list + references on create but never persist them to their internal package model. + + steps: + - id: get_after_create + title: "get_media_buys after create" + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Return the media buy with packages[0].targeting_overlay.property_list + and .collection_list populated with the list_id values sent on create. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "inventory_list_targeting--get_after_create" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.property_list.list_id" + value: "acme_outdoor_allowlist_v1" + description: "property_list.list_id persisted after create" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.collection_list.list_id" + value: "acme_outdoor_collections_v1" + description: "collection_list.list_id persisted after create" + + - id: update_swap_lists + title: "Swap property_list and collection_list via update_media_buy" + narrative: | + The buyer swaps both list references. This is the create/update parity check: + a seller that accepts list references on create but silently drops them on + update would fail to update the persisted targeting. We exercise update + explicitly to catch that class of regression. + + steps: + - id: update_buy_swap_lists + title: "update_media_buy replaces both list references" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The seller acknowledges the updated targeting. Both property_list and + collection_list are replaced (not merged) with the new list_id values. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + packages: + - package_id: "$context.package_id" + targeting_overlay: + property_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_v1" + collection_list: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_collections_v1" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_inventory_list_targeting_update_swap_lists_update_buy_swap_lists" + context: + correlation_id: "inventory_list_targeting--update_buy_swap_lists" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + + - id: get_after_update + title: "Confirm swap persisted" + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The persisted package targeting reflects the new list_id values. This is + the create/update parity guarantee: edits through update_media_buy land + in persistent state just like fields on create_media_buy. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "inventory_list_targeting--get_after_update" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.property_list.list_id" + value: "acme_outdoor_no_match_v1" + description: "property_list.list_id updated after update_media_buy" + - check: field_value + path: "media_buys[0].packages[0].targeting_overlay.collection_list.list_id" + value: "acme_outdoor_no_match_collections_v1" + description: "collection_list.list_id updated after update_media_buy" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/measurement_terms_rejected.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/measurement_terms_rejected.yaml new file mode 100644 index 0000000000..3ac9452152 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/measurement_terms_rejected.yaml @@ -0,0 +1,195 @@ +id: media_buy_seller/measurement_terms_rejected +version: "1.0.0" +title: "Seller rejects unworkable measurement_terms" +category: media_buy_seller +summary: "Buyer proposes measurement_terms the seller will not accept; seller returns TERMS_REJECTED; buyer retries with seller-compatible terms." +track: media_buy +required_tools: + - get_products + - create_media_buy + +narrative: | + measurement_terms negotiation is a round-trip. The buyer proposes a measurement vendor, + window, and variance tolerance on each package; the seller either accepts (returning + measurement_terms echoed in the response) or rejects with TERMS_REJECTED and a message + explaining what is unacceptable. The buyer corrects the terms and retries the same + create_media_buy idempotency_key with an adjusted payload. + + This scenario sends an intentionally aggressive first proposal (max_variance_percent: 0 + with a window the seller does not guarantee), verifies the seller rejects with + TERMS_REJECTED, then retries with a relaxed proposal that falls inside the seller's + stated tolerance and expects a successful create_media_buy. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - measurement_terms + examples: + - "Broadcast sellers" + - "CTV sellers with C3/C7 guarantees" + - "Any seller that negotiates measurement terms" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports measurement_terms on create_media_buy packages. Buyer knows the + seller's declared measurement capabilities from get_adcp_capabilities. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: discover_products + title: "Discover products" + steps: + - id: get_products_brief + title: "Find a product that supports measurement_terms" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products with pricing_options. At least one product should advertise + measurement support in the response. + sample_request: + buying_mode: "brief" + brief: "Premium video inventory with measurement guarantees. Q2 flight, $50K." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].product_id" + description: "At least one product returned" + + - id: reject_terms + title: "Seller rejects unworkable terms" + narrative: | + The buyer proposes max_variance_percent: 0 — a tolerance almost no seller can + honor against third-party measurement. The seller must reject with TERMS_REJECTED + and indicate what would be acceptable. + + steps: + - id: create_media_buy_aggressive_terms + title: "Propose aggressive measurement_terms" + 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 + expect_error: true + negative_path: payload_well_formed + stateful: false + expected: | + Reject with: + - code: TERMS_REJECTED + - recovery: correctable + - message indicating which terms are unworkable (vendor, window, or variance) + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "measurement-terms-probe-aggressive-v1" + start_time: "2026-05-01T00:00:00Z" + end_time: "2026-05-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 25000 + pricing_option_id: "$context.pricing_option_id" + measurement_terms: + billing_measurement: + vendor: + domain: "videoamp.example" + measurement_window: "c30" + max_variance_percent: 0 + makegood_policy: + available_remedies: ["credit"] + + context: + correlation_id: "measurement_terms_rejected--aggressive" + validations: + - check: error_code + value: "TERMS_REJECTED" + description: "Error code is TERMS_REJECTED" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "measurement_terms_rejected--aggressive" + description: "Context correlation_id returned unchanged" + + - id: accept_terms + title: "Buyer retries with acceptable terms" + narrative: | + The buyer relaxes measurement_terms to match the seller's stated capability + (c7 window, 10% variance, makegood remedies) and retries. The seller accepts and + returns the confirmed terms echoed in the response. + + steps: + - id: create_media_buy_relaxed_terms + title: "Propose seller-compatible measurement_terms" + 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: | + The buy succeeds. The response echoes the accepted measurement_terms (possibly + normalized by the seller). + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "measurement-terms-probe-relaxed-v1" + start_time: "2026-05-01T00:00:00Z" + end_time: "2026-05-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 25000 + pricing_option_id: "$context.pricing_option_id" + measurement_terms: + billing_measurement: + vendor: + domain: "videoamp.example" + measurement_window: "c7" + max_variance_percent: 10 + makegood_policy: + available_remedies: ["additional_delivery", "credit"] + + context: + correlation_id: "measurement_terms_rejected--relaxed" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller returns a media_buy_id after accepting terms" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "measurement_terms_rejected--relaxed" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/pending_creatives_to_start.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/pending_creatives_to_start.yaml new file mode 100644 index 0000000000..9bf1a639a0 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/pending_creatives_to_start.yaml @@ -0,0 +1,250 @@ +id: media_buy_seller/pending_creatives_to_start +version: "1.0.0" +title: "Creative sync unblocks pending_creatives → pending_start" +category: media_buy_seller +summary: "Verifies that a media buy created without creatives sits in pending_creatives until sync_creatives completes, then transitions to pending_start." +track: media_buy +required_tools: + - get_products + - create_media_buy + - sync_creatives + - get_media_buys + +narrative: | + When a buyer creates a media buy without creative_assignments, the seller cannot start + delivery until creatives are supplied. The seller must report status: pending_creatives + and, after sync_creatives attaches the required assets, transition the buy to + pending_start (awaiting flight start) or active (if already in flight). + + This scenario walks the transition end-to-end: create the buy, confirm pending_creatives, + sync a creative, and confirm the status advances to pending_start. The transition + proves that the creative pipeline and the buy state machine are wired together — a + common integration gap when creative and media buy systems live on different backends. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_creatives + examples: + - "Any seller that requires creatives before starting delivery" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + Seller supports create_media_buy without creative_assignments and honors the + pending_creatives → pending_start transition when creatives are synced. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Discover products and formats" + steps: + - id: get_products_brief + title: "Discover a product" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return at least one product with format_ids and pricing options. + sample_request: + buying_mode: "brief" + brief: "Display inventory on outdoor lifestyle content. Q3 flight." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "products[0].product_id" + key: "product_id" + - path: "products[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + - path: "products[0].format_ids[0]" + key: "format_id" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Product declares a format_id for creative sync" + + - id: create_without_creatives + title: "Create media buy without creative_assignments" + narrative: | + The buyer intentionally omits creative_assignments. The seller must accept the + buy, persist it, and return status: pending_creatives with valid_actions + including sync_creatives. + + steps: + - id: create_buy_no_creatives + title: "Create buy in pending_creatives" + 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: | + Media buy created with: + - media_buy_id assigned + - status: pending_creatives + - valid_actions including sync_creatives + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "pending-creatives-transition-v1" + start_time: "2026-08-01T00:00:00Z" + end_time: "2026-08-31T23:59:59Z" + packages: + - product_id: "$context.product_id" + budget: 10000 + pricing_option_id: "$context.pricing_option_id" + + context: + correlation_id: "pending_creatives_to_start--create_buy_no_creatives" + context_outputs: + - path: "media_buy_id" + key: "media_buy_id" + - path: "packages[0].package_id" + key: "package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Seller assigns a media_buy_id" + - check: field_value + path: "status" + value: "pending_creatives" + description: "Status is pending_creatives because no creatives supplied" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "pending_creatives_to_start--create_buy_no_creatives" + description: "Context correlation_id returned unchanged" + + - id: supply_creatives + title: "Supply creatives and assign to the package" + narrative: | + The buyer syncs a creative with the format the product requires, then updates the + media buy with creative_assignments so the seller can attach the asset to the + package and advance state. + + steps: + - id: sync_creative + title: "Sync the creative asset" + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + The seller ingests the creative and returns status active (or approved). + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "acme-outdoor-display-q3" + name: "Acme Outdoor Q3 display" + format_id: "$context.format_id" + assets: + image: + asset_type: "image" + url: "https://creative.acmeoutdoor.example/q3/display-300x250.jpg" + width: 300 + height: 250 + mime_type: "image/jpeg" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_pending_creatives_to_start_supply_creatives_sync_creative" + context: + correlation_id: "pending_creatives_to_start--sync_creative" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + + - id: assign_creative_to_package + title: "Assign the creative to the package" + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The seller acknowledges the assignment and advances the buy state. The response + status should be pending_start (or active if the flight has started). + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + packages: + - package_id: "$context.package_id" + creative_assignments: + - creative_id: "acme-outdoor-display-q3" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_pending_creatives_to_start_supply_creatives_assign_creative_to_package" + context: + correlation_id: "pending_creatives_to_start--assign_creative_to_package" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + - check: field_value + path: "status" + allowed_values: ["pending_start", "active"] + description: "Status advances out of pending_creatives once creatives attached" + + - id: verify_transition + title: "Verify the status transition" + narrative: | + Independently of the update_media_buy response, call get_media_buys to confirm the + seller's stored state has advanced. This protects against sellers that return the + updated status in response but do not persist it. + + steps: + - id: get_media_buy_after_sync + title: "Confirm pending_start after creative sync" + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + The media buy's persisted status is pending_start (or active). valid_actions + no longer includes sync_creatives as a required next step. + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "pending_creatives_to_start--get_media_buy_after_sync" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_value + path: "media_buys[0].status" + allowed_values: ["pending_start", "active"] + description: "Persisted status is past pending_creatives" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/proposal_finalize.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/proposal_finalize.yaml new file mode 100644 index 0000000000..cd30feb805 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/proposal_finalize.yaml @@ -0,0 +1,243 @@ +id: media_buy_seller/proposal_finalize +version: "1.0.0" +title: "Seller handles proposal refinement and finalize" +category: media_buy_seller +summary: "Verifies the full proposal lifecycle: brief with proposals, refine a proposal, finalize to committed, and accept via create_media_buy." +track: media_buy +required_tools: + - get_products + - create_media_buy + +narrative: | + Proposals are curated media plans that the seller generates alongside products. The buyer + reviews proposals, refines them (adjust budget splits, swap products, add constraints), + and then finalizes a proposal to get firm pricing and an inventory hold. Once committed, + the buyer accepts the proposal via create_media_buy. + + The proposal lifecycle is: draft (indicative pricing) -> refine -> finalize (firm pricing, + inventory hold) -> create_media_buy (accept and go live). + + This scenario exercises the complete proposal flow including the finalize action, which + transitions a proposal from draft to committed status with an expires_at hold window. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - generates_proposals + examples: + - "Full-service publisher with proposal engine" + - "Retail media network with curated packages" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials. The seller must support + proposal generation (return proposals alongside products in get_products responses). + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Account setup" + steps: + - id: sync_accounts + title: "Establish account" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_proposal_finalize_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: brief_with_proposals + title: "Brief with proposals" + narrative: | + Send a brief and receive proposals alongside products. Proposals are curated + media plans with budget allocations the buyer can review and refine. + + steps: + - id: get_products_brief + title: "Send a brief and receive proposals" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products and proposals matching the brief: + - products: individual products with pricing and forecasts + - proposals: curated media plans with proposal_id, budget_allocations, rationale + + sample_request: + buying_mode: "brief" + brief: "Premium video and display across outdoor lifestyle and sports. Q2 flight, $50K budget. Adults 25-54, US and Canada." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + - check: field_present + path: "proposals" + description: "Response contains proposals" + - check: field_present + path: "proposals[0].proposal_id" + description: "Proposals have IDs" + + - id: refine_proposal + title: "Refine the proposal" + narrative: | + The buyer reviews the proposals and wants to adjust one. They call get_products in + refine mode targeting a specific proposal_id with changes. The seller applies the + refinements and returns the updated proposal. + + steps: + - id: get_products_refine + title: "Refine a specific proposal" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: true + expected: | + Return the refined proposal: + - proposals: updated proposal reflecting the requested changes + - refinement_applied: how each refinement was handled + - Updated budget allocations, product selections, and forecasts + + sample_request: + buying_mode: "refine" + refine: + - scope: "proposal" + proposal_id: "balanced_reach_q2" + ask: "Shift 60% of budget to CTV. Drop the display product and redistribute that budget to video." + - scope: "request" + ask: "All products must support frequency capping at 3 per day." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "proposals" + description: "Response contains updated proposals" + + - id: finalize_proposal + title: "Finalize the proposal" + narrative: | + The buyer is satisfied with the refined proposal and requests finalization. This + triggers the transition from draft (indicative pricing) to committed (firm pricing + with inventory hold). The seller may need time to process this — the buyer should + not set a time_budget to signal willingness to wait. + + After finalization, the proposal has firm pricing and an expires_at timestamp. + The buyer must create the media buy before the hold expires. + + steps: + - id: get_products_finalize + title: "Finalize the proposal" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: true + expected: | + Return the finalized proposal with committed status: + - proposals[0].proposal_status: committed + - proposals[0].expires_at: timestamp for the inventory hold window + - Firm pricing (not indicative) + - The proposal is ready to accept via create_media_buy + + sample_request: + buying_mode: "refine" + refine: + - scope: "proposal" + proposal_id: "balanced_reach_q2" + action: "finalize" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "proposals" + description: "Response contains the finalized proposal" + + - id: accept_proposal + title: "Accept the committed proposal" + narrative: | + The buyer accepts the committed proposal by creating a media buy with the + proposal_id. The seller converts the proposal's product selections and budget + allocations into active packages. + + steps: + - id: create_media_buy + title: "Create media buy from proposal" + 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: | + Convert the committed proposal into an active media buy: + - media_buy_id: the seller's identifier + - status: active + - confirmed_at: timestamp + - packages: line items derived from the proposal's budget allocations + - proposal_id: echoed back to confirm which proposal was accepted + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + sandbox: true + proposal_id: "balanced_reach_q2" + total_budget: + amount: 50000 + currency: "USD" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_proposal_finalize_accept_proposal_create_media_buy" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" diff --git a/dist/compliance/3.0.1/protocols/media-buy/scenarios/refine_products.yaml b/dist/compliance/3.0.1/protocols/media-buy/scenarios/refine_products.yaml new file mode 100644 index 0000000000..f47c311c2c --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/scenarios/refine_products.yaml @@ -0,0 +1,148 @@ +id: media_buy_seller/refine_products +version: "1.0.0" +title: "Seller handles product refinement" +category: media_buy_seller +summary: "Verifies that a media buy seller supports buying_mode: refine with product-level and request-level changes." +track: media_buy +required_tools: + - get_products + +narrative: | + After a buyer receives products from a brief, they refine the results without starting + over. The buyer calls get_products with buying_mode: refine and a refine array describing + changes at the request level ("only guaranteed") or product level ("increase this product's + budget"). + + The seller must apply each refinement, return updated products, and include + refinement_applied showing how each request was handled. This is the standard negotiation + loop — brief, review, refine, repeat until satisfied. + + Every media buy seller that supports negotiation (not just wholesale) must handle + product-level refinement. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + examples: + - "Any media buy seller that accepts briefs" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: setup + title: "Account setup" + steps: + - id: sync_accounts + title: "Establish account" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id and status active. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#media_buy_seller_refine_products_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: brief + title: "Initial brief" + narrative: | + Send a brief to get the initial product set that the buyer will refine. + + steps: + - id: get_products_brief + title: "Send a brief" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. Each product should include product_id, + delivery_type, pricing_models, and forecast. + + sample_request: + buying_mode: "brief" + brief: "Premium video and display on sports and outdoor lifestyle. Q2 flight, $50K budget. Adults 25-54, US and Canada." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + - check: field_present + path: "products[0].product_id" + description: "Products have IDs" + + - id: refine + title: "Refine products" + narrative: | + The buyer has reviewed the initial products and wants to narrow down. They call + get_products with buying_mode: refine and a refine array with request-level and + product-level changes. The seller applies each refinement and returns the updated + product set. + + steps: + - id: get_products_refine + title: "Refine with request-level and product-level changes" + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: true + expected: | + Return updated products reflecting the refinements: + - Apply each refinement to the relevant scope (request or product) + - Include refinement_applied showing how each request was handled + - Preserve products that weren't targeted by refinements + - Update pricing and forecasts to reflect the changes + + sample_request: + buying_mode: "refine" + refine: + - scope: "request" + ask: "Only guaranteed packages. Must include completion rate SLA above 80%." + - scope: "product" + product_id: "sports_preroll_q2" + ask: "Increase budget allocation to $30K" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains updated products" diff --git a/dist/compliance/3.0.1/protocols/media-buy/state-machine.yaml b/dist/compliance/3.0.1/protocols/media-buy/state-machine.yaml new file mode 100644 index 0000000000..ddc67b9a33 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/media-buy/state-machine.yaml @@ -0,0 +1,442 @@ +id: media_buy_state_machine +version: "1.0.0" +title: "Media buy state machine lifecycle" +category: media_buy_state_machine +summary: "Validates media buy state transitions: create, pause, resume, cancel, and terminal state enforcement." +track: media_buy +required_tools: + - create_media_buy + - update_media_buy + +narrative: | + A media buy has a well-defined state machine: pending_creatives, pending_start, active, + paused, completed, rejected, canceled. Transitions between states must follow the spec — you cannot + resume a canceled buy or pause a completed one. + + This storyboard creates a media buy, walks it through pause/resume/cancel transitions, then + verifies that the agent rejects updates to a buy in a terminal state (canceled or completed). + It also tests package-level pause/resume independent of the media buy status. + + The state machine is the backbone of media buy reliability. If an agent allows invalid + transitions, buyers cannot trust the status field and automation breaks down. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + examples: + - "Any AdCP seller with media buy support" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity for account setup. The test creates a media buy + using discovered products, then exercises state transitions. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "media_buy_state_machine--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: "media_buy_state_machine--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: setup + title: "Create a media buy" + narrative: | + Discover products and create a media buy to use for state transition testing. + The media buy ID is captured and passed to subsequent phases. + + steps: + - id: discover_products + title: "Discover products for media buy" + narrative: | + Send a brief to get available products with pricing options. The first product + with a pricing option will be used to create the test media buy. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products with: + - product_id + - pricing_options with pricing_option_id + + sample_request: + buying_mode: "brief" + brief: "Display advertising products for state machine testing" + brand: + domain: "acmeoutdoor.example" + + context: + correlation_id: "media_buy_state_machine--discover_products" + context_outputs: + - name: product_id + path: 'products[0].product_id' + - name: pricing_option_id + path: 'products[0].pricing_options[0].pricing_option_id' + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products[0].product_id" + description: "At least one product with a product_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--discover_products" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: create_buy + title: "Create the test media buy" + narrative: | + Create a media buy using the discovered product. This buy will be used for + all subsequent state transition tests. + 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 an active media buy with: + - media_buy_id + - status: active + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + start_time: "2026-05-01T00:00:00Z" + end_time: "2026-05-31T23:59:59Z" + packages: + - product_id: "test-product" + budget: 10000 + pricing_option_id: "test-pricing" + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_setup_create_buy" + context: + correlation_id: "media_buy_state_machine--create_buy" + context_outputs: + - name: media_buy_id + path: 'media_buy_id' + - name: package_id + path: 'packages[0].package_id' + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Response includes a media_buy_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--create_buy" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "packages[0].package_id" + description: "Seller assigns package_id — must be echoed in update_media_buy" + - id: state_transitions + title: "Valid state transitions" + narrative: | + Exercise the valid state transitions: pause, resume, and cancel. Each transition + must update the status field correctly. + + steps: + - id: pause_buy + title: "Pause the media buy" + narrative: | + Pause the active media buy. The status should transition to paused. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Media buy status transitions to paused. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: true + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_state_transitions_pause_buy" + context: + correlation_id: "media_buy_state_machine--pause_buy" + validations: + - check: field_present + path: "status" + description: "Response includes updated status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--pause_buy" + description: "Context correlation_id returned unchanged" + - id: resume_buy + title: "Resume the media buy" + narrative: | + Resume the paused media buy. The status should transition back to active. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Media buy status transitions back to active. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: false + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_state_transitions_resume_buy" + context: + correlation_id: "media_buy_state_machine--resume_buy" + validations: + - check: field_present + path: "status" + description: "Response includes updated status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--resume_buy" + description: "Context correlation_id returned unchanged" + - id: cancel_buy + title: "Cancel the media buy" + narrative: | + Cancel the media buy. This is a terminal transition — the buy cannot be + resumed or modified after cancellation. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Media buy status transitions to canceled. This is terminal. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_state_transitions_cancel_buy" + context: + correlation_id: "media_buy_state_machine--cancel_buy" + validations: + - check: field_present + path: "status" + description: "Response includes canceled status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--cancel_buy" + description: "Context correlation_id returned unchanged" + - id: terminal_enforcement + title: "Terminal state enforcement" + narrative: | + Verify that the agent rejects state transitions on a canceled media buy. Pausing + or resuming a terminal buy returns INVALID_STATE (generic terminal-state rule). + Re-canceling a terminal buy returns NOT_CANCELLABLE — the cancellation-specific + code takes precedence over the generic INVALID_STATE when the attempted + transition is itself a cancellation. Non-cancellation illegal transitions into + or out of terminal states still return INVALID_STATE. + + steps: + - id: pause_canceled_buy + title: "Reject pause on canceled buy" + narrative: | + Attempt to pause a canceled media buy. The agent must reject this with + INVALID_STATE. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: terminal_state_enforcement + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with INVALID_STATE — cannot pause a canceled buy. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: true + idempotency_key: "$generate:uuid_v4#pause_canceled_buy" + + context: + correlation_id: "media_buy_state_machine--pause_canceled_buy" + validations: + - check: error_code + allowed_values: ["INVALID_STATE"] + description: "Invalid transition rejected with INVALID_STATE" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--pause_canceled_buy" + description: "Context correlation_id returned unchanged" + - id: resume_canceled_buy + title: "Reject resume on canceled buy" + narrative: | + Attempt to resume a canceled media buy. The agent must reject this with + INVALID_STATE. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: terminal_state_enforcement + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with INVALID_STATE — cannot resume a canceled buy. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + paused: false + idempotency_key: "$generate:uuid_v4#resume_canceled_buy" + + context: + correlation_id: "media_buy_state_machine--resume_canceled_buy" + validations: + - check: error_code + allowed_values: ["INVALID_STATE"] + description: "Invalid transition rejected with INVALID_STATE" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--resume_canceled_buy" + description: "Context correlation_id returned unchanged" + - id: recancel_buy + title: "Reject re-cancel of canceled buy with NOT_CANCELLABLE" + narrative: | + Attempt to cancel an already-canceled media buy. The agent MUST reject + this with NOT_CANCELLABLE. The cancellation-specific code takes + precedence over the generic terminal-state INVALID_STATE when the + terminal update is itself a cancellation attempt — see §128/§129 of + the media-buy specification and media_buy_seller/invalid_transitions + for the canonical vector. Idempotent acceptance is NOT conformant for + this case. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: terminal_state_enforcement + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Reject with: + - code: NOT_CANCELLABLE + - recovery: correctable + - context echoed unchanged + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + canceled: true + + idempotency_key: "$generate:uuid_v4#media_buy_state_machine_terminal_enforcement_recancel_buy" + context: + correlation_id: "media_buy_state_machine--recancel_buy" + validations: + - check: error_code + value: "NOT_CANCELLABLE" + description: "Error code is NOT_CANCELLABLE on re-cancel of canceled buy" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" + - check: field_value + path: "context.correlation_id" + value: "media_buy_state_machine--recancel_buy" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/signals/index.yaml b/dist/compliance/3.0.1/protocols/signals/index.yaml new file mode 100644 index 0000000000..8301cc20f2 --- /dev/null +++ b/dist/compliance/3.0.1/protocols/signals/index.yaml @@ -0,0 +1,266 @@ +id: signals_baseline +version: "1.1.0" +title: "Signals baseline" +protocol: signals +category: signals_baseline +summary: "Baseline domain storyboard — every signals agent must discover signals and return an activation, regardless of whether they are owned or marketplace." +track: signals +required_tools: + - get_signals + - activate_signal + +narrative: | + Signals domain agents expose audience and contextual signals to buyers and + activate them on downstream destinations (DSPs, sales agents, clean rooms). + Every signals agent — whether a first-party owned platform or a third-party + marketplace — must support the same three-call flow at the protocol level: + + 1. Declare the signals protocol in get_adcp_capabilities. + 2. Respond to get_signals with a schema-valid signals array. + 3. Respond to activate_signal with a schema-valid deployments array. + + This baseline tests those three calls and nothing beyond them. Specialism + storyboards (signal-owned, signal-marketplace) exercise the richer flows + specific to each model — pricing option selection, source/provenance + discriminators, agent-destination vs. platform-destination activation, + and deactivation for compliance. + + Agents declaring supported_protocols: ["signals"] MUST pass this baseline + even before claiming a specialism. Declaring signals without exposing + get_signals or activate_signal fails the baseline with missing_tool. + +agent: + interaction_model: owned_signals + capabilities: [] + examples: + - "Any signals agent (owned or marketplace)" + - "Retailer CDPs" + - "Publisher contextual platforms" + - "Data provider marketplaces" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The buyer has a campaign brief with broad targeting objectives. The test + kit provides a sample brand (Nova Motors) with a signal description that + any signals agent should be able to interpret. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent serves signals + before issuing discovery or activation calls. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares `signals` in supported_protocols. + Without this claim the buyer will not send get_signals or + activate_signal. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring `signals` in supported_protocols. + + sample_request: + context: + correlation_id: "signals_baseline--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: "signals_baseline--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: discovery + title: "Signal discovery" + narrative: | + The buyer calls get_signals with a natural language signal_spec. Every + signals agent — owned or marketplace — must return a schema-valid + response. The buyer uses the returned signal_agent_segment_id values + to drive activation in the next phase. + + steps: + - id: search_signals + title: "Discover signals matching a spec" + narrative: | + The buyer describes a target audience in natural language. The agent + returns a list of signals from its catalog that match. Each signal + must include the fields the buyer needs to proceed to activation. + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow + stateful: false + expected: | + Return a signals array with at least one entry. Each signal must + carry a signal_agent_segment_id that the buyer can pass to + activate_signal, along with pricing_options and a signal_id that + includes a source discriminator. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_spec: "Adults interested in electric vehicles" + context: + correlation_id: "signals_baseline--search_signals" + context_outputs: + - name: signal_agent_segment_id + path: "signals[0].signal_agent_segment_id" + - name: pricing_option_id + path: "signals[0].pricing_options[0].pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-signals-response.json schema" + - check: field_present + path: "signals[0].signal_agent_segment_id" + description: "First signal carries a signal_agent_segment_id" + - check: field_present + path: "signals[0].signal_id.source" + description: "Signal ID carries a source discriminator (agent_native or data_provider)" + - check: field_present + path: "signals[0].pricing_options" + description: "Signal carries pricing options the buyer can select" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signals_baseline--search_signals" + description: "Context correlation_id returned unchanged" + + - id: activation + title: "Signal activation" + narrative: | + The buyer activates one of the signals returned from discovery against + a destination. The protocol baseline tests only that the agent accepts + the call and returns a schema-valid deployments array — specialisms + exercise owned vs. marketplace activation patterns in depth. + + steps: + - id: activate_on_agent + title: "Activate on a sales agent destination" + narrative: | + Using the signal_agent_segment_id and pricing_option_id captured + from the previous step, the buyer activates the signal on a sales + agent destination. Every signals agent MUST accept `type: agent` + per the signals specification — the SA records the activation + internally and applies targeting in subsequent media-buy calls. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployments array with at least one entry carrying a + `type` discriminator and, for live deployments, an + `activation_key`. Agents MAY return an async deployment with + `is_live: false` and `estimated_activation_duration_minutes`. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "$context.signal_agent_segment_id" + pricing_option_id: "$context.pricing_option_id" + destinations: + - type: "agent" + agent_url: "https://wonderstruck.salesagents.example" + idempotency_key: "$generate:uuid_v4#signals_baseline_activate_agent" + + context: + correlation_id: "signals_baseline--activate_on_agent" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].type" + description: "Deployment carries a type discriminator" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signals_baseline--activate_on_agent" + description: "Context correlation_id returned unchanged" + + - id: activate_on_platform + title: "Activate on a platform destination" + narrative: | + The buyer re-activates the same signal against a platform + destination (a DSP). Signal agents MUST accept `type: platform` + per the signals specification — the agent pushes the segment to + the platform and returns a deployment record. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployments array with at least one entry whose + `type: "platform"` and, for live deployments, an + `activation_key` with `type: "segment_id"`. Async deployments + MAY report `is_live: false` with + `estimated_activation_duration_minutes`. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "$context.signal_agent_segment_id" + pricing_option_id: "$context.pricing_option_id" + destinations: + - type: "platform" + platform: "the-trade-desk" + account: "agency-123-ttd" + idempotency_key: "$generate:uuid_v4#signals_baseline_activate_platform" + + context: + correlation_id: "signals_baseline--activate_on_platform" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].type" + description: "Deployment carries a type discriminator" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signals_baseline--activate_on_platform" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/protocols/sponsored-intelligence/index.yaml b/dist/compliance/3.0.1/protocols/sponsored-intelligence/index.yaml new file mode 100644 index 0000000000..9846ad7bcc --- /dev/null +++ b/dist/compliance/3.0.1/protocols/sponsored-intelligence/index.yaml @@ -0,0 +1,256 @@ +id: si_baseline +version: "1.0.0" +title: "Sponsored intelligence baseline" +protocol: sponsored-intelligence +category: si_baseline +summary: "Baseline domain storyboard — every SI agent must discover offerings, initiate a session, exchange messages, and terminate cleanly." +track: si +required_tools: + - si_initiate_session + +narrative: | + You run an AI platform that supports sponsored intelligence — conversational ad experiences + embedded in AI-powered search, chat, or assistant products. A buyer agent connects to + discover what SI offerings are available, initiate a session, send messages within the + conversation, and cleanly terminate when done. + + Sponsored intelligence is fundamentally different from display or video advertising. The + ad experience is conversational — the user asks a question, the AI responds, and sponsored + content is woven into the response in a way that is transparent and relevant. + + This storyboard covers the SI session lifecycle from the buyer's perspective: discovering + what the platform offers, starting a conversation, exchanging messages, and ending the + session. + +agent: + interaction_model: si_platform + capabilities: + - sponsored_intelligence + examples: + - "Perplexity" + - "ChatGPT Search" + - "Arc Browser" + - "AI assistants with ad support" + +caller: + role: buyer_agent + example: "Nova Motors (advertiser)" + +prerequisites: + description: | + The caller needs brand context and campaign parameters for SI. The test kit provides + a sample brand (Nova Motors) with signal definitions suitable for conversational + ad experiences. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports sponsored intelligence before initiating sessions. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring sponsored_intelligence in supported_protocols, confirming the agent supports conversational ad experiences. + sample_request: + context: + correlation_id: "si_session--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: "si_session--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: offering_discovery + title: "Discover SI offerings" + narrative: | + Before initiating any session, the buyer discovers what sponsored intelligence + offerings the platform has available. This determines what kinds of conversational + experiences can be sponsored and at what pricing. + + steps: + - id: si_get_offering + title: "Get available SI offerings" + narrative: | + The buyer calls si_get_offering to learn what conversational ad experiences + the platform supports. The response describes available offerings with pricing, + targeting options, and format specifications. + task: si_get_offering + schema_ref: "sponsored-intelligence/si-get-offering-request.json" + response_schema_ref: "sponsored-intelligence/si-get-offering-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_get_offering" + comply_scenario: si_availability + stateful: false + expected: | + Return available SI offerings: + - Offering descriptions with pricing + - Supported conversation types + - Targeting and context options + - Format specifications for sponsored content + + sample_request: + offering_id: "novamotors_conversational_v1" + context: + correlation_id: "si_session--si_get_offering" + + context_outputs: + - name: offering_id + path: 'offering_id' + validations: + - check: response_schema + description: "Response matches si-get-offering-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_get_offering" + description: "Context correlation_id returned unchanged" + - id: session_lifecycle + title: "Session lifecycle" + narrative: | + The buyer initiates a session, exchanges messages within it, and terminates + cleanly. Each session represents a single conversational ad experience — the + buyer provides context and the platform weaves sponsored content into the + conversation. + + steps: + - id: si_initiate_session + title: "Start a conversation session" + narrative: | + The buyer initiates a new SI session with campaign context. The platform + creates a session and returns a session ID that the buyer uses for subsequent + messages. + task: si_initiate_session + schema_ref: "sponsored-intelligence/si-initiate-session-request.json" + response_schema_ref: "sponsored-intelligence/si-initiate-session-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_initiate_session" + comply_scenario: si_session_lifecycle + stateful: true + expected: | + Return a new session: + - session_id: platform-assigned session identifier + - status: active + - Initial context acknowledgment + - Available interaction modes + + sample_request: + intent: "User is researching electric vehicles for long road trips and wants to talk to Nova Motors" + identity: + consent_granted: true + consent_timestamp: "2026-04-22T14:00:00Z" + user: + locale: "en-US" + + idempotency_key: "$generate:uuid_v4#si_baseline_session_lifecycle_si_initiate_session" + context: + correlation_id: "si_session--si_initiate_session" + context_outputs: + - name: session_id + path: 'session_id' + validations: + - check: response_schema + description: "Response matches si-initiate-session-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_initiate_session" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "session_id" + description: "Platform assigns session_id — must be echoed in si_send_message and si_terminate_session" + - id: si_send_message + title: "Exchange messages" + narrative: | + The buyer sends a message within the active session. The platform processes + the message and returns a response that may include sponsored content woven + into the conversational experience. + task: si_send_message + schema_ref: "sponsored-intelligence/si-send-message-request.json" + response_schema_ref: "sponsored-intelligence/si-send-message-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_send_message" + comply_scenario: si_session_lifecycle + stateful: true + expected: | + Process the message and return a response: + - Message acknowledgment + - Response content (may include sponsored elements) + - Session state (active, waiting, etc.) + + sample_request: + session_id: "$context.session_id" + message: "What are the best electric vehicles for long road trips?" + + idempotency_key: "$generate:uuid_v4#si_baseline_session_lifecycle_si_send_message" + context: + correlation_id: "si_session--si_send_message" + validations: + - check: response_schema + description: "Response matches si-send-message-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_send_message" + description: "Context correlation_id returned unchanged" + - id: si_terminate_session + title: "End the session" + narrative: | + The buyer terminates the SI session. The platform records session metrics + and returns a summary of the conversation including any sponsored content + that was delivered. + task: si_terminate_session + schema_ref: "sponsored-intelligence/si-terminate-session-request.json" + response_schema_ref: "sponsored-intelligence/si-terminate-session-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_terminate_session" + comply_scenario: si_handoff + stateful: true + expected: | + Terminate the session and return a summary: + - session_id: confirms which session was terminated + - status: terminated + - Session metrics (duration, messages exchanged) + - Sponsored content delivery summary + + sample_request: + session_id: "$context.session_id" + reason: "handoff_complete" + + context: + correlation_id: "si_session--si_terminate_session" + validations: + - check: response_schema + description: "Response matches si-terminate-session-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "si_session--si_terminate_session" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/audience-sync/index.yaml b/dist/compliance/3.0.1/specialisms/audience-sync/index.yaml new file mode 100644 index 0000000000..618999fede --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/audience-sync/index.yaml @@ -0,0 +1,280 @@ +id: audience_sync +version: "1.0.0" +title: "Audience sync" +protocol: media-buy +category: audience_sync +summary: "Full audience lifecycle: account discovery, audience creation with hashed identifiers, and audience deletion." +track: audiences +required_tools: + - list_accounts + - sync_audiences + +# Cross-step assertion (adcp#2664, extended in adcp-client#782). status.monotonic +# rejects audience status transitions observed across steps that aren't on +# the spec lifecycle graph. Audience lifecycle is fully bidirectional across +# processing / ready / too_small — the graph allows ready ↔ processing on +# re-sync, ready ↔ too_small as counts cross minimum_size, and the re-sync +# path too_small → processing → ready. Off-graph regressions (e.g. a seller +# silently downgrading from ready to an unknown enum value) fail here rather +# than slipping past per-step schema checks. +invariants: + - status.monotonic + +platform_types: + - display_ad_server + - social_platform + - retail_media + - audio_platform + - dsp + - pmax_platform + +narrative: | + You support audience syncing via the sync_audiences task. Buyers push first-party + audience segments to your platform using hashed identifiers (emails, phones). Your + platform matches those identifiers against your user graph and returns match rates. + + This storyboard verifies the full audience lifecycle: discover an account to sync + against, create a test audience with hashed identifiers, and clean up by deleting it. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_audiences + examples: + - "Retail media networks" + - "Social platforms" + - "DSPs with audience onboarding" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs an active account on the seller platform. The account_setup phase + discovers or creates the account relationship before syncing audiences. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before syncing audiences. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "audience_sync--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: "audience_sync--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account setup" + narrative: | + Before syncing audiences, the buyer needs an account on the seller platform. + This phase discovers an existing account via list_accounts or establishes one + via sync_accounts. + + steps: + - id: discover_account + title: "Discover or create account" + narrative: | + List existing accounts to find one suitable for audience sync. The account + must already exist and be active. The account_id is captured for use in + subsequent audience operations. + task: list_accounts + schema_ref: "account/list-accounts-request.json" + response_schema_ref: "account/list-accounts-response.json" + doc_ref: "/accounts/tasks/list_accounts" + stateful: false + expected: | + Return at least one account with: + - account_id: platform-assigned identifier + - status: active (required for audience sync) + + sample_request: + context: + correlation_id: "audience_sync--discover_account" + + validations: + - check: response_schema + description: "Response matches list-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "At least one account exists with an account_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "audience_sync--discover_account" + description: "Context correlation_id returned unchanged" + - id: audience_sync + title: "Audience sync" + narrative: | + The buyer syncs a test audience containing hashed email and phone identifiers. + The seller platform matches those identifiers and returns per-audience results + including action (created/updated), status, and match rates. + + After verifying creation, the test audience is deleted to clean up. + + steps: + - id: discover_audiences + title: "Discover existing audiences" + narrative: | + Call sync_audiences with no audiences array to discover what audiences + already exist on the platform for this account. This is a read-only + discovery call. + task: sync_audiences + schema_ref: "media-buy/sync-audiences-request.json" + response_schema_ref: "media-buy/sync-audiences-response.json" + doc_ref: "/media-buy/task-reference/sync_audiences" + comply_scenario: sync_audiences + stateful: false + expected: | + Return existing audiences for the account (may be empty). + Each audience should have an audience_id and status. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + idempotency_key: "$generate:uuid_v4#audience_sync_audience_sync_discover_audiences" + context: + correlation_id: "audience_sync--discover_audiences" + validations: + - check: response_schema + description: "Response matches sync-audiences-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "audience_sync--discover_audiences" + description: "Context correlation_id returned unchanged" + - id: create_audience + title: "Create test audience" + narrative: | + Push a test audience with hashed email and phone identifiers. The seller + matches identifiers against their user graph and returns the audience with + action: created, match counts, and match rate. + task: sync_audiences + schema_ref: "media-buy/sync-audiences-request.json" + response_schema_ref: "media-buy/sync-audiences-response.json" + doc_ref: "/media-buy/task-reference/sync_audiences" + comply_scenario: sync_audiences + stateful: true + expected: | + Accept the audience and return: + - audience_id: matches the submitted ID + - action: created + - status: active or processing + - uploaded_count: number of identifiers received + - matched_count: number of identifiers matched + - effective_match_rate: ratio of matched to uploaded + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + audiences: + - audience_id: "adcp-test-audience-001" + name: "AdCP test audience" + add: + - external_id: "adcp-user-0001" + hashed_email: "a000000000000000000000000000000000000000000000000000000000000000" + - external_id: "adcp-user-0002" + hashed_phone: "b000000000000000000000000000000000000000000000000000000000000000" + + idempotency_key: "$generate:uuid_v4#audience_sync_audience_sync_create_audience" + context: + correlation_id: "audience_sync--create_audience" + validations: + - check: response_schema + description: "Response matches sync-audiences-response.json schema" + - check: field_present + path: "audiences[0].audience_id" + description: "Audience has an audience_id" + - check: field_present + path: "audiences[0].action" + description: "Audience has an action (created or updated)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "audience_sync--create_audience" + description: "Context correlation_id returned unchanged" + - id: delete_audience + title: "Delete test audience" + narrative: | + Clean up by deleting the test audience. The seller should acknowledge the + deletion with action: deleted. + task: sync_audiences + schema_ref: "media-buy/sync-audiences-request.json" + response_schema_ref: "media-buy/sync-audiences-response.json" + doc_ref: "/media-buy/task-reference/sync_audiences" + comply_scenario: sync_audiences + stateful: true + expected: | + Delete the test audience and return: + - audience_id: matches the test audience + - action: deleted + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + audiences: + - audience_id: "adcp-test-audience-001" + delete: true + + idempotency_key: "$generate:uuid_v4#audience_sync_audience_sync_delete_audience" + context: + correlation_id: "audience_sync--delete_audience" + validations: + - check: response_schema + description: "Response matches sync-audiences-response.json schema" + - check: field_present + path: "audiences[0].action" + description: "Audience deletion acknowledged with action field" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "audience_sync--delete_audience" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/brand-rights/index.yaml b/dist/compliance/3.0.1/specialisms/brand-rights/index.yaml new file mode 100644 index 0000000000..19bfcfa9fb --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/brand-rights/index.yaml @@ -0,0 +1,350 @@ +id: brand_rights +version: "1.0.0" +title: "Brand identity and rights licensing" +protocol: brand +category: brand_rights +summary: "Brand agent that serves identity assets and licenses rights for AI-generated content." +track: core +required_tools: + - get_brand_identity +requires_scenarios: + - brand_rights/governance_denied + +narrative: | + You run a brand agent — a system that holds brand identity data (logos, colors, fonts, + tone of voice) and licenses rights for AI-generated content. A buyer agent connects to + discover your brand identity, browse available rights, acquire licenses, manage them over + time, and submit generated creatives for brand approval. + + Brand agents are the bridge between brand owners and generative AI. When a DSP or + creative platform wants to generate an ad for your brand, they call your brand agent + to get the identity guidelines, license the right to generate content, and then submit + the result for approval before it goes live. + + This storyboard covers the full brand rights lifecycle: discovering the brand, browsing + rights, acquiring a license, managing it, and approving generated content. + +agent: + interaction_model: brand_rights_holder + capabilities: + - brand_identity + - rights_licensing + examples: + - "Rights management platforms" + - "Talent agencies" + - "Brand licensing services" + - "Enterprise brand portals" + +caller: + role: buyer_agent + example: "Pinnacle Agency (creative buyer)" + +prerequisites: + description: | + The caller needs brand context for identity discovery and campaign parameters for + rights acquisition. The test kit provides a sample brand with visual identity assets. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports brand identity and rights before requesting brand data. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring brand in supported_protocols, confirming the agent serves brand identity and rights. + sample_request: + context: + correlation_id: "brand_rights--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: "brand_rights--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: identity_discovery + title: "Brand identity discovery" + narrative: | + The buyer retrieves the brand's public identity — logos, colors, fonts, tone of + voice, and visual guidelines. This is the foundation for any generative content: + the buyer needs to know what the brand looks like and sounds like before generating + anything. + + steps: + - id: get_brand_identity + title: "Retrieve brand identity" + narrative: | + The buyer calls get_brand_identity to retrieve the brand's visual and verbal + identity. Public-tier access returns basic assets. Authorized access (after + account linking) provides high-resolution assets, voice configs, and detailed + tone guidelines. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand-protocol/tasks/get_brand_identity" + stateful: false + expected: | + Return brand identity data: + - Logos at multiple resolutions + - Brand colors (primary, secondary, accent) + - Typography (fonts, weights, sizes) + - Tone of voice guidelines + - Visual style guidelines + + sample_request: + brand_id: "acme_outdoor" + + context: + correlation_id: "brand_rights--get_brand_identity" + + validations: + - check: response_schema + description: "Response matches brand identity schema" + - check: field_present + path: "brand_id" + description: "Response includes brand_id" + - check: field_value + path: "brand_id" + value: "acme_outdoor" + description: "Returned brand_id matches the requested brand" + - check: field_present + path: "names" + description: "Response includes brand names" + + - id: get_brand_identity_invalid + title: "Reject invalid brand ID" + narrative: | + The buyer requests identity for a brand that does not exist. The agent must + return an error rather than generating default identity data — an agent that + silently returns placeholder identity would pass schema checks but fail this + behavioral test. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand-protocol/tasks/get_brand_identity" + stateful: false + expect_error: true + negative_path: payload_well_formed + expected: | + Reject the request with an error: + - Error indicates the brand was not found + - No default or placeholder identity data returned + + sample_request: + brand_id: "nonexistent_brand_xyz" + + validations: + - check: error_code + description: "Error code present for unknown brand" + + - id: rights_search + title: "Browse available rights" + narrative: | + The buyer discovers what rights are available for licensing. Rights define what + generative content can be created — image generation, video synthesis, voice + cloning, copy writing — and at what terms. + + steps: + - id: get_rights + title: "Discover available rights" + narrative: | + The buyer calls get_rights to search across the agent's full rights + roster (talent, music, stock media, etc.). `buyer_brand` identifies + the advertiser so the agent can apply compatibility filtering + (competitor exclusions, dietary conflicts from the buyer's + brand.json). The optional `brand_id` filter — which scopes results + to a specific rights-holder brand (e.g., a named athlete) — is + omitted here so the agent returns its full catalog. + task: get_rights + schema_ref: "brand/get-rights-request.json" + response_schema_ref: "brand/get-rights-response.json" + doc_ref: "/brand-protocol/tasks/get_rights" + stateful: false + expected: | + Return available rights with pricing: + - Right types (image_generation, video_synthesis, copy_writing, etc.) + - Pricing options per right + - Duration and renewal terms + - Usage constraints (impression caps, geo restrictions) + + sample_request: + query: "image generation rights for advertising" + uses: + - "ai_generated_image" + buyer_brand: + domain: "acmeoutdoor.example" + + context: + correlation_id: "brand_rights--get_rights" + context_outputs: + - path: "rights.0.rights_id" + key: "rights_id" + - path: "rights.0.pricing_options.0.pricing_option_id" + key: "pricing_option_id" + + validations: + - check: response_schema + description: "Response matches get_rights schema" + - check: field_present + path: "rights" + description: "Response includes rights array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "brand_rights--get_rights" + description: "Context correlation_id returned unchanged" + - id: rights_acquisition + title: "Acquire a rights license" + narrative: | + The buyer selects a right and acquires a license. This is the contractual commitment — + the buyer pays for the right to generate content of a specific type for a defined + period. The brand agent issues generation credentials. + + steps: + - id: acquire_rights + title: "Purchase a rights license" + narrative: | + The buyer acquires a specific right by selecting a pricing option and providing + campaign context. The brand agent validates the request, processes payment, + and returns a rights grant with generation credentials. + task: acquire_rights + schema_ref: "brand/acquire-rights-request.json" + response_schema_ref: "brand/acquire-rights-response.json" + doc_ref: "/brand-protocol/tasks/acquire_rights" + stateful: true + expected: | + Return the acquired rights grant: + - rights_grant_id: unique identifier + - status: active + - Generation credentials + - Expiration date and usage limits + - Terms and constraints + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + rights_id: "$context.rights_id" + pricing_option_id: "$context.pricing_option_id" + buyer: + domain: "pinnacle-agency.example" + campaign: + description: "AI-generated display ads for Acme Outdoor summer trail gear campaign" + uses: + - "likeness" + - "commercial" + start_date: "2026-04-01" + end_date: "2026-06-30" + revocation_webhook: + url: "https://pinnacle-agency.example/webhooks/revocation" + authentication: + schemes: + - "Bearer" + credentials: "pinnacle-revocation-webhook-secret-token" + + idempotency_key: "$generate:uuid_v4#brand_rights_rights_acquisition_acquire_rights" + context: + correlation_id: "brand_rights--acquire_rights" + context_outputs: + - path: "rights_grant_id" + key: "rights_grant_id" + + validations: + - check: response_schema + description: "Response matches acquire_rights schema" + - check: field_present + path: "rights_id" + description: "Response includes rights_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "brand_rights--acquire_rights" + description: "Context correlation_id returned unchanged" + # NOTE: update_rights and creative_approval are intentionally absent. + # - update_rights has no published JSON request/response schemas yet (spec + # describes it conceptually in rights-terms.json). + # - creative_approval is modeled in the spec as a webhook (POST to the + # approval_webhook returned from acquire_rights), not an AdCP tool. Agents + # expose it as a regular HTTP endpoint outside the MCP surface. + # Both will be added back as storyboard phases once upstream schemas land + # (see https://github.com/adcontextprotocol/adcp for tracking). + + - id: rights_enforcement + title: "Rights enforcement" + narrative: | + These steps test that the brand agent enforces rights constraints — expired + licenses, impression cap violations, and geographic restrictions. An agent that + only implements the happy path would pass the phases above but fail here. + + steps: + - id: acquire_expired_rights + title: "Reject acquisition with expired campaign dates" + narrative: | + The buyer attempts to acquire rights for a campaign with dates entirely in + the past. The brand agent must reject this — a compliant agent does not issue + licenses for expired campaigns. + task: acquire_rights + schema_ref: "brand/acquire-rights-request.json" + response_schema_ref: "brand/acquire-rights-response.json" + doc_ref: "/brand-protocol/tasks/acquire_rights" + stateful: true + expect_error: true + negative_path: payload_well_formed + expected: | + Reject the request: + - Campaign dates are in the past + - Error indicates the rights period is invalid or expired + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + rights_id: "$context.rights_id" + pricing_option_id: "$context.pricing_option_id" + buyer: + domain: "pinnacle-agency.example" + campaign: + description: "Expired campaign test" + uses: + - "commercial" + start_date: "2024-01-01" + end_date: "2024-03-31" + revocation_webhook: + url: "https://pinnacle-agency.example/webhooks/revocation" + authentication: + schemes: + - "Bearer" + credentials: "pinnacle-expired-rights-revocation-webhook-token" + idempotency_key: "$generate:uuid_v4#brand_rights_rights_enforcement_acquire_expired_rights" + + validations: + - check: error_code + description: "Error code present for expired campaign dates" diff --git a/dist/compliance/3.0.1/specialisms/brand-rights/scenarios/governance_denied.yaml b/dist/compliance/3.0.1/specialisms/brand-rights/scenarios/governance_denied.yaml new file mode 100644 index 0000000000..e65c6547e9 --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/brand-rights/scenarios/governance_denied.yaml @@ -0,0 +1,204 @@ +id: brand_rights/governance_denied +version: "1.0.0" +title: "Brand agent rejects rights acquisition when governance denies" +category: brand_rights +summary: "Verifies that a brand agent propagates GOVERNANCE_DENIED when the buyer's governance plan denies a rights license." +track: brand +required_tools: + - sync_governance + - get_rights + - acquire_rights + +narrative: | + Acquiring rights is a spending event. The brand agent must consult the buyer's + governance agent before issuing a license and deny the acquisition when governance + returns denied. This keeps licensing authority consistent with media buy spending + authority — a buyer cannot commit to a generative campaign budget that exceeds the + plan even when the spending flows to a brand agent instead of a seller. + + This scenario sets up a strict $50 governance plan, registers governance with the + brand agent via sync_governance, then attempts to acquire rights whose pricing + exceeds the plan. The brand agent must return GOVERNANCE_DENIED with findings + propagated from the governance agent. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: brand_rights_holder + capabilities: + - rights_licensing + - governance_aware + examples: + - "Any brand rights agent that honors governance before licensing" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance, and a brand + agent that supports sync_governance + acquire_rights. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up strict governance plan" + steps: + - id: sync_plans + title: "Create strict governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan. + sample_request: + plans: + - plan_id: "comply-rights-gov-denied" + brand: + domain: "acmeoutdoor.example" + objectives: "Restricted plan — rights licensing test" + budget: + total: 50 + currency: "USD" + reallocation_threshold: 25 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US"] + idempotency_key: "$generate:uuid_v4#brand_rights_governance_denied_governance_plan_setup_sync_plans" + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: brand_agent_setup + title: "Register account and governance with the brand agent" + steps: + - id: sync_accounts + title: "Establish account with brand agent" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Brand agent returns the account with account_id active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#brand_rights_governance_denied_brand_agent_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with brand agent" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Brand agent acknowledges governance registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority"] + idempotency_key: "$generate:uuid_v4#brand_rights_governance_denied_brand_agent_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: rights_denied + title: "Attempt rights acquisition — governance denies" + steps: + - id: get_rights_catalog + title: "Discover rights to license" + task: get_rights + schema_ref: "brand/get-rights-request.json" + response_schema_ref: "brand/get-rights-response.json" + doc_ref: "/brand-protocol/tasks/get_rights" + stateful: false + expected: | + Return rights available for licensing, each with pricing_options. + sample_request: + buyer: + domain: "pinnacle-agency.example" + query: "licensed commercial rights for a regional outdoor retail campaign" + uses: + - "commercial" + - "endorsement" + context_outputs: + - path: "rights[0].rights_id" + key: "rights_id" + - path: "rights[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" + validations: + - check: response_schema + description: "Response matches get-rights-response.json schema" + + - id: acquire_rights_denied + title: "acquire_rights — governance denies" + task: acquire_rights + schema_ref: "brand/acquire-rights-request.json" + response_schema_ref: "brand/acquire-rights-response.json" + doc_ref: "/brand-protocol/tasks/acquire_rights" + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Brand agent rejects with: + - code: GOVERNANCE_DENIED + - findings propagated from the governance agent + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + rights_id: "$context.rights_id" + pricing_option_id: "$context.pricing_option_id" + buyer: + domain: "pinnacle-agency.example" + campaign: + description: "Governance-denied rights acquisition probe" + uses: + - "likeness" + - "commercial" + start_date: "2026-04-01" + end_date: "2026-06-30" + revocation_webhook: + url: "https://pinnacle-agency.example/webhooks/revocation" + authentication: + schemes: + - "Bearer" + credentials: "pinnacle-revocation-webhook-secret-token" + idempotency_key: "$generate:uuid_v4#brand_rights_governance_denied_acquire_rights_denied" + + context: + correlation_id: "brand_rights--governance_denied--acquire" + validations: + - check: error_code + value: "GOVERNANCE_DENIED" + description: "Error code is GOVERNANCE_DENIED" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" diff --git a/dist/compliance/3.0.1/specialisms/collection-lists/index.yaml b/dist/compliance/3.0.1/specialisms/collection-lists/index.yaml new file mode 100644 index 0000000000..770d89e341 --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/collection-lists/index.yaml @@ -0,0 +1,359 @@ +id: collection_lists +version: "1.0.0" +title: "Collection lists" +protocol: governance +category: collection_lists +summary: "Curated collection lists for program-level brand safety and content targeting — create, query, update, and delete lists of content programs (shows, series, podcasts)." +track: governance +required_tools: + - create_collection_list + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph. Silent on collection-list-only runs (no tracked +# lifecycle resource), but wired so phases that touch media_buy / +# account status are automatically gated. +invariants: + - status.monotonic + +narrative: | + You run a governance agent that manages collection lists for brand safety. Unlike + property lists which operate on technical surfaces (domains, apps), collection lists + operate on content programs (shows, series, podcasts) identified by platform-independent + IDs like IMDb, Gracenote, or EIDR. + + Buyers create inclusion and exclusion lists that define which programs their ads can + and cannot appear against. Your agent resolves the base collections and filters into a + concrete list of program identifiers that sellers can cache and enforce at bid time. + + Collection lists are setup-time resources: the governance agent resolves them once and + sellers cache the result. Unlike property lists, there is no post-delivery validation + task yet — enforcement happens at serve time via the cached list, not via after-the-fact + compliance checks. This storyboard therefore exercises the full CRUD lifecycle (create, + query, update, delete) but does not test delivery validation. + +agent: + interaction_model: governance_agent + capabilities: + - collection_lists + - brand_safety + examples: + - "IAS" + - "DoubleVerify" + - "GARM-aligned platforms" + - "Brand safety services" + +caller: + role: buyer_agent + example: "Nova Motors (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and content-program knowledge (distribution IDs, + publisher identifiers, or genre taxonomies). The test kit provides a sample brand + with campaign context. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports governance + before creating or fetching collection lists. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring governance in supported_protocols, confirming + the agent provides governance services. + sample_request: + context: + correlation_id: "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: "collection_lists--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: create_list + title: "Create collection lists" + narrative: | + The buyer creates an inclusion collection list for the campaign. The list defines + which content programs the brand is willing to advertise against, selected by + platform-independent distribution identifiers. + + steps: + - id: create_inclusion_list + title: "Create an inclusion collection list" + narrative: | + The buyer creates an inclusion list of programs the brand considers safe to + advertise against, referenced by distribution IDs (e.g. IMDb) so the selection + is portable across publishers. + task: create_collection_list + schema_ref: "collection/create-collection-list-request.json" + response_schema_ref: "collection/create-collection-list-response.json" + doc_ref: "/governance/collection/tasks/collection_lists" + comply_scenario: governance_collection_lists + stateful: true + expected: | + Return the created collection list: + - list_id: platform-assigned identifier + - collection_count reflecting the resolved programs + - auth_token issued for sellers to fetch the list + - Timestamps populated + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + brand: + domain: "novamotors.example" + name: "Nova Motors approved programs" + base_collections: + - selection_type: "distribution_ids" + identifiers: + - type: "imdb_id" + value: "tt9999901" + - type: "imdb_id" + value: "tt9999902" + - type: "imdb_id" + value: "tt9999903" + filters: + kinds: ["series"] + + idempotency_key: "$generate:uuid_v4#collection_lists_create_list_create_inclusion_list" + context: + correlation_id: "collection_lists--create_inclusion_list" + context_outputs: + - path: "list.list_id" + key: "collection_list_id" + + validations: + - check: response_schema + description: "Response matches create-collection-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Governance agent assigns list_id — must be echoed in get/update/delete" + - check: field_present + path: "auth_token" + description: "Agent issues an auth_token at creation time for seller fetches" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "collection_lists--create_inclusion_list" + description: "Context correlation_id returned unchanged" + + - id: list_and_get + title: "Query collection lists" + narrative: | + The buyer lists all collection lists for the brand and retrieves a specific list + with resolved collections. + + steps: + - id: list_collection_lists + title: "List all collection lists" + narrative: | + The buyer lists all collection lists for the brand. The response includes + list metadata without full resolved collection details. + task: list_collection_lists + schema_ref: "collection/list-collection-lists-request.json" + response_schema_ref: "collection/list-collection-lists-response.json" + doc_ref: "/governance/collection/tasks/collection_lists" + comply_scenario: governance_collection_lists + stateful: true + expected: | + Return collection list summaries: + - Array of lists with list_id, name, collection_count + - Includes the list created in the prior phase + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + name_contains: "Nova Motors" + + context: + correlation_id: "collection_lists--list_collection_lists" + validations: + - check: response_schema + description: "Response matches list-collection-lists-response.json schema" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "collection_lists--list_collection_lists" + description: "Context correlation_id returned unchanged" + + - id: get_collection_list + title: "Get a specific collection list with resolved collections" + narrative: | + The buyer retrieves a specific collection list with resolve:true to confirm + the agent can materialize the programs the list references. + task: get_collection_list + schema_ref: "collection/get-collection-list-request.json" + response_schema_ref: "collection/get-collection-list-response.json" + doc_ref: "/governance/collection/tasks/collection_lists" + comply_scenario: governance_collection_lists + stateful: true + expected: | + Return the full collection list: + - list_id, name, collection_count + - Resolved collections with distribution_ids, content_rating, genre + - cache_valid_until timestamp for seller caching + + sample_request: + list_id: "$context.collection_list_id" + resolve: true + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "collection_lists--get_collection_list" + validations: + - check: response_schema + description: "Response matches get-collection-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "List id returned" + - check: field_present + path: "collections" + description: "Resolved collections returned when resolve:true" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "collection_lists--get_collection_list" + description: "Context correlation_id returned unchanged" + + - id: update_list + title: "Update collection lists" + narrative: | + The buyer modifies an existing collection list — replacing the base collections + as the campaign's content preferences evolve. + + steps: + - id: update_collection_list + title: "Update a collection list" + narrative: | + The buyer replaces the base collections on an existing list with a new set + of distribution identifiers. + task: update_collection_list + schema_ref: "collection/update-collection-list-request.json" + response_schema_ref: "collection/update-collection-list-response.json" + doc_ref: "/governance/collection/tasks/collection_lists" + comply_scenario: governance_collection_lists + stateful: true + expected: | + Return the updated collection list: + - Same list_id + - Updated collection_count reflecting the replaced base collections + - Updated updated_at timestamp + + sample_request: + list_id: "$context.collection_list_id" + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + base_collections: + - selection_type: "distribution_ids" + identifiers: + - type: "imdb_id" + value: "tt9999901" + - type: "imdb_id" + value: "tt9999904" + - type: "imdb_id" + value: "tt9999905" + - type: "imdb_id" + value: "tt9999906" + + idempotency_key: "$generate:uuid_v4#collection_lists_update_list_update_collection_list" + context: + correlation_id: "collection_lists--update_collection_list" + validations: + - check: response_schema + description: "Response matches update-collection-list-response.json schema" + - check: field_present + path: "list.list_id" + description: "Updated list retains its list_id" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "collection_lists--update_collection_list" + description: "Context correlation_id returned unchanged" + + - id: delete_list + title: "Delete a collection list" + narrative: | + The buyer removes a collection list that is no longer needed. Deleting a list + revokes the associated auth_token. + + steps: + - id: delete_collection_list + title: "Delete a collection list" + narrative: | + The buyer deletes a collection list. The governance agent removes the list + and returns confirmation. + task: delete_collection_list + schema_ref: "collection/delete-collection-list-request.json" + response_schema_ref: "collection/delete-collection-list-response.json" + doc_ref: "/governance/collection/tasks/collection_lists" + comply_scenario: governance_collection_lists + stateful: true + expected: | + Confirm deletion: + - deleted: true + - list_id: the deleted list + + sample_request: + list_id: "$context.collection_list_id" + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + + idempotency_key: "$generate:uuid_v4#collection_lists_delete_list_delete_collection_list" + context: + correlation_id: "collection_lists--delete_collection_list" + validations: + - check: response_schema + description: "Response matches delete-collection-list-response.json schema" + - check: field_value + path: "deleted" + value: true + description: "Delete returns deleted: true" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "collection_lists--delete_collection_list" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/content-standards/index.yaml b/dist/compliance/3.0.1/specialisms/content-standards/index.yaml new file mode 100644 index 0000000000..e063e3e01a --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/content-standards/index.yaml @@ -0,0 +1,572 @@ +id: content_standards +version: "1.0.0" +title: "Content standards" +protocol: governance +category: content_standards +summary: "Define creative quality rules, calibrate content against them, and validate that delivered ads met the standards." +track: governance +required_tools: + - list_content_standards + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph. Silent on content-standards-only runs (no tracked +# lifecycle resource), but wired so phases that touch creative / +# account status are automatically gated. +invariants: + - status.monotonic + +narrative: | + You run a governance agent that manages content standards — rules that define what + creative content is acceptable for a brand or campaign. Buyers create standards that + specify quality requirements, safety constraints, and brand compliance rules. Your + agent stores these standards, calibrates sample content against them, and validates + that delivered creatives met the requirements. + + Content standards complement property governance. Property lists control where ads appear; + content standards control what the ads look like. Together they form a complete brand + safety framework. + + This storyboard covers the full content standards lifecycle: defining standards, querying + them, calibrating content before launch, and validating delivery after the fact. + +agent: + interaction_model: governance_agent + capabilities: + - content_standards + - creative_quality + examples: + - "Creative quality platforms" + - "Brand safety services" + - "Ad verification platforms" + - "GARM-aligned quality tools" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and creative quality requirements. The test kit + provides a sample brand with visual assets for calibration. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports governance before registering plans or checking compliance. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring governance in supported_protocols, confirming the agent provides governance services. + sample_request: + context: + correlation_id: "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: "content_standards--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: create_standards + title: "Define content standards" + narrative: | + The buyer creates content standards that define what creative content is acceptable. + Standards include quality requirements, safety constraints, and brand-specific rules. + + steps: + - id: create_content_standards + title: "Create content standards" + narrative: | + The buyer defines a set of content standards. These rules will be used to + evaluate creatives before and after delivery. + 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/content-standards/tasks/create_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return the created content standards: + - standards_id: platform-assigned identifier + - Rules registered with severity levels + - Status: active + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + scope: + languages_any: ["en"] + description: "Acme Outdoor creative quality standards" + policies: + - policy_id: no_violent_imagery + policy_categories: [brand_safety] + enforcement: must + policy: "No violent or controversial imagery" + - policy_id: min_display_dpi + policy_categories: [imagery_quality] + enforcement: should + channels: [display] + policy: "Minimum 72 DPI for display assets" + - policy_id: brand_color_tolerance + policy_categories: [brand_compliance] + enforcement: must + policy: "Brand colors must match palette within 5% tolerance" + + idempotency_key: "$generate:uuid_v4#content_standards_create_standards_create_content_standards" + context: + correlation_id: "content_standards--create_content_standards" + validations: + - check: response_schema + description: "Response matches create-content-standards-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "content_standards--create_content_standards" + description: "Context correlation_id returned unchanged" + - id: list_and_get + title: "Query content standards" + narrative: | + The buyer lists all content standards for the account and retrieves a specific + standard to inspect its rules. + + steps: + - id: list_content_standards + title: "List all content standards" + narrative: | + The buyer lists all content standards for the brand. The response includes + standard metadata without full rule details. + 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/content-standards/tasks/list_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return content standard summaries: + - Array of standards with standards_id, name, rule_count + - Status of each standard set + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "content_standards--list_content_standards" + validations: + - check: response_schema + description: "Response matches list-content-standards-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "content_standards--list_content_standards" + description: "Context correlation_id returned unchanged" + - id: get_content_standards + title: "Get a specific content standard" + narrative: | + The buyer retrieves the full details of a specific content standard, including + all rules and their severity levels. + task: get_content_standards + schema_ref: "content-standards/get-content-standards-request.json" + response_schema_ref: "content-standards/get-content-standards-response.json" + doc_ref: "/governance/content-standards/tasks/get_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return the full content standard: + - standards_id, name + - All rules with categories, descriptions, and severity + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + standards_id: "$context.content_standards_id" + + context: + correlation_id: "content_standards--get_content_standards" + validations: + - check: response_schema + description: "Response matches get-content-standards-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "content_standards--get_content_standards" + description: "Context correlation_id returned unchanged" + - id: update_standards + title: "Update content standards" + narrative: | + The buyer modifies existing content standards — adding rules, adjusting severity, + or removing obsolete constraints. + + steps: + - id: update_content_standards + title: "Update content standards" + narrative: | + The buyer updates an existing content standard with new or modified rules. + task: update_content_standards + schema_ref: "content-standards/update-content-standards-request.json" + response_schema_ref: "content-standards/update-content-standards-response.json" + doc_ref: "/governance/content-standards/tasks/update_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return the updated content standard: + - Updated rule set + - Confirmation of changes applied + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + standards_id: "$context.content_standards_id" + policies: + - policy_id: no_violent_imagery + policy_categories: [brand_safety] + enforcement: must + policy: "No violent or controversial imagery" + - policy_id: min_display_dpi + policy_categories: [imagery_quality] + enforcement: should + channels: [display] + policy: "Minimum 72 DPI for display assets" + - policy_id: brand_color_tolerance + policy_categories: [brand_compliance] + enforcement: must + policy: "Brand colors must match palette within 5% tolerance" + - policy_id: image_alt_text + policy_categories: [accessibility] + enforcement: should + policy: "All images must have alt text" + + idempotency_key: "$generate:uuid_v4#content_standards_update_standards_update_content_standards" + context: + correlation_id: "content_standards--update_content_standards" + validations: + - check: response_schema + description: "Response matches update-content-standards-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "content_standards--update_content_standards" + description: "Context correlation_id returned unchanged" + - id: calibration + title: "Calibrate content" + narrative: | + Before launching a campaign, the buyer calibrates sample creatives against the + content standards. This is a pre-flight check — does the creative meet the rules + before it goes live? + + steps: + - id: calibrate_content + title: "Calibrate content against standards" + narrative: | + The buyer submits sample creative content for evaluation against the content + standards. The governance agent checks each rule and returns a calibration + report indicating pass/fail per rule. + task: calibrate_content + schema_ref: "content-standards/calibrate-content-request.json" + response_schema_ref: "content-standards/calibrate-content-response.json" + doc_ref: "/governance/content-standards/tasks/calibrate_content" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return a calibration report: + - overall_pass: boolean + - Per-rule results with pass/fail and evidence + - Remediation guidance for failed rules + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + standards_id: "$context.content_standards_id" + artifact: + property_rid: "test-publisher.example" + artifact_id: "display_trail_pro_300x250" + assets: + - type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + width: 300 + height: 250 + + idempotency_key: "$generate:uuid_v4#content_standards_calibration_calibrate_content" + context: + correlation_id: "content_standards--calibrate_content" + validations: + - check: response_schema + description: "Response matches calibrate-content-response.json schema" + - check: field_present + path: "verdict" + description: "Response includes calibration verdict" + + - id: must_rule_violation + title: "Must-rule violation" + narrative: | + The buyer submits content that violates a "must" severity rule in the content + standards. The governance agent must reject it — this tests that the agent + distinguishes between "must" (blocking) and "should" (advisory) severity levels. + + steps: + - id: calibrate_must_violation + title: "Calibrate content that violates a must rule" + narrative: | + The buyer submits creative content containing violent imagery, which violates + the "No violent or controversial imagery (must)" rule. The agent must return + a failing verdict with a reference to the violated rule. + task: calibrate_content + schema_ref: "content-standards/calibrate-content-request.json" + response_schema_ref: "content-standards/calibrate-content-response.json" + doc_ref: "/governance/content-standards/tasks/calibrate_content" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return a failing calibration: + - verdict: fail + - At least one rule failure referencing the violent imagery must-rule + - Remediation guidance explaining why the content was rejected + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + standards_id: "$context.content_standards_id" + artifact: + property_rid: "test-publisher.example" + artifact_id: "display_violent_imagery_300x250" + description: "Display ad featuring graphic hunting scene with violent imagery" + assets: + - type: "image" + url: "https://cdn.pinnacle-agency.example/violent-hunting-scene-300x250.png" + width: 300 + height: 250 + + idempotency_key: "$generate:uuid_v4#content_standards_must_rule_violation_calibrate_must_violation" + validations: + - check: response_schema + description: "Response matches calibrate-content-response.json schema" + - check: field_value + path: "verdict" + value: "fail" + description: "Content that violates a must-rule receives fail verdict" + + - id: standards_version_change + title: "Policy version change" + narrative: | + The buyer updates the content standards to add a stricter rule, then re-calibrates + previously passing content. This tests that the agent applies the updated policy + rather than caching the old version. + + steps: + - id: update_stricter_standards + title: "Add stricter must-rule to content standards" + narrative: | + The buyer updates the content standards to require alt text on all images + as a must-severity rule (previously "should"). Content that previously passed + calibration may now fail. + task: update_content_standards + schema_ref: "content-standards/update-content-standards-request.json" + response_schema_ref: "content-standards/update-content-standards-response.json" + doc_ref: "/governance/content-standards/tasks/update_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return the updated content standard: + - Updated policy with stricter alt text rule + - Confirmation of changes applied + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + standards_id: "$context.content_standards_id" + policies: + - policy_id: no_violent_imagery + policy_categories: [brand_safety] + enforcement: must + policy: "No violent or controversial imagery" + - policy_id: min_display_dpi + policy_categories: [imagery_quality] + enforcement: should + channels: [display] + policy: "Minimum 72 DPI for display assets" + - policy_id: brand_color_tolerance + policy_categories: [brand_compliance] + enforcement: must + policy: "Brand colors must match palette within 5% tolerance" + - policy_id: image_alt_text + policy_categories: [accessibility] + enforcement: must + policy: "All images must have alt text" + - policy_id: no_stock_photography + policy_categories: [brand_compliance] + enforcement: must + policy: "No stock photography" + + idempotency_key: "$generate:uuid_v4#content_standards_standards_version_change_update_stricter_standards" + validations: + - check: response_schema + description: "Response matches update-content-standards-response.json schema" + + - id: calibrate_after_policy_change + title: "Re-calibrate content against updated standards" + narrative: | + The buyer re-calibrates the same sample content against the updated standards. + If the agent is properly applying the current policy version, content without + alt text or using stock photography should now fail. + task: calibrate_content + schema_ref: "content-standards/calibrate-content-request.json" + response_schema_ref: "content-standards/calibrate-content-response.json" + doc_ref: "/governance/content-standards/tasks/calibrate_content" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return calibration results reflecting the updated policy: + - verdict reflects evaluation against current (not cached) standards + - Rule results reference the updated policy rules + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + standards_id: "$context.content_standards_id" + artifact: + property_rid: "test-publisher.example" + artifact_id: "display_stock_photo_300x250" + description: "Display ad using stock photography without alt text" + assets: + - type: "image" + url: "https://cdn.pinnacle-agency.example/stock-photo-300x250.png" + width: 300 + height: 250 + + idempotency_key: "$generate:uuid_v4#content_standards_standards_version_change_calibrate_after_policy_change" + context: + correlation_id: "content_standards--calibrate_after_policy_change" + validations: + - check: response_schema + description: "Response matches calibrate-content-response.json schema" + - check: field_value + path: "verdict" + value: "fail" + description: "Content violating updated must-rules receives fail verdict" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "content_standards--calibrate_after_policy_change" + description: "Context correlation_id returned unchanged" + - id: delivery_validation + title: "Validate delivered content" + narrative: | + After the campaign runs, the buyer validates that the delivered creatives met the + content standards. The governance agent checks actual delivered content against + the rules and flags any violations. + + steps: + - id: validate_content_delivery + title: "Validate content delivery compliance" + narrative: | + The buyer submits delivery data with creative references. The governance agent + evaluates the delivered content against the content standards and returns + a compliance report. + task: validate_content_delivery + schema_ref: "content-standards/validate-content-delivery-request.json" + response_schema_ref: "content-standards/validate-content-delivery-response.json" + doc_ref: "/governance/content-standards/tasks/validate_content_delivery" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return validation results: + - compliant: boolean overall status + - Per-creative compliance status + - Violations with rule references and evidence + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + standards_id: "$context.content_standards_id" + records: + - record_id: "delivery_display_001" + artifact: + property_rid: "test-publisher.example" + artifact_id: "display_trail_pro_300x250" + assets: + - type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + width: 300 + height: 250 + - record_id: "delivery_video_001" + artifact: + property_rid: "test-publisher.example" + artifact_id: "video_30s_trail_pro" + assets: + - type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + duration_ms: 30000 + + context: + correlation_id: "content_standards--validate_content_delivery" + validations: + - check: response_schema + description: "Response matches validate-content-delivery-response.json schema" + - check: field_present + path: "summary" + description: "Response includes validation summary with pass/fail counts" + - check: field_present + path: "results" + description: "Response includes per-record validation results" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "content_standards--validate_content_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/creative-ad-server/index.yaml b/dist/compliance/3.0.1/specialisms/creative-ad-server/index.yaml new file mode 100644 index 0000000000..2d0e2b3c9a --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/creative-ad-server/index.yaml @@ -0,0 +1,383 @@ +id: creative_ad_server +version: "1.1.0" +title: "Creative ad server" +protocol: creative +category: creative_ad_server +summary: "Stateful ad server with pre-loaded creatives. Generates serving tags per media buy. Optionally bills through AdCP." +track: creative +required_tools: + - build_creative + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. approved → processing on a creative asset. +invariants: + - status.monotonic + +narrative: | + You run a creative ad server — think Innovid, Flashtalking, or CM360. Your clients + have already uploaded their creatives through your UI. Buyers connect to browse your + creative library and request serving tags for their media buys. + + Your agent is stateful: creatives already exist in your system. The buyer never pushes + assets to you. Instead, they browse your library, pick creatives, and ask you to generate + tags for specific placements. For a campaign with 25 media buys, you'll generate 25 tags. + + Billing is optional. If you bill through AdCP, the buyer's account drives the + rate card: list_creatives surfaces pricing_options, build_creative returns the + applied pricing_option_id and vendor_cost, and report_usage closes the loop + after delivery. If you bill out of band (flat license, SaaS contract, bundled + enterprise agreement — CM360 is the canonical example), omit the pricing fields + and surface report_usage records as not accepted rather than fake-accepting. + response_schema validates shape in both cases; the specialism doesn't force a + billing business model on you. + + This storyboard walks through that flow from the buyer's perspective. + +agent: + interaction_model: stateful_preloaded + capabilities: + - has_creative_library + examples: + - "Innovid" + - "Flashtalking" + - "CM360" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + Creatives must already exist in the ad server's library, loaded through the + platform's own UI or API. The buyer does not push assets — they browse and + request tags for what's already there. + + Pricing is optional. Ad servers that bill through AdCP (rate-carded creative + serving) expose pricing_options on creatives and pricing_option_id on build + responses. Ad servers that bill out of band (flat license, SaaS contract, or + bundled enterprise agreement — e.g. CM360) return creatives and tags without + those fields. Both shapes are conformant; response_schema validates shape when + the fields are present. + controller_seeding: true + +fixtures: + creatives: + - creative_id: "campaign_hero_video" + status: "approved" + format_id: + id: "vast_30s" + pricing_options: + - pricing_option_id: "po_vast_30s_cpm" + pricing_model: "cpm" + rate: 0.50 + currency: "USD" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports creative operations before browsing or building creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring creative in supported_protocols, confirming the agent handles creative operations. + sample_request: + context: + correlation_id: "creative_ad_server--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: "creative_ad_server--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: browse_library + title: "Browse the creative library" + narrative: | + The buyer connects to your ad server and wants to see what creatives are + available for a campaign. They call list_creatives to browse concepts and + individual creatives in your library. + + If your agent bills through AdCP and the buyer provides an account, each + creative includes the applicable rate from the account's rate card. If you + bill out of band, pricing_options is omitted. + + steps: + - id: list_creatives + title: "Browse available creatives" + narrative: | + The buyer asks: "What creatives do you have for this advertiser?" This is + the primary entry point for ad server interactions. + + The buyer sends include_pricing=true and an account reference. If your + agent bills through AdCP, each creative returns pricing_options from the + account's rate card; vendors may offer multiple options (volume tiers, + context-specific rates, or different models per product line). If your + agent bills out of band (flat license, SaaS contract, or bundled + enterprise agreement), omit pricing_options — response_schema validates + shape either way. + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_flow + stateful: true + expected: | + Return creatives from your library. Each creative should include: + - creative_id (your platform's identifier) + - format_id referencing the creative's format + - name and status (approved, pending_review, rejected) + - concept_id grouping related creatives across sizes + - pricing_options array with pricing_option_id, model, rate, currency — + only when your agent bills through AdCP and include_pricing=true with + an account provided. Agents that bill out of band omit this field. + + sample_request: + account: + account_id: "acct_acme_creative" + include_pricing: true + filters: + statuses: + - "approved" + + context: + correlation_id: "creative_ad_server--list_creatives" + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema. Validates pricing_options shape when present; absence is conformant for agents that bill out of band." + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_ad_server--list_creatives" + description: "Context correlation_id returned unchanged" + - id: list_output_formats + title: "Check available output formats" + narrative: | + The buyer checks what output formats your ad server supports. For an ad + server, this is typically the tag formats you can generate — HTML, JavaScript, + VAST, native. The input formats are less relevant because creatives are already + in your system. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_sync + stateful: false + expected: | + Return the output formats you support. For an ad server, these are typically + tag formats (HTML, JavaScript, VAST) rather than visual ad formats. + + - id: generate_tags + title: "Generate serving tags" + narrative: | + The buyer has selected creatives from your library and now needs serving tags + for their media buys. They call build_creative for each media buy/package + combination, passing the creative_id, account, and the context needed to + generate the right tag. + + If your agent bills through AdCP, the response carries pricing_option_id and + vendor_cost (zero at build time for CPM — cost accrues on impressions). If + your agent bills out of band, omit these fields; the manifest is the + required output. + + steps: + - id: build_tag + title: "Generate a tag for a media buy" + narrative: | + The buyer requests a serving tag for a specific creative, format, and media + buy. Your ad server generates a tag scoped to that placement. + + For Innovid, this produces a VAST tag for a CTV placement. For Flashtalking, + this might produce an HTML tag for a display placement. The media_buy_id and + package_id provide the trafficking context. + + Agents that bill through AdCP return pricing_option_id and vendor_cost so + the buyer knows which rate applies. Agents that bill out of band omit + those fields. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a creative manifest with the serving tag. The output should include: + - An HTML, JavaScript, or VAST asset containing the tag + - The format_id matching the target format + - Macro placeholders (CLICK_URL, CACHEBUSTER) if applicable + - pricing_option_id, vendor_cost, and currency — only when your agent + bills through AdCP. Agents billing out of band omit these fields. + + sample_request: + creative_id: "campaign_hero_video" + account: + account_id: "acct_acme_creative" + target_format_id: + agent_url: "https://your-ad-server.example.com" + id: "vast_30s" + media_buy_id: "mb_summer_campaign_001" + package_id: "pkg_ctv_premium" + + idempotency_key: "$generate:uuid_v4#creative_ad_server_generate_tags_build_tag" + context: + correlation_id: "creative_ad_server--build_tag" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.assets" + description: "Output includes a serving tag asset" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_ad_server--build_tag" + description: "Context correlation_id returned unchanged" + - id: track_delivery + title: "Track creative delivery" + narrative: | + After the campaign runs, the buyer checks how each creative performed. They call + get_creative_delivery to get variant-level delivery data — impressions, spend, and + breakdowns by creative variant. + + steps: + - id: get_delivery + title: "Get creative delivery metrics" + narrative: | + The buyer asks: "How did my creatives perform across the media buys?" Your ad + server returns delivery data per creative, including impressions, spend, and + variant-level breakdowns. + task: get_creative_delivery + schema_ref: "creative/get-creative-delivery-request.json" + response_schema_ref: "creative/get-creative-delivery-response.json" + doc_ref: "/creative/task-reference/get_creative_delivery" + comply_scenario: creative_flow + stateful: true + expected: | + Return per-creative delivery metrics including: + - Impressions and spend per creative + - Variant-level breakdowns (which version of each creative was served) + - Media buy context (which buys each creative was active on) + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "mb_summer_campaign_001" + + context: + correlation_id: "creative_ad_server--get_delivery" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches get-creative-delivery-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_ad_server--get_delivery" + description: "Context correlation_id returned unchanged" + - id: report_billing + title: "Report usage for billing" + narrative: | + After delivery, the buyer reports creative usage to the ad server. The billing + path is optional at the specialism level — agents that bill through AdCP + accept the record and reconcile against their rate card; agents that bill + out of band return accepted: 0 with an entry in the errors array pointing at + the offending usage record and a message explaining that billing is handled + out of band for this account. response_schema validates the response shape + either way. + + steps: + - id: report_usage + title: "Report impressions and cost" + narrative: | + The buyer sends a usage report covering a billing period. Each record + includes the creative_id, pricing_option_id (from the build_creative + response — omitted by agents that bill out of band), impressions served, + and the computed vendor_cost. The sample_request below is written for + the AdCP-billed case; out-of-band agents receive the same request shape + and decide the response. + + Agents that bill through AdCP verify the rate and return accepted: 1 + with empty errors. Agents that bill out of band return accepted: 0 and + populate errors with a field pointing at the offending record (e.g. + `usage[0].pricing_option_id`) and a human-readable message. Don't + fake-accept records you won't bill on — silent acceptance breaks + reconciliation for buyers who trust the response. A standard error + code for "billing is handled out of band" is not yet defined in the + spec; vendor codes are fine today. + task: report_usage + schema_ref: "account/report-usage-request.json" + response_schema_ref: "account/report-usage-response.json" + doc_ref: "/accounts/tasks/report_usage" + comply_scenario: creative_flow + stateful: true + expected: | + Return a response matching the report-usage-response.json schema. + Agents that bill through AdCP return accepted: 1 and empty errors. + Agents that bill out of band return accepted: 0 with an errors entry + pointing at the offending record and explaining that billing is + handled out of band. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + reporting_period: + start: "2026-03-01T00:00:00Z" + end: "2026-03-31T23:59:59Z" + usage: + - account: + account_id: "acct_acme_creative" + creative_id: "campaign_hero_video" + pricing_option_id: "po_vast_30s_cpm" + impressions: 2400000 + vendor_cost: 1200.00 + currency: "USD" + + context: + correlation_id: "creative_ad_server--report_usage" + validations: + - check: response_schema + description: "Response matches report-usage-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_ad_server--report_usage" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/creative-generative/generative-seller.yaml b/dist/compliance/3.0.1/specialisms/creative-generative/generative-seller.yaml new file mode 100644 index 0000000000..d0a4cf69db --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/creative-generative/generative-seller.yaml @@ -0,0 +1,758 @@ +id: creative_generative/seller +version: "1.0.0" +title: "Generative seller agent" +category: creative_generative +summary: "Seller agent that generates creatives from briefs at buy time — no pre-built assets required." +track: media_buy +required_tools: + - get_products + - create_media_buy + - list_creative_formats + - sync_creatives + - get_media_buy_delivery +requires_scenarios: + - media_buy_seller/refine_products + - media_buy_seller/delivery_reporting + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. active → pending_creatives on a media_buy or +# approved → processing on a creative asset. +invariants: + - status.monotonic + +narrative: | + You run a generative sell-side platform — an AI ad network, generative DSP, or any system that + both sells inventory and generates creatives from a brief. The buyer doesn't upload finished + assets. Instead, they describe what they want via a creative brief, point you at a brand.json, + and your platform produces finished creatives ready for delivery. + + This is the media buy seller flow with generative creative capabilities. Your platform handles + the full lifecycle from brief to reporting, but the creative sync step accepts briefs instead + of static assets. Your formats declare what brief inputs they accept, and your platform + generates the creative — potentially asynchronously. + + A programmatic seller with generative capabilities should also support standard IAB formats + (display, video, VAST, etc.) for buyers who bring their own assets. A platform that sells + programmatic inventory but can't accept a pre-built VAST tag or display banner is incoherent. + The generative capability is additive to standard programmatic creative acceptance. + + This storyboard focuses on the generative creative delta. Governance agent registration and + proposal refinement work identically to the base media_buy_seller storyboard — see that + storyboard for those phases. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - supports_generation + - supports_guaranteed + - supports_non_guaranteed + examples: + - "OpenAds" + - "AI ad networks" + - "Generative DSPs" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity hosted at the brand's domain (brand.json) or resolvable + via AgenticAdvertising.org. The brand.json provides visual identity — logos, colors, fonts, + tone — that the generative seller uses to produce on-brand creatives. The test kit provides + a sample brand with campaign parameters. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "outdoor_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "outdoor_display_q2" + pricing_option_id: "cpm_guaranteed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "creative_generative--seller--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: "creative_generative--seller--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account setup" + narrative: | + Before buying anything, the buyer establishes an account relationship with your platform. + This is the same handshake as any media buy seller — the buyer identifies the brand and + operator, and you provision the account. + + steps: + - id: sync_accounts + title: "Establish account relationship" + narrative: | + The buyer registers their brand and operator with your platform. Your platform should + resolve the brand via AgenticAdvertising.org to pull brand identity for creative + generation. If the brand domain doesn't resolve, return an error — don't generate + creatives for unknown brands. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with: + - account_id: your platform's identifier for this relationship + - action: created or updated + - status: active (instant approval) or pending_approval (requires human review) + - account_scope: operator, brand, operator_brand, or agent + - setup: URL and message if pending_approval + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#creative_generative_seller_account_setup_sync_accounts" + context: + correlation_id: "creative_generative--seller--sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + - check: field_present + path: "accounts[0].status" + description: "Account has a status (active or pending_approval)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--sync_accounts" + description: "Context correlation_id returned unchanged" + - id: format_discovery + title: "Format discovery" + narrative: | + The buyer discovers what your platform can accept. A generative seller's format catalog + has two parts: generative formats that accept briefs as inputs, and standard IAB formats + that accept pre-built assets. + + Generative formats declare brief asset slots. Standard formats declare image, video, or + HTML asset slots. The buyer uses this to decide whether to send a brief or upload finished + assets for each package. + + A platform selling programmatic inventory should support both. Generative-only is fine for + a pure creative agent, but a seller that takes media buys needs to accept pre-built assets + from buyers who already have creatives. + + steps: + - id: list_formats + title: "Discover creative formats" + narrative: | + The buyer asks what formats your platform supports. Your response should include both + generative formats (accepting brief inputs) and standard IAB formats (accepting image, + video, VAST, etc.). + + A programmatic seller that only returns generative formats means the buyer can never + bring their own assets. If you sell inventory, you should accept pre-built creatives + too — the generative capability is additive. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return your format catalog. It should include: + + Generative formats: + - format_id with your agent_url and a unique id (e.g., "display_300x250_generative") + - Asset slots accepting brief asset types + - Render dimensions for the output + - Description indicating this is a generative format + + Standard formats: + - Standard IAB display formats (300x250, 728x90, etc.) + - Video formats (VAST, pre-roll, etc.) if applicable + - Asset slots accepting image, video, html, etc. + + Both types should be present for a programmatic seller. Buyers who already + have creatives need to be able to upload them directly. + + sample_request: + context: + correlation_id: "creative_generative--seller--list_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains a formats array" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Each format has a format_id with agent_url" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--list_formats" + description: "Context correlation_id returned unchanged" + - id: product_discovery + title: "Product discovery" + narrative: | + The buyer sends a natural-language brief describing what they want to buy. Your platform + interprets the brief against your inventory and returns products with pricing, delivery + forecasts, and creative format requirements. + + Products from a generative seller should reference generative format IDs — telling the + buyer that this product accepts a brief rather than requiring pre-built assets. Products + may also reference standard formats for buyers who prefer to supply their own creatives. + + steps: + - id: get_products_brief + title: "Send a brief" + narrative: | + The buyer describes what they want. Your platform returns products. Each product's + creative_format_ids should reference formats from the catalog — some generative, + some standard. The buyer uses this to decide the creative approach per product. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. Each product should include: + - product_id: unique identifier + - name and description + - delivery_type: guaranteed or non_guaranteed + - pricing_models: available pricing options + - forecast: estimated impressions, reach + - creative_format_ids: formats this product accepts (including generative formats) + + sample_request: + buying_mode: "brief" + brief: "Premium display and video inventory on outdoor lifestyle content. Q2 flight, $50K budget. Adults 25-54, US. We want your platform to generate the creatives from our brand brief." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "creative_generative--seller--get_products_brief" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + - check: field_present + path: "products[0].product_id" + description: "Each product has a product_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--get_products_brief" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: create_buy + title: "Create the media buy" + narrative: | + The buyer commits to specific products with budgets and flight dates. This is the same + create_media_buy flow as any seller — the generative aspect doesn't change the buy + creation. The buy may return pending_creatives status, indicating the buyer needs to + sync creatives (via brief) before the campaign can go live. + + steps: + - id: create_media_buy + title: "Create a media buy" + narrative: | + The buyer commits to products. The response may include pending_creatives status + for packages that require creative sync before activation. + 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: | + Process the media buy request and return one of: + - completed: buy confirmed, packages may be pending_creatives + - working: platform is processing (poll for status) + - submitted: long-running async (approval workflow, IO signing) + - input-required: need more information + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "outdoor_display_q2" + budget: 30000 + pricing_option_id: "cpm_guaranteed" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + push_notification_config: + url: "https://buyer.example/webhooks/adcp" + authentication: + schemes: + - "HMAC-SHA256" + credentials: "creative-generative-seller-webhook-secret-token" + + idempotency_key: "$generate:uuid_v4#creative_generative_seller_create_buy_create_media_buy" + context: + correlation_id: "creative_generative--seller--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: creative_brief_sync + title: "Creative brief sync" + narrative: | + This is where a generative seller diverges from the standard flow. Instead of the buyer + uploading finished assets, they send a creative brief through sync_creatives. The brief + describes campaign messaging, tone, audience, and compliance requirements. The format_id + points to a generative format that accepts brief asset types. + + Your platform resolves the brand via AgenticAdvertising.org (using the account's brand + domain), pulls brand.json for visual identity, and generates the creative. This may be + synchronous (accepted immediately) or asynchronous (pending_review while generation + completes). The buyer polls or waits for a webhook to know when the creative is ready. + + steps: + - id: sync_creatives_brief + title: "Send creative brief via sync_creatives" + narrative: | + The buyer sends a creative brief through sync_creatives, using a generative format_id + that accepts brief asset types. The account's brand domain tells your platform where + to resolve brand identity for generation. + + Your platform should actually read the operator and brand from the request — not + ignore them. If the brand domain is invalid or unresolvable, return a rejection + with a clear error rather than generating with defaults. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept the creative brief and begin generation: + - Per-creative action: created + - Per-creative status: pending_review (generation in progress) or accepted (instant) + - The brand domain from the account should be resolved via AgenticAdvertising.org + - If the brand domain is invalid, reject with a clear error + - If the brief references a non-generative format, reject with format mismatch error + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "gen_display_summer_sale" + name: "Summer Sale - Generated Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250_generative" + assets: + brief: + asset_type: "brief" + name: "Acme Outdoor Summer Sale Q2" + objective: "conversion" + tone: "Bold, adventurous, urgent" + audience: "Active adults 25-54, outdoor enthusiasts" + territory: "Summer gear clearance" + messaging: + headline: "Summer Sale — 40% Off All Gear" + tagline: "Gear up for less" + cta: "Shop Now" + key_messages: + - "40% off all trail running and hiking gear" + - "Free shipping on orders over $75" + compliance: + required_disclosures: + - text: "Sale ends June 30, 2026. Exclusions apply." + position: "footer" + jurisdictions: ["US"] + prohibited_claims: + - "Best price guaranteed" + - creative_id: "gen_video_summer_sale" + name: "Summer Sale - Generated Video 30s" + format_id: + agent_url: "https://your-platform.example.com" + id: "video_30s_generative" + assets: + brief: + asset_type: "brief" + name: "Acme Outdoor Summer Sale Q2 - Video" + objective: "awareness" + tone: "Cinematic, energetic" + audience: "Active adults 25-54" + territory: "Summer adventure" + messaging: + headline: "Your Next Adventure Starts Here" + tagline: "Acme Outdoor — Gear up for less" + cta: "Shop the Sale" + key_messages: + - "40% off all gear this summer" + compliance: + required_disclosures: + - text: "Sale ends June 30, 2026. Exclusions apply." + position: "audio" + jurisdictions: ["US"] + assignments: + - creative_id: "gen_display_summer_sale" + package_id: "outdoor_display_q2" + - creative_id: "gen_video_summer_sale" + package_id: "outdoor_video_q2" + + idempotency_key: "$generate:uuid_v4#creative_generative_seller_creative_brief_sync_sync_creatives_brief" + context: + correlation_id: "creative_generative--seller--sync_creatives_brief" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + - check: field_present + path: "creatives[0].action" + description: "Each creative has a status (pending_review or accepted)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--sync_creatives_brief" + description: "Context correlation_id returned unchanged" + - id: sync_creatives_standard + title: "Sync standard creatives alongside generative" + narrative: | + The buyer sends a pre-built creative using a standard format — verifying that this + programmatic seller also accepts traditional asset uploads alongside generative briefs. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept the standard creative: + - Per-creative action: created + - Per-creative status: accepted or pending_review + - Standard asset validation (dimensions, file size, mime type) + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "static_display_300x250" + name: "Trail Pro 3000 - Pre-built Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + format: "png" + width: 300 + height: 250 + + idempotency_key: "$generate:uuid_v4#creative_generative_seller_creative_brief_sync_sync_creatives_standard" + context: + correlation_id: "creative_generative--seller--sync_creatives_standard" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Standard creative has an action" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--sync_creatives_standard" + description: "Context correlation_id returned unchanged" + - id: sync_creatives_invalid_brand + title: "Reject brief with invalid brand" + narrative: | + The buyer sends a brief referencing a brand domain that doesn't exist in + AgenticAdvertising.org. The seller should reject the creative rather than + generating with unknown brand identity. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Reject the creative with a clear error: + - Per-creative status: rejected + - Validation error indicating the brand domain could not be resolved + - The seller should not generate creatives for unknown brands + + sample_request: + account: + brand: + domain: "nonexistent-brand-xyz.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "gen_invalid_brand" + name: "Invalid Brand Test" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250_generative" + assets: + brief: + asset_type: "brief" + name: "Test brief" + objective: "awareness" + tone: "Neutral" + messaging: + headline: "Test" + cta: "Click" + + idempotency_key: "$generate:uuid_v4#creative_generative_seller_creative_brief_sync_sync_creatives_invalid_brand" + context: + correlation_id: "creative_generative--seller--sync_creatives_invalid_brand" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Creative has a status (expected: rejected)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--sync_creatives_invalid_brand" + description: "Context correlation_id returned unchanged" + - id: creative_generation_lifecycle + title: "Creative generation lifecycle" + narrative: | + If creative generation is asynchronous, the buyer monitors progress. The seller + transitions creatives through processing → pending_review → approved as generation + completes. The comply_test_controller can force these transitions for deterministic + testing via force_creative_status. + + The buyer polls by re-calling sync_creatives with the same creatives. Because + sync_creatives has upsert semantics, re-sending the same brief is idempotent — the + seller returns the current status without restarting generation. + + steps: + - id: check_creative_status + title: "Poll creative generation status" + narrative: | + The buyer re-sends the same creatives via sync_creatives. Because sync has upsert + semantics, this is idempotent — the seller returns updated statuses without + restarting generation. If the creative was pending_review after the initial sync, + it should transition to approved once generation completes. In testing, the + comply_test_controller forces this transition via force_creative_status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Return updated creative statuses: + - Generative creatives should transition from pending_review to approved + - Approved creatives should include generated assets (image URLs, VAST tags, etc.) + - The generated output should reflect the brand identity from the original brief + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "gen_display_summer_sale" + name: "Summer Sale - Generated Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250_generative" + assets: + brief: + asset_type: "brief" + name: "Acme Outdoor Summer Sale Q2" + objective: "conversion" + tone: "Bold, adventurous, urgent" + messaging: + headline: "Summer Sale — 40% Off All Gear" + cta: "Shop Now" + - creative_id: "gen_video_summer_sale" + name: "Summer Sale - Generated Video 30s" + format_id: + agent_url: "https://your-platform.example.com" + id: "video_30s_generative" + assets: + brief: + asset_type: "brief" + name: "Acme Outdoor Summer Sale Q2 - Video" + objective: "awareness" + tone: "Cinematic, energetic" + messaging: + headline: "Your Next Adventure Starts Here" + cta: "Shop the Sale" + + idempotency_key: "$generate:uuid_v4#creative_generative_seller_creative_generation_lifecycle_check_creative_status" + context: + correlation_id: "creative_generative--seller--check_creative_status" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has a current status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--check_creative_status" + description: "Context correlation_id returned unchanged" + - id: delivery_monitoring + title: "Delivery and reporting" + narrative: | + The campaign is live. Delivery monitoring is identical to any media buy seller — + the generative aspect doesn't change how delivery is reported. The buyer monitors + impressions, clicks, spend, and pacing. + + steps: + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data for the active media buy. Reporting is the same + regardless of whether creatives were generated or uploaded. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics for the media buy: + - Per-package delivery: impressions, clicks, spend, completion rates + - Daily breakdown if requested + - Pacing information + - Budget utilization + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "creative_generative--seller--get_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--seller--get_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/creative-generative/index.yaml b/dist/compliance/3.0.1/specialisms/creative-generative/index.yaml new file mode 100644 index 0000000000..299dc1d5cb --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/creative-generative/index.yaml @@ -0,0 +1,746 @@ +id: creative_generative +version: "1.0.0" +title: "Generative creative agent" +protocol: creative +category: creative_generative +summary: "Agent that takes a brief and generates finished creatives from scratch — no input assets required." +required_tools: + - build_creative +# Note: sync_catalogs is NOT in required_tools. The catalog_augmented_generation +# phase below is additive — brief-only generative agents (pure-prompt DSPs) pass +# the other phases without claiming catalog support. Agents that do not implement +# sync_catalogs grade that phase not_applicable rather than fail the specialism. + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. approved → processing on a creative asset. +invariants: + - status.monotonic + +narrative: | + You run a generative creative platform — an AI ad network, a generative DSP, or any system + that creates ad creatives from a natural-language brief and brand identity. The buyer doesn't + push assets to you. Instead, they describe what they want, point you at a brand.json, and + your agent produces finished creatives ready for trafficking. + + This is fundamentally different from template-based transformation (Celtra) or library-based + retrieval (Innovid). Your agent creates something new. The buyer sends a brief, optionally + with seed assets or constraints, and your platform generates creatives — potentially in + multiple formats at once. + + This storyboard walks through the generation flow: format discovery, brief-driven generation, + multi-format builds, iterative refinement, and quality progression from draft to production. + +agent: + interaction_model: stateless_generate + capabilities: + - supports_generation + examples: + - "OpenAds" + - "AI ad networks" + - "Generative DSPs" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity (brand.json at the brand's domain) for the agent + to resolve visual identity — logos, colors, fonts, tone. The test kit provides a + sample brand with campaign parameters. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports creative operations before browsing or building creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring creative in supported_protocols, confirming the agent handles creative operations. + sample_request: + context: + correlation_id: "creative_generative--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: "creative_generative--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: format_discovery + title: "Discover generative formats" + narrative: | + The buyer needs to know what your agent can generate. Unlike a template platform + where formats are fixed dimensions, generative formats describe what the agent + produces — the output type, constraints, and what inputs it accepts. A generative + format might be "display_300x250_generative" that accepts a brief and produces + a finished banner, or "social_post_generative" that creates platform-native content. + + steps: + - id: discover_formats + title: "Discover available generative formats" + narrative: | + The buyer asks: "What can you generate?" Your platform returns the formats + you support. Each format describes the output type, dimensions, and what + inputs it needs (brief text, brand reference, seed images, etc.). + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_sync + stateful: false + expected: | + Return your generative formats. Each format should include: + - format_id with your agent_url and a unique id + - Human-readable name and description + - Asset slots describing what inputs are accepted (brief text, images, etc.) + - Render dimensions for the output + - Variables for any dynamic fields the buyer can control + + sample_request: + context: + correlation_id: "creative_generative--discover_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains a formats array" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Each format has a format_id with agent_url" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--discover_formats" + description: "Context correlation_id returned unchanged" + - id: generate_from_brief + title: "Generate from brief" + narrative: | + The buyer describes what they want in natural language and your agent generates + a creative from scratch. The brief includes the campaign message, target audience + context, and a brand reference so your agent can resolve visual identity. + + This is the core generative flow. The buyer doesn't provide finished assets — + they provide intent, and your agent creates. + + steps: + - id: build_draft + title: "Generate a draft creative from brief" + narrative: | + The buyer sends a brief describing the campaign and a target format. Your + agent generates a draft creative — fast, lower-fidelity output the buyer can + review before committing to a production build. + + The brief is passed as a message alongside the target format. The brand + reference lets your agent resolve brand.json for visual identity. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return a generated creative manifest: + - creative_manifest with the generated assets (images, copy, serving code) + - format_id matching the target format + - If include_preview is true, include a preview render + + The output should be a coherent creative that reflects the brief and brand + identity — not a template with placeholder text. + + sample_request: + message: "Create a display banner for a summer outdoor gear sale. Bold, adventurous tone. Headline should emphasize 40% off. Target audience: active adults 25-54." + target_format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + quality: "draft" + include_preview: true + + idempotency_key: "$generate:uuid_v4#creative_generative_generate_from_brief_build_draft" + context: + correlation_id: "creative_generative--build_draft" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.assets" + description: "Output manifest includes generated assets" + - check: field_present + path: "creative_manifest.format_id" + description: "Output manifest includes format_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--build_draft" + description: "Context correlation_id returned unchanged" + - id: refine + title: "Refine the creative" + narrative: | + The buyer reviews the draft and wants changes. They send the generated manifest + back with refinement instructions. Your agent modifies the creative based on the + feedback — adjusting copy, swapping imagery, or changing the layout. + + This is iterative: the buyer can refine multiple times until they're satisfied, + then request a production-quality build. + + steps: + - id: refine_creative + title: "Refine with feedback" + narrative: | + The buyer passes the generated manifest back with a message describing what + to change. Your agent applies the refinements and returns an updated creative. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return a refined creative manifest reflecting the requested changes. + The output should preserve what worked in the original while applying + the refinements. + + sample_request: + message: "Make the headline larger. Replace the mountain imagery with a trail running scene. Add a CTA button that says 'Shop Now'." + creative_manifest: + format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + assets: + generated_image: + asset_type: "image" + url: "https://your-agent.example.com/generated/abc123.jpg" + width: 300 + height: 250 + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + target_format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + quality: "draft" + include_preview: true + + idempotency_key: "$generate:uuid_v4#creative_generative_refine_refine_creative" + context: + correlation_id: "creative_generative--refine_creative" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.assets" + description: "Refined manifest includes assets" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--refine_creative" + description: "Context correlation_id returned unchanged" + - id: multi_format + title: "Multi-format generation" + narrative: | + The buyer needs the creative in multiple sizes. Instead of generating each + format separately, they pass target_format_ids (plural) and your agent produces + all formats in a single call. This is where generative agents shine — adapting + a concept across formats while maintaining visual coherence. + + steps: + - id: build_multi_format + title: "Generate for multiple formats" + narrative: | + The buyer passes the refined manifest with multiple target formats. Your + agent generates a creative for each format, adapting layout, copy, and + imagery to fit each size while maintaining brand consistency. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return creative manifests for each requested format in creative_manifests + (plural). Each manifest should: + - Have a format_id matching one of the target formats + - Contain complete, format-appropriate assets + - Maintain visual coherence across formats + + If a format cannot be produced, include it with an error — don't fail + the entire request. + + sample_request: + message: "Generate production-ready versions for all three sizes." + creative_manifest: + format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + assets: + generated_image: + asset_type: "image" + url: "https://your-agent.example.com/generated/abc123-refined.jpg" + width: 300 + height: 250 + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + cta: + asset_type: "text" + content: "Shop Now" + target_format_ids: + - agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + - agent_url: "https://your-agent.example.com" + id: "display_728x90_generative" + - agent_url: "https://your-agent.example.com" + id: "display_320x50_generative" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + quality: "production" + + idempotency_key: "$generate:uuid_v4#creative_generative_multi_format_build_multi_format" + context: + correlation_id: "creative_generative--build_multi_format" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifests" + description: "Response contains creative_manifests array (plural)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--build_multi_format" + description: "Context correlation_id returned unchanged" + - id: production_build + title: "Production build" + narrative: | + The buyer is satisfied with the concept and needs a final, production-quality + creative ready for trafficking. They switch quality from draft to production. + Your agent generates the finished output with full-fidelity assets. + + steps: + - id: build_production + title: "Build at production quality" + narrative: | + The buyer requests a production-quality build of the approved concept. Your + agent generates the final creative with full-fidelity rendering, polished + assets, and serving code ready for trafficking. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return a production-quality creative manifest: + - Full-fidelity generated assets + - Serving code (HTML, JavaScript, or VAST) ready for trafficking + - Provenance metadata if AI-generated (digital_source_type, ai_tool) + + sample_request: + message: "Final production build. No changes needed." + creative_manifest: + format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + assets: + generated_image: + asset_type: "image" + url: "https://your-agent.example.com/generated/abc123-refined.jpg" + width: 300 + height: 250 + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + cta: + asset_type: "text" + content: "Shop Now" + target_format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + quality: "production" + include_preview: true + + idempotency_key: "$generate:uuid_v4#creative_generative_production_build_build_production" + context: + correlation_id: "creative_generative--build_production" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.assets" + description: "Production manifest includes assets" + - check: field_present + path: "creative_manifest.format_id" + description: "Production manifest includes format_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--build_production" + description: "Context correlation_id returned unchanged" + + - id: catalog_augmented_generation + title: "Catalog-augmented generation" + narrative: | + Generative platforms routinely hydrate catalog items into the generation context: + the buyer pushes a product catalog, and each generated creative references a specific + catalog item (via `{SKU}`, `{GTIN}`, or the catalog-item family) in its impression + trackers and click trackers. This is how generative DSPs produce per-SKU dynamic + creative at scale. + + This phase exercises the catalog-acceptance leg and emits a generative build that + includes catalog-item macros in its tracker URL assets. `preview_creative` is the + natural observation point for the runtime substitution-safety check tracked in + #2638 — when the substitution-observer contract lands, a runner can assert that + the previewed HTML contains percent-encoded catalog-item values per + `docs/creative/universal-macros#substitution-safety-catalog-item-macros`. + + steps: + - id: sync_generation_catalog + title: "Push a product catalog for generation" + narrative: | + The buyer pushes a small inline catalog. Your agent will use these items as the + hydration targets for subsequent generated creatives. + task: sync_catalogs + schema_ref: "media-buy/sync-catalogs-request.json" + response_schema_ref: "media-buy/sync-catalogs-response.json" + doc_ref: "/media-buy/task-reference/sync_catalogs" + stateful: true + expected: | + Return per-catalog results with catalog_id, action, item_count, and + items_approved counts. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + catalogs: + - catalog_id: "acme_generative_source" + type: "product" + content_id_type: "sku" + name: "Acme Outdoor — Generative source feed" + items: + - item_id: "peak_jacket_x" + title: "Peak Jacket X" + description: "Goretex 3L shell, seam-taped, 280g." + url: "https://acmeoutdoor.example/gear/peak-jacket-x" + image_url: "https://cdn.acmeoutdoor.example/gear/peak-jacket-x.jpg" + price: + amount: 449.00 + currency: "USD" + + idempotency_key: "$generate:uuid_v4#creative_generative_catalog_augmented_generation_sync_generation_catalog" + context: + correlation_id: "creative_generative--sync_generation_catalog" + validations: + - check: response_schema + description: "Response matches sync-catalogs-response.json schema" + - check: field_present + path: "catalogs[0].catalog_id" + description: "Catalog has an ID" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--sync_generation_catalog" + description: "Context correlation_id returned unchanged" + + - id: build_catalog_aware_creative + title: "Generate a creative bound to a catalog item" + narrative: | + The buyer asks your agent to build a creative for a specific catalog item. + The generated manifest MUST include tracker URLs with catalog-item macros + (`{SKU}`, `{GTIN}`) so impression-time substitution renders per-SKU output. + + Per #2620, the substitution step MUST percent-encode catalog-item values + against the RFC 3986 unreserved set — but the substitution itself happens at + serve time, not in the build_creative response. Runtime observability is + tracked in #2638. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a creative manifest whose tracker assets include catalog-item + macros ({SKU} or {GTIN}) and whose format references a catalog-capable + format declared by the agent. + + sample_request: + message: "Generate a 300x250 display ad for the Peak Jacket X, using our catalog feed for product fields and tracker URLs." + target_format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + quality: "draft" + include_preview: true + + idempotency_key: "$generate:uuid_v4#creative_generative_catalog_augmented_generation_build_catalog_aware_creative" + context: + correlation_id: "creative_generative--build_catalog_aware_creative" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.format_id" + description: "Generated manifest includes format_id" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_generative--build_catalog_aware_creative" + description: "Context correlation_id returned unchanged" + + - id: catalog_substitution_safety + title: "Catalog-item macro substitution safety" + narrative: | + Per docs/creative/universal-macros#substitution-safety-catalog-item-macros, + sales and creative agents MUST percent-encode catalog-item macro values + such that only RFC 3986 `unreserved` characters remain unescaped before + substituting them into a URL context. Nested macro expansion is prohibited. + + This phase exercises the rule with five canonical attacker-shaped + catalog values drawn from the fixture at + `static/test-vectors/catalog-macro-substitution.json`: + reserved-char breakout, nested-expansion preservation, non-ASCII UTF-8, + CRLF injection, and bidi-override neutralization. Generative pipelines + have a higher risk profile on bidi-override than template pipelines + (LLM-generated copy with embedded user text can round-trip bidi + controls into attribute contexts), so this phase exercises bidi + alongside the baseline vectors. The two remaining fixture vectors — + `mixed-path-and-query-contexts` (requires the same macro in two URL + positions) and `url-scheme-injection-neutralized` (requires an + href-whole-value macro binding) — are unit-test-layer conformance only; + runtime observation in a storyboard requires specialism-specific + template shapes tracked as follow-ups. + + `build_creative`-with-`include_preview: true` is the natural observation + point on a generative specialism — the substitution-observer runner + (#2638) parses the returned `preview_html` and asserts each macro + position carries the fixture's `expected_encoded` form, not the raw + attacker bytes. + + Scope note: this phase validates substitution on the PREVIEW surface + only. Sellers with divergent preview vs impression-time substitution + paths MAY pass here while failing at serve time; serve-time attestation + or log-introspection observability is deferred (#2651, out-of-scope v1 + per the observer contract). + + The `expect_substitution_safe` step is gated on the + `substitution_observer_runner` test-kit contract (see + `test-kits/substitution-observer-runner.yaml`). Runners that do not + advertise the contract grade the step as `not_applicable` — the earlier + `sync_substitution_probe_catalog` and `build_substitution_probe_creative` + steps still run (exercising the catalog-acceptance and build paths), + but the substituted-URL assertion is skipped. + + steps: + - id: sync_substitution_probe_catalog + title: "Push a probe catalog with attacker-shaped values" + narrative: | + Push a small probe catalog whose `sku` fields contain three of the + canonical attacker-shaped values from the substitution-safety + fixture. The agent MUST accept the payload at sync_catalogs — the + encoding rule applies at substitution time, not at ingest — but + subsequent build_creative output must percent-encode each value. + task: sync_catalogs + schema_ref: "media-buy/sync-catalogs-request.json" + response_schema_ref: "media-buy/sync-catalogs-response.json" + doc_ref: "/media-buy/task-reference/sync_catalogs" + stateful: true + expected: | + Catalog accepted with per-item counts. Runner captures the + catalog_id + item_ids for the downstream assertion binding. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + catalogs: + - catalog_id: "creative_generative_substitution_probe_v1" + type: "product" + content_id_type: "sku" + name: "Substitution safety probe" + items: + # item_id names the vector; the `sku` field carries the + # attacker-shaped value that substitution must encode. + - item_id: "reserved_char_breakout" + sku: "00013&cmd=drop" + title: "Reserved-char breakout probe" + url: "https://acmeoutdoor.example/probe/reserved" + image_url: "https://cdn.acmeoutdoor.example/probe/reserved.jpg" + price: { amount: 1.00, currency: "USD" } + - item_id: "nested_expansion" + sku: "vacancy-{DEVICE_ID}-42" + title: "Nested-expansion probe" + url: "https://acmeoutdoor.example/probe/nested" + image_url: "https://cdn.acmeoutdoor.example/probe/nested.jpg" + price: { amount: 1.00, currency: "USD" } + - item_id: "non_ascii" + sku: "café-amsterdam" + title: "Non-ASCII UTF-8 probe" + url: "https://acmeoutdoor.example/probe/non-ascii" + image_url: "https://cdn.acmeoutdoor.example/probe/non-ascii.jpg" + price: { amount: 1.00, currency: "USD" } + - item_id: "crlf_injection" + sku: "abc\r\nHost: evil.example" + title: "CRLF injection probe" + url: "https://acmeoutdoor.example/probe/crlf" + image_url: "https://cdn.acmeoutdoor.example/probe/crlf.jpg" + price: { amount: 1.00, currency: "USD" } + - item_id: "bidi_override" + sku: "VIN-\u202E1234" + title: "Bidi-override probe" + url: "https://acmeoutdoor.example/probe/bidi" + image_url: "https://cdn.acmeoutdoor.example/probe/bidi.jpg" + price: { amount: 1.00, currency: "USD" } + + idempotency_key: "$generate:uuid_v4#creative_generative_catalog_substitution_safety_sync_substitution_probe_catalog" + context: + correlation_id: "creative_generative--sync_substitution_probe_catalog" + validations: + - check: response_schema + description: "Response matches sync-catalogs-response.json schema" + - check: field_present + path: "catalogs[0].catalog_id" + description: "Probe catalog accepted" + + - id: build_substitution_probe_creative + title: "Generate a creative with catalog-item macros bound to the probe" + narrative: | + Generate a draft creative that uses `{SKU}` in its impression tracker + URL and request `include_preview: true` so the substitution-observer + runner has a preview surface to inspect. + + The `message` is a natural-language brief; a generative agent that + ignores the `{SKU}` directive (generates its own macro, inlines an + encoded value, omits the tracker) fails the downstream + `expect_substitution_safe` step with `substitution_binding_missing`. + To keep that failure mode diagnostically clear, this step validates + the creative_manifest contains `{SKU}` unsubstituted in at least + one tracker asset before the observer runs. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Creative manifest returned with preview_html populated. Impression + tracker asset includes {SKU} unsubstituted in its template (to be + resolved at impression time against catalog items). + + sample_request: + message: "Generate a 300x250 display ad for the creative_generative_substitution_probe_v1 catalog. Include the `{SKU}` macro as a literal token in the impression tracker URL — the platform resolves it per-impression against catalog items." + target_format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_generative" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + quality: "draft" + include_preview: true + + idempotency_key: "$generate:uuid_v4#creative_generative_catalog_substitution_safety_build_substitution_probe_creative" + context: + correlation_id: "creative_generative--build_substitution_probe_creative" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.format_id" + description: "Creative manifest returned" + + - id: expect_substitution_safe + title: "Assert substituted tracker URLs percent-encode attacker shapes" + narrative: | + The runner inspects the preview_html from the previous step, extracts + tracker URLs that bind `{SKU}` to a probe catalog item, and asserts + each value is percent-encoded per RFC 3986 (unreserved-whitelist). + Raw-byte leakage fails. Every declared binding MUST be observed — a + generative agent that silently strips `{SKU}` rather than + substituting it fails with `substitution_binding_missing`. + task: expect_substitution_safe + requires_contract: substitution_observer_runner + source: html_inline + source_path: "/creative_manifest/preview_html" + macro_template: "https://track.example/imp?sku={SKU}" + require_every_binding_observed: true + catalog_bindings: + # Each binding: `catalog_item_id` is the item_id in the probe + # catalog; `vector_name` is the fixture entry whose raw_value and + # expected_encoded the runner loads from the unit-test fixture. + - macro: "{SKU}" + catalog_item_id: "reserved_char_breakout" + vector_name: "reserved-character-breakout" + - macro: "{SKU}" + catalog_item_id: "nested_expansion" + vector_name: "nested-expansion-preserved-as-literal" + - macro: "{SKU}" + catalog_item_id: "non_ascii" + vector_name: "non-ascii-utf8-percent-encoding" + - macro: "{SKU}" + catalog_item_id: "crlf_injection" + vector_name: "crlf-injection-neutralized" + - macro: "{SKU}" + catalog_item_id: "bidi_override" + vector_name: "bidi-override-neutralized" diff --git a/dist/compliance/3.0.1/specialisms/creative-template/index.yaml b/dist/compliance/3.0.1/specialisms/creative-template/index.yaml new file mode 100644 index 0000000000..d2a71913da --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/creative-template/index.yaml @@ -0,0 +1,413 @@ +id: creative_template +version: "1.0.0" +title: "Creative template and transformation agent" +protocol: creative +category: creative_template +summary: "Stateless creative agent that takes assets in, applies templates, and produces tags or rendered output." +track: creative +required_tools: + - build_creative + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. approved → processing on a creative asset. +invariants: + - status.monotonic + +narrative: | + You build a creative management or rich media platform — think Celtra, a format + conversion service, or any tool that defines ad templates and transforms input assets + into finished creatives. Your agent is stateless: every call is self-contained. The + caller passes assets inline with each request, and you return the result. There is no + creative library to sync to and no persistent state between calls. + + A buyer agent connects to discover your templates, preview them with real brand assets, + and request fully built creatives for trafficking. This storyboard walks through that + flow step by step. + +agent: + interaction_model: stateless_transform + capabilities: + - supports_transformation + examples: + - "Celtra" + - "Format conversion services" + - "Rich media template platforms" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity (brand.json with colors, fonts, logos) and creative + assets that match the format's requirements (images at the right dimensions, text at + the right lengths). The test kit provides these ingredients so you can test without + assembling them yourself. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports creative operations before browsing or building creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring creative in supported_protocols, confirming the agent handles creative operations. + sample_request: + context: + correlation_id: "creative_template--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: "creative_template--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: format_exposure + title: "Format discovery" + narrative: | + The buyer's first question is: what can your platform do? They call + list_creative_formats to discover your templates — the available ad formats, + what assets each one requires, what dimensions they render at, and what + variables (dynamic fields) they support. + + For a Celtra-like platform, this is where your publisher templates (Yahoo + Lighthouse, custom slider units) and standard templates (300x250 banners, + responsive display) get exposed as AdCP-compliant format definitions. + + steps: + - id: discover_formats + title: "Discover available formats" + narrative: | + A buyer agent asks: "What creative formats do you support?" This is + the entry point for any interaction with your agent. The response tells + the buyer what templates are available, what assets each one needs, and + what the output dimensions will be. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_sync + stateful: false + expected: | + Return your supported formats. Each format must include: + - format_id with your agent_url and a unique id + - Human-readable name and description + - Asset slots with types, roles, and requirements (dimensions, file sizes, mime types) + - Render dimensions (width/height per render) + - Variables array if the format supports dynamic fields (text, colors, images) + + sample_request: + context: + correlation_id: "creative_template--discover_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains a formats array" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Each format has a format_id with agent_url" + - check: field_present + path: "formats[0].assets" + description: "Each format defines its asset requirements" + - check: field_present + path: "formats[0].renders" + description: "Each format defines its render dimensions" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_template--discover_formats" + description: "Context correlation_id returned unchanged" + - id: filter_by_type + title: "Filter formats by type" + narrative: | + The buyer narrows the search. Maybe they only want display formats, or + they need formats that accept image assets at specific dimensions. This + tests that your agent handles filter parameters correctly. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_sync + stateful: false + expected: | + Return only formats matching the filter criteria. If no formats match, + return an empty formats array — not an error. + + sample_request: + type: "display" + max_width: 728 + max_height: 90 + + context: + correlation_id: "creative_template--filter_by_type" + validations: + - check: response_schema + description: "Response matches schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_template--filter_by_type" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - id: preview + title: "Preview with real assets" + narrative: | + The buyer wants to see what the template looks like with actual content. + They pass a complete creative manifest inline — brand identity, assets, + and format reference — and your agent renders a preview. + + This is where the stateless nature matters: the buyer doesn't sync assets + to your library first. Everything needed is in the request. + + To test this, you need compatible "ingredients" — a brand.json and assets + that match your format's requirements. The test kit provides these. + + steps: + - id: preview_creative + title: "Preview a creative" + narrative: | + The buyer has chosen a format from Phase 1 and assembled a manifest with + brand assets. They call preview_creative with the full manifest inline to + see how the template renders with real content. + + For a Celtra-like platform, this is where the template gets populated with + the advertiser's images, copy, and brand colors, and a preview URL or HTML + snippet is returned. + task: preview_creative + schema_ref: "creative/preview-creative-request.json" + response_schema_ref: "creative/preview-creative-response.json" + doc_ref: "/creative/task-reference/preview_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return a preview render. The response should include: + - A preview URL (iframe-embeddable) and/or inline HTML + - Render dimensions matching the format spec + - An expiration timestamp (previews are ephemeral) + + The preview should show the template populated with the provided assets — + not a placeholder or empty template. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + request_type: "single" + creative_manifest: + format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-300x250.jpg" + width: 300 + height: 250 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/summer-sale" + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + output_format: "url" + quality: "draft" + + context: + correlation_id: "creative_template--preview_creative" + validations: + - check: response_schema + description: "Response matches preview-creative-response.json schema" + - check: field_present + path: "previews[0].renders[0].preview_url" + description: "Preview includes a renderable URL" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_template--preview_creative" + description: "Context correlation_id returned unchanged" + - id: build + title: "Full build (transformation)" + narrative: | + Now the buyer needs a finished creative — a serving tag or rendered output they + can traffic in a media buy. They call build_creative with the same inline manifest + and a target format. Your agent applies the template and returns the output. + + This is where the three tag generation models come in: + - Universal tags: a single tag that adapts to any environment + - Single-placement tags: scoped to specific dimensions + - Multi-placement tags: covering multiple sizes in one tag + + The output is a creative manifest with the serving code in an HTML, JavaScript, + or VAST asset — ready for the buyer to traffic. + + steps: + - id: build_creative + title: "Build a creative from assets" + narrative: | + The buyer passes assets inline along with a target format and asks your agent + to produce a finished creative. This is the core transformation: raw assets in, + serving tag out. + + For a Celtra-like platform, this means applying the template, assembling the + rich media unit, and returning an ad tag the buyer can traffic. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return a creative manifest with the output creative. The response should include: + - A creative manifest with serving code (html, javascript, or vast asset) + - The format_id matching the target format + - Assets array with the generated output + + The output should be a valid, traffickable creative — not a preview or placeholder. + + sample_request: + creative_manifest: + format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250" + assets: + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-300x250.jpg" + width: 300 + height: 250 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/summer-sale" + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + target_format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + idempotency_key: "$generate:uuid_v4#creative_template_build_build_creative" + context: + correlation_id: "creative_template--build_creative" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.assets" + description: "Output manifest includes assets" + - check: field_present + path: "creative_manifest.format_id" + description: "Output manifest includes format_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_template--build_creative" + description: "Context correlation_id returned unchanged" + - id: build_multi_format + title: "Build for multiple formats" + narrative: | + The buyer needs the same creative adapted to several sizes — 300x250, 728x90, + and 320x50. Instead of making three separate calls, they pass target_format_ids + (plural) and your agent returns manifests for each. + + This tests the multi-format generation capability that makes template agents + valuable — build once, get all sizes. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return creative manifests for each requested format. If a format cannot be + produced, include it in the response with an error — don't fail the entire + request. + + sample_request: + creative_manifest: + format_id: + agent_url: "https://your-agent.example.com" + id: "source_master" + assets: + image: + asset_type: "image" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-master.jpg" + width: 1200 + height: 628 + click_url: + asset_type: "url" + url: "https://acmeoutdoor.example/summer-sale" + headline: + asset_type: "text" + content: "Summer Sale — 40% Off All Gear" + target_format_ids: + - agent_url: "https://your-agent.example.com" + id: "display_300x250" + - agent_url: "https://your-agent.example.com" + id: "display_728x90" + - agent_url: "https://your-agent.example.com" + id: "display_320x50" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + idempotency_key: "$generate:uuid_v4#creative_template_build_build_multi_format" + context: + correlation_id: "creative_template--build_multi_format" + validations: + - check: response_schema + description: "Response matches schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "creative_template--build_multi_format" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/governance-aware-seller/index.yaml b/dist/compliance/3.0.1/specialisms/governance-aware-seller/index.yaml new file mode 100644 index 0000000000..ce4be2d241 --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/governance-aware-seller/index.yaml @@ -0,0 +1,136 @@ +id: governance_aware_seller +version: "1.0.0" +title: "Governance-aware seller" +protocol: media-buy +category: governance_aware_seller +summary: "Seller agent that composes with a campaign-governance agent on the buyer side — accepts sync_governance, calls check_governance before confirming spend, and propagates governance approvals, conditions, and denials unchanged. Optional claim; pure sellers without governance composition do not claim this specialism and skip the governance scenarios as not_applicable." +track: media_buy +required_tools: + - sync_governance + - create_media_buy + +# Cross-step assertion (adcp#2639): governance-aware sellers are exactly +# the surface this assertion protects. If a seller propagates a denial +# response from its governance agent but then creates the media buy +# anyway, no per-step validation notices — this assertion does. +# Cross-step assertion (adcp#2664): status.monotonic rejects media_buy +# status transitions observed across steps that aren't on the spec +# lifecycle graph — catches an approved-with-conditions buy jumping +# from pending_creatives back to pending_start, or active → pending_creatives. +invariants: + - governance.denial_blocks_mutation + - status.monotonic +requires_scenarios: + - media_buy_seller/governance_approved + - media_buy_seller/governance_conditions + - media_buy_seller/governance_denied + - media_buy_seller/governance_denied_recovery + +narrative: | + Governance composition is an optional seller capability. A seller agent can be + fully spec-compliant without integrating with a buyer-side governance agent — + it accepts `create_media_buy`, validates the request against its own rules + (budget, inventory, creative), and returns success or a seller-scoped error + (`VALIDATION_ERROR`, `INVENTORY_UNAVAILABLE`, `TERMS_REJECTED`, etc.). The + governance handshake — `sync_governance` registration, `check_governance` + consultation before confirming spend, and `GOVERNANCE_DENIED` / governance + condition propagation — is only exercisable by a seller that has opted into + composing with the buyer's governance agent. + + Sellers that want to advertise "I honor the buyer's governance plan" claim + this specialism. The grading exercises the four scenarios that require the + governance handshake: + - `governance_approved` — seller accepts a buy that governance approved and + echoes the `governance_context` token back, + - `governance_conditions` — seller accepts a buy with conditions attached by + governance and propagates them through to the response, + - `governance_denied` — seller refuses a buy that governance denied and + returns `GOVERNANCE_DENIED` with the governance findings, + - `governance_denied_recovery` — seller accepts the corrected retry that + falls within the plan limits after the buyer reads the denial findings. + + Sellers that do not claim this specialism are graded `not_applicable` on + these scenarios rather than failed. This mirrors the pattern used by the + other governance-* specialisms (`governance-spend-authority`, + `governance-delivery-monitor`) and by `measurement-verification` — + cross-cutting capabilities that involve campaign behavior are gated behind + explicit specialism claims instead of being force-included under every + sales-* specialism. (`signed-requests` was previously listed here; it was + reclassified in 3.1 to a universal capability-gated storyboard.) + + Composition means the seller: + - accepts `sync_governance` to register the buyer's governance agent URL and + authority categories, + - calls `check_governance` on the registered agent before confirming a + spend-committing request, + - returns `GOVERNANCE_DENIED` with the governance findings propagated + unchanged when the check denies, + - echoes the `governance_context` token from approvals and surfaces + governance conditions to the buyer, + - does not silently downgrade the denial to a seller-scoped error + (`VALIDATION_ERROR` etc.) — the buyer needs to know the denial came from + its own governance agent to correct and retry. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher seller that integrates sync_governance and check_governance" + - "SSP that honors buyer-side governance plans before confirming bids" + - "Retail media network that refuses buys when the buyer's governance denies" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer) with a registered governance agent" + +prerequisites: + description: | + A governance agent that supports `sync_plans` and `check_governance`, and + a seller agent that supports `sync_governance` and propagates governance + approvals, conditions, and denials unchanged. + + By default, the grading runner uses the training governance agent at + `test-agent.adcontextprotocol.org`. Override with `--governance-agent-url` + to use a custom governance agent that satisfies the campaign-governance + protocol. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "governance_aware_seller--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: "governance_aware_seller--get_capabilities" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/governance-delivery-monitor/index.yaml b/dist/compliance/3.0.1/specialisms/governance-delivery-monitor/index.yaml new file mode 100644 index 0000000000..45e1a2b993 --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/governance-delivery-monitor/index.yaml @@ -0,0 +1,441 @@ +id: governance_delivery_monitor +version: "1.0.0" +title: "Campaign governance — delivery monitoring with drift detection" +protocol: governance +category: governance_delivery_monitor +summary: "Governance agent monitors delivery, detects budget drift past thresholds, and triggers re-evaluation." +track: campaign_governance +required_tools: + - check_governance + +# Cross-step assertion (adcp#2639): mid-flight drift can return a denial +# re-evaluation. The assertion gates any post-denial mutation on the +# affected plan, catching sellers that ignore a mid-campaign denial and +# keep delivering / committing new spend. +# Cross-step assertion (adcp#2664): status.monotonic rejects media_buy +# status transitions observed across steps that aren't on the spec +# lifecycle graph — paused → active → paused is fine, +# active → pending_creatives fails. +invariants: + - governance.denial_blocks_mutation + - status.monotonic + +narrative: | + After a media buy is confirmed with governance approval, the governance agent monitors + delivery. When spend drifts past a reallocation threshold — for example, one line item + is overspending while another is underspending — the governance agent triggers a delivery + phase re-evaluation. + + This storyboard covers the delivery monitoring governance flow: initial approval, + delivery metrics showing drift, governance re-check triggered by drift, and either + re-approval with adjusted conditions or a pause recommendation. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform with governance and delivery reporting" + - "Retail media network with pacing data" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, a governance agent URL, + and an active media buy with delivery data. The governance plan defines a + reallocation threshold that triggers re-evaluation. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "outdoor_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + - product_id: "outdoor_video_q2" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_15s" + pricing_options: + - product_id: "outdoor_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + - product_id: "outdoor_video_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 12.0 + plans: + - plan_id: "gov_acme_delivery_monitor" + brand: + domain: "acmeoutdoor.example" + objectives: "Delivery-phase governance with drift detection and rebalancing" + budget: + total: 40000 + currency: "USD" + reallocation_threshold: 8000 + flight: + start: "2027-01-01T00:00:00Z" + end: "2027-12-31T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "drift_reevaluation" + enforcement: "must" + policy: "Re-evaluate governance if any line item drifts more than 20% from plan." + media_buys: + - media_buy_id: "mb_acme_q2_2026" + status: "active" + budget: + total: 40000 + currency: "USD" + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "governance_delivery_monitor--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: "governance_delivery_monitor--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: plan_registration + title: "Register governance plan with delivery monitoring" + narrative: | + The buyer registers a governance plan that includes delivery monitoring thresholds. + When spend drift exceeds the reallocation threshold, the governance agent triggers + a delivery-phase re-evaluation. + + steps: + - id: sync_plans + title: "Register a governance plan with reallocation threshold" + narrative: | + The buyer's governance agent registers a plan with full spending authority and + a 20% reallocation threshold. If any line item's spend drifts more than 20% + from the planned budget allocation, the governance agent re-evaluates. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: governance_delivery_monitor + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - budget.reallocation_threshold: amount above which reallocation requires re-evaluation + + sample_request: + idempotency_key: "governance-delivery-monitor-sync-plans-v1" + plans: + - plan_id: "gov_acme_delivery_monitor" + brand: + domain: "acmeoutdoor.example" + objectives: "Delivery-phase governance with drift detection and rebalancing" + budget: + total: 40000 + currency: "USD" + reallocation_threshold: 8000 + flight: + start: "2027-01-01T00:00:00Z" + end: "2027-12-31T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "drift_reevaluation" + enforcement: "must" + policy: "Re-evaluate governance if any line item drifts more than 20% from plan." + + context: + correlation_id: "governance_delivery_monitor--sync_plans" + context_outputs: + - name: plan_id + path: 'plans[0].plan_id' + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_delivery_monitor--sync_plans" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "plans[0].plan_id" + description: "Governance agent assigns plan_id — must be echoed in check_governance" + - id: initial_approval + title: "Initial governance check — approved" + narrative: | + The buyer proposes a media buy within authority. The governance agent approves + the buy with the delivery monitoring conditions active. + + steps: + - id: check_governance_approved + title: "Pre-buy governance check (approved)" + narrative: | + The buyer calls check_governance with a media buy within authority. The governance + agent approves and notes that delivery monitoring is active with the 20% drift + threshold. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_delivery_monitor + stateful: true + expected: | + Return an approved governance decision: + - decision: approved + - governance_context: token for the media buy + - monitoring: delivery phase governance is active + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + tool: "create_media_buy" + payload: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + total_budget: 40000 + packages: + - product_id: "sports_ctv_q2" + budget: 20000 + - product_id: "outdoor_video_q2" + budget: 20000 + + context: + correlation_id: "governance_delivery_monitor--check_governance_approved" + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains a governance decision" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_delivery_monitor--check_governance_approved" + description: "Context correlation_id returned unchanged" + - id: create_buy + title: "Create media buy under governance approval" + narrative: | + With governance approved, the buyer creates the media buy. The seller confirms + and returns a media_buy_id the buyer will use to pull delivery metrics as the + campaign runs. + + steps: + - id: create_media_buy + title: "Create a media buy" + narrative: | + The buyer creates the media buy with the governance_context from the approved + check. The seller confirms and returns a media_buy_id that subsequent delivery + monitoring steps reference. + 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: governance_delivery_monitor + stateful: true + expected: | + Confirm the media buy: + - media_buy_id: platform-assigned identifier + - status: active + - packages: confirmed line items + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + governance_context: "gov_ctx_acme_delivery_approved" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 20000 + pricing_option_id: "cpm_guaranteed" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + + idempotency_key: "$generate:uuid_v4#governance_delivery_monitor_create_buy_create_media_buy" + context: + correlation_id: "governance_delivery_monitor--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + - check: field_present + path: "media_buy_id" + description: "Response contains a media_buy_id for delivery monitoring" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_delivery_monitor--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: delivery_monitoring + title: "Delivery metrics show budget drift" + narrative: | + The campaign is running. The buyer checks delivery and finds that one line item + is overspending while the other is underspending. The CTV line item has consumed + 70% of its budget at the halfway point while the video line item is at 30%. + + steps: + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data and finds budget drift. One line item is + significantly overpacing while the other is underpacing. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: governance_delivery_monitor + stateful: true + expected: | + Return delivery metrics showing drift: + - Per-package delivery with impressions, spend, and pacing + - One package overpacing (>60% spend at 50% flight) + - One package underpacing (<40% spend at 50% flight) + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "governance_delivery_monitor--get_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_delivery_monitor--get_delivery" + description: "Context correlation_id returned unchanged" + - id: drift_recheck + title: "Governance re-check — drift detected" + narrative: | + The buyer's governance agent detects that budget drift has exceeded the 20% + reallocation threshold. It triggers a delivery-phase governance check with the + current delivery data as evidence. The governance agent re-evaluates and returns + a decision — either re-approved with updated conditions or a recommendation to + pause and rebalance. + + steps: + - id: check_governance_drift + title: "Delivery-phase governance re-check (drift exceeded)" + narrative: | + The buyer calls check_governance with phase: delivery and attaches + the current delivery metrics as evidence. The governance agent evaluates the + drift against the reallocation threshold and returns a decision. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_delivery_monitor + stateful: true + expected: | + Return a governance decision about the delivery drift: + - decision: approved (with rebalancing conditions) or denied (pause recommended) + - findings: warning-severity findings noting the drift amounts + - severity: warning + - category_id: BUDGET_DRIFT_EXCEEDED + - explanation: explains which line items drifted and by how much + - conditions: if approved, conditions for rebalancing (e.g., "Reallocate $5K from CTV to video") + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + phase: "delivery" + governance_context: "gov_ctx_acme_delivery_approved" + delivery_metrics: + reporting_period: + start: "2026-05-01T00:00:00Z" + end: "2026-05-15T00:00:00Z" + spend: 20000 + cumulative_spend: 20000 + channel_distribution: + ctv: 70 + video: 30 + pacing: "ahead" + + context: + correlation_id: "governance_delivery_monitor--check_governance_drift" + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains a governance decision" + - check: field_present + path: "findings" + description: "Response contains findings about the drift" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_delivery_monitor--check_governance_drift" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/governance-spend-authority/denied.yaml b/dist/compliance/3.0.1/specialisms/governance-spend-authority/denied.yaml new file mode 100644 index 0000000000..65816831c7 --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/governance-spend-authority/denied.yaml @@ -0,0 +1,221 @@ +id: governance_spend_authority/denied +version: "1.0.0" +title: "Campaign governance — denied" +category: governance_spend_authority +summary: "Governance agent denies a media buy that exceeds the agent's spending authority. No human escalation — the buy is blocked." +track: campaign_governance +required_tools: + - sync_plans + - check_governance + +# Cross-step assertion (adcp#2639): once a plan is denied, no subsequent +# step in the same run may acquire a resource for that plan. Catches the +# failure mode where a seller surfaces the denial response but still +# creates the media buy / activates the signal / syncs the property list +# anyway. Plan-scoped — unrelated plans proceed normally. +# Cross-step assertion (adcp#2664): status.monotonic rejects media_buy +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. active → pending_creatives. +invariants: + - governance.denial_blocks_mutation + - status.monotonic + +narrative: | + The buyer's governance agent registers a plan with strict spending authority. The buyer + then proposes a media buy that exceeds the agent's per-transaction threshold. The + governance agent denies the buy outright with a must-severity finding. + + Unlike the escalation storyboard, there is no human override here. The denial is final. + The buyer must reduce the buy amount or restructure into smaller transactions that fall + within the agent's authority. This tests the hard-stop governance path. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform with governance support" + - "Retail media network" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, and a governance agent URL. + The governance plan must define a per-transaction threshold below the intended buy amount. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "governance_spend_authority--denied--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: "governance_spend_authority--denied--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: plan_registration + title: "Register governance plan" + narrative: | + The buyer registers a governance plan with strict spending authority. The plan sets + an agent_limited authority level with a $10K per-transaction threshold. Any buy above + this amount is denied without escalation. + + steps: + - id: sync_plans + title: "Register a governance plan with strict authority" + narrative: | + The buyer's governance agent registers a plan with a $10K per-transaction limit + and no escalation path. Buys that exceed this threshold are denied outright. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: governance_spend_authority/denied + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - budget.total: $10K cap that any single buy above will exceed + + sample_request: + idempotency_key: "governance-spend-authority-denied-sync-plans-v1" + plans: + - plan_id: "gov_acme_strict" + brand: + domain: "acmeoutdoor.example" + objectives: "Acme Outdoor low-budget validation flight" + budget: + total: 10000 + currency: "USD" + reallocation_threshold: 0 + flight: + start: "2027-01-01T00:00:00Z" + end: "2027-12-31T23:59:59Z" + countries: ["US"] + + context: + correlation_id: "governance_spend_authority--denied--sync_plans" + context_outputs: + - name: plan_id + path: 'plans[0].plan_id' + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_spend_authority--denied--sync_plans" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "plans[0].plan_id" + description: "Governance agent assigns plan_id — must be echoed in check_governance" + - id: governance_check + title: "Governance check — denied" + narrative: | + The buyer proposes a $50K media buy against the $10K threshold. The governance agent + evaluates the binding and returns a denied decision with a must-severity finding + explaining the spending authority violation. No escalation instructions are provided + because the plan has no escalation path. + + steps: + - id: check_governance_denied + title: "Pre-buy governance check (denied, no escalation)" + narrative: | + The buyer calls check_governance with a $50K media buy binding. The governance + agent denies the buy because the total exceeds the $10K per-transaction authority. + The response includes a critical-severity finding with the violation details. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_spend_authority/denied + stateful: true + expected: | + Return a denied governance decision: + - decision: denied + - findings: array with at least one critical-severity finding + - severity: critical + - category_id: SPENDING_AUTHORITY_EXCEEDED + - explanation: explains the threshold and how much the buy exceeds it + - No escalation instructions (plan has no escalation path) + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + tool: "create_media_buy" + payload: + idempotency_key: "$generate:uuid_v4#governance_spend_authority_denied_check_governance_denied_payload" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + start_time: "2027-01-01T00:00:00Z" + end_time: "2027-03-31T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + pricing_option_id: "cpm_standard" + - product_id: "outdoor_video_q2" + budget: 20000 + pricing_option_id: "cpm_standard" + + context: + correlation_id: "governance_spend_authority--denied--check_governance_denied" + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains a governance decision" + - check: field_value + path: "status" + value: "denied" + description: "Decision is denied" + - check: field_present + path: "findings" + description: "Response contains findings explaining the denial" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_spend_authority--denied--check_governance_denied" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/governance-spend-authority/index.yaml b/dist/compliance/3.0.1/specialisms/governance-spend-authority/index.yaml new file mode 100644 index 0000000000..437b4bcfba --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/governance-spend-authority/index.yaml @@ -0,0 +1,330 @@ +id: governance_spend_authority +version: "1.0.0" +title: "Campaign governance — conditional approval" +protocol: governance +category: governance_spend_authority +summary: "Governance agent approves a media buy with conditions. Buyer re-checks after meeting the conditions." +track: campaign_governance +required_tools: + - sync_plans + - check_governance + +# Cross-step assertion (adcp#2639): silent on approval runs; fires only +# if a seller surfaces a denial signal mid-flow and then still mutates +# the plan's resources. Kept wired here so any future "conditions not +# met → denied" phase added to this storyboard is automatically gated. +# Cross-step assertion (adcp#2664): status.monotonic rejects media_buy +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. active → pending_creatives. +invariants: + - governance.denial_blocks_mutation + - status.monotonic + +narrative: | + The buyer's governance agent evaluates a media buy that falls within spending authority but + triggers policy conditions — for example, the buy targets a channel that requires weekly + reporting, or the creative format requires brand safety review. + + The governance agent returns approved_with_conditions, attaching conditions the buyer must + honor during the campaign. The buyer can proceed with the media buy by passing the + governance context, but the conditions are binding. + + This storyboard tests the middle path between outright approval and denial: the buy is + allowed, but with strings attached. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform with governance support" + - "SSP that respects governance checks" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, and a governance agent URL. + The governance plan defines policy conditions that trigger on specific buy parameters. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "sports_ctv_q2" + delivery_type: "guaranteed" + channels: ["ctv"] + format_ids: + - id: "video_30s" + - product_id: "lifestyle_display_q2" + delivery_type: "guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + pricing_options: + - product_id: "sports_ctv_q2" + pricing_option_id: "cpm_guaranteed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 45.0 + - product_id: "lifestyle_display_q2" + pricing_option_id: "cpm_standard" + pricing_model: "cpm" + currency: "USD" + fixed_price: 8.0 + plans: + - plan_id: "gov_acme_spend_authority_q2_2027" + brand: + domain: "acmeoutdoor.example" + objectives: "Full spending authority with conditional policies on CTV reporting and UGC brand safety" + budget: + total: 100000 + currency: "USD" + reallocation_unlimited: true + flight: + start: "2027-01-01T00:00:00Z" + end: "2027-12-31T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "ctv_weekly_reporting" + enforcement: "must" + policy: "CTV buys require weekly delivery reporting." + - policy_id: "ugc_brand_safety" + enforcement: "must" + policy: "UGC placements require brand safety review before go-live." + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "governance_spend_authority--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: "governance_spend_authority--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: plan_registration + title: "Register governance plan with policy conditions" + narrative: | + The buyer registers a governance plan that allows the agent full spending authority + but attaches policy conditions for specific channels or formats. Buys that trigger + these policies are approved with conditions rather than denied. + + steps: + - id: sync_plans + title: "Register a governance plan with policy conditions" + narrative: | + The buyer's governance agent registers a plan with full spending authority but + custom policies that require weekly reporting for CTV buys and brand safety + review for user-generated content placements. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: governance_spend_authority + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - custom_policies: policy conditions registered + + sample_request: + idempotency_key: "governance-spend-authority-sync-plans-v1" + plans: + - plan_id: "gov_acme_spend_authority_q2_2027" + brand: + domain: "acmeoutdoor.example" + objectives: "Full spending authority with conditional policies on CTV reporting and UGC brand safety" + budget: + total: 100000 + currency: "USD" + reallocation_unlimited: true + flight: + start: "2027-01-01T00:00:00Z" + end: "2027-12-31T23:59:59Z" + countries: ["US"] + custom_policies: + - policy_id: "ctv_weekly_reporting" + enforcement: "must" + policy: "CTV buys require weekly delivery reporting." + - policy_id: "ugc_brand_safety" + enforcement: "must" + policy: "UGC placements require brand safety review before go-live." + + context: + correlation_id: "governance_spend_authority--sync_plans" + context_outputs: + - name: plan_id + path: 'plans[0].plan_id' + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_spend_authority--sync_plans" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "plans[0].plan_id" + description: "Governance agent assigns plan_id — must be echoed in check_governance" + - check: field_present + path: "plans[0].version" + description: "Plan includes version for concurrency" + - id: governance_check_conditions + title: "Governance check — approved with conditions" + narrative: | + The buyer proposes a media buy that includes CTV inventory. The governance agent + approves the buy but attaches the weekly reporting condition from the plan. The + buyer receives a governance context that encodes these conditions. + + steps: + - id: check_governance_conditions + title: "Pre-buy governance check (approved with conditions)" + narrative: | + The buyer calls check_governance with a media buy that includes CTV products. + The governance agent approves the buy but attaches conditions: weekly delivery + reporting is required for the CTV line items. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: governance_spend_authority + stateful: true + expected: | + Return an approved governance decision with conditions: + - decision: approved + - conditions: array of requirements the buyer must honor + - e.g., "Weekly delivery reporting required for CTV line items" + - governance_context: token the buyer passes to create_media_buy + - findings: may include warning-severity findings noting the conditions + + sample_request: + plan_id: "$context.plan_id" + caller: "https://pinnacle-agency.example" + tool: "create_media_buy" + payload: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + total_budget: 40000 + packages: + - product_id: "sports_ctv_q2" + budget: 25000 + - product_id: "lifestyle_display_q2" + budget: 15000 + + context: + correlation_id: "governance_spend_authority--check_governance_conditions" + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "status" + description: "Response contains a governance decision" + - check: field_present + path: "conditions" + description: "Approval includes conditions" + - check: field_present + path: "governance_context" + description: "Response includes governance context for the media buy" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_spend_authority--check_governance_conditions" + description: "Context correlation_id returned unchanged" + - id: create_buy_with_conditions + title: "Create media buy with governance conditions" + narrative: | + The buyer creates the media buy, passing the governance context from the conditional + approval. The seller validates the governance approval and confirms the buy. The + conditions from the governance check are now binding for the campaign duration. + + steps: + - id: create_media_buy + title: "Create a media buy with conditional governance approval" + narrative: | + The buyer creates the media buy with the governance_context token from the + conditional approval. The seller confirms the buy. The buyer is now bound by + the conditions (weekly reporting for CTV line items). + 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: governance_spend_authority + stateful: true + expected: | + Confirm the media buy with governance approval: + - media_buy_id: your platform's identifier + - status: active + - governance_context: echoed back confirming governance was validated + - packages: line items + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + governance_context: "gov_ctx_acme_conditional_approved" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 25000 + pricing_option_id: "cpm_guaranteed" + - product_id: "lifestyle_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + + idempotency_key: "$generate:uuid_v4#governance_spend_authority_create_buy_with_conditions_create_media_buy" + context: + correlation_id: "governance_spend_authority--create_media_buy" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "governance_spend_authority--create_media_buy" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/property-lists/index.yaml b/dist/compliance/3.0.1/specialisms/property-lists/index.yaml new file mode 100644 index 0000000000..24fa47926d --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/property-lists/index.yaml @@ -0,0 +1,482 @@ +id: property_lists +version: "1.0.0" +title: "Property lists" +protocol: governance +category: property_lists +summary: "Curated property lists for inventory grouping, targeting governance, and delivery compliance — create, query, update, delete, and validate." +track: governance +required_tools: + - create_property_list + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph. Silent on property-list-only runs (no tracked +# lifecycle resource), but wired so phases that touch media_buy / +# account status (e.g. via a validation run against delivery) are +# automatically gated. +invariants: + - status.monotonic + +narrative: | + You run a governance agent that manages property lists for brand safety. Buyers create + inclusion and exclusion lists that define where their ads can and cannot appear. Your + agent stores these lists, lets buyers query and update them, and validates that actual + ad delivery complied with the property constraints. + + Property governance is how brands control their environment. An inclusion list says + "only show my ads on these properties." An exclusion list says "never show my ads here." + The validation step checks after the fact: did the seller actually respect the lists? + + This storyboard covers the full property list lifecycle: creating lists, querying them, + updating and deleting, and validating that delivery matched the constraints. + +agent: + interaction_model: governance_agent + capabilities: + - property_lists + - brand_safety + examples: + - "IAS" + - "DoubleVerify" + - "GARM-aligned platforms" + - "Brand safety services" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and property domain knowledge. The test kit + provides a sample brand with campaign context. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports governance before registering plans or checking compliance. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring governance in supported_protocols, confirming the agent provides governance services. + sample_request: + context: + correlation_id: "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: "property_lists--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: create_list + title: "Create property lists" + narrative: | + The buyer creates inclusion and exclusion property lists for the campaign. These + define the safe and unsafe environments for the brand's ads. + + steps: + - id: create_inclusion_list + title: "Create an inclusion list" + narrative: | + The buyer creates an inclusion list specifying which properties (domains, + apps, channels) are approved for ad placement. + task: create_property_list + schema_ref: "property/create-property-list-request.json" + response_schema_ref: "property/create-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return the created property list: + - list_id: platform-assigned identifier + - list_type: inclusion + - Properties registered + - Status: active + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + name: "Acme Outdoor approved properties" + base_properties: + - selection_type: "identifiers" + identifiers: + - type: "domain" + value: "outdoormagazine.example" + - type: "domain" + value: "hikingtrails.example" + - type: "domain" + value: "campinggear.example" + + idempotency_key: "$generate:uuid_v4#property_lists_create_list_create_inclusion_list" + context: + correlation_id: "property_lists--create_inclusion_list" + context_outputs: + - path: "list.list_id" + key: "property_list_id" + + validations: + - check: response_schema + description: "Response matches create-property-list-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "property_lists--create_inclusion_list" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "list.list_id" + description: "Governance agent assigns list_id — must be echoed in get/update/delete" + - id: list_and_get + title: "Query property lists" + narrative: | + The buyer lists all property lists for the account and retrieves a specific list + to inspect its contents. + + steps: + - id: list_property_lists + title: "List all property lists" + narrative: | + The buyer lists all property lists for the brand. The response includes list + metadata (name, type, property count) without full property details. + task: list_property_lists + schema_ref: "property/list-property-lists-request.json" + response_schema_ref: "property/list-property-lists-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: property_list_filters + stateful: true + expected: | + Return property list summaries: + - Array of lists with list_id, name, list_type, property_count + - Includes both inclusion and exclusion lists + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + name_contains: "Acme Outdoor" + + context: + correlation_id: "property_lists--list_property_lists" + validations: + - check: response_schema + description: "Response matches list-property-lists-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "property_lists--list_property_lists" + description: "Context correlation_id returned unchanged" + - id: get_property_list + title: "Get a specific property list" + narrative: | + The buyer retrieves the full details of a specific property list, including + all properties in the list. + task: get_property_list + schema_ref: "property/get-property-list-request.json" + response_schema_ref: "property/get-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return the full property list: + - list_id, name, list_type + - All properties in the list with their details + + sample_request: + list_id: "$context.property_list_id" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "property_lists--get_property_list" + validations: + - check: response_schema + description: "Response matches get-property-list-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "property_lists--get_property_list" + description: "Context correlation_id returned unchanged" + - id: update_list + title: "Update property lists" + narrative: | + The buyer modifies an existing property list — replacing the base properties + as brand safety requirements evolve. + + steps: + - id: update_property_list + title: "Update a property list" + narrative: | + The buyer replaces the base properties on an existing list with a new set. + task: update_property_list + schema_ref: "property/update-property-list-request.json" + response_schema_ref: "property/update-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return the updated property list: + - Updated property count + - Confirmation of the replaced base properties + + sample_request: + list_id: "$context.property_list_id" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + base_properties: + - selection_type: "identifiers" + identifiers: + - type: "domain" + value: "outdoormagazine.example" + - type: "domain" + value: "hikingtrails.example" + - type: "domain" + value: "mountaineering.example" + + idempotency_key: "$generate:uuid_v4#property_lists_update_list_update_property_list" + context: + correlation_id: "property_lists--update_property_list" + validations: + - check: response_schema + description: "Response matches update-property-list-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "property_lists--update_property_list" + description: "Context correlation_id returned unchanged" + - id: delivery_validation + title: "Validate delivery compliance" + narrative: | + After ads have been delivered, the buyer validates that the seller respected the + property lists. The governance agent checks actual delivery data against the + inclusion/exclusion constraints. + + steps: + - id: validate_property_delivery + title: "Validate property compliance" + narrative: | + The buyer submits delivery data and the governance agent checks whether all + placements complied with the property lists. Non-compliant placements are + flagged with details. + task: validate_property_delivery + schema_ref: "property/validate-property-delivery-request.json" + response_schema_ref: "property/validate-property-delivery-response.json" + doc_ref: "/governance/property/tasks/validate_property_delivery" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return validation results: + - compliant: boolean overall status + - summary aggregates compliant/non_compliant/not_covered/unidentified counts + - Per-placement compliance status + - features[] entries on each non_compliant record explaining the breach + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + list_id: "$context.property_list_id" + records: + - record_id: "delivery_outdoor_001" + identifier: + type: "domain" + value: "outdoormagazine.example" + impressions: 50000 + - record_id: "delivery_random_001" + identifier: + type: "domain" + value: "randomsite.example" + impressions: 200 + + context: + correlation_id: "property_lists--validate_property_delivery" + validations: + - check: response_schema + description: "Response matches validate-property-delivery-response.json schema" + - check: field_value + path: "compliant" + value: false + description: "Overall compliance is false when randomsite.example violates inclusion list" + + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "property_lists--validate_property_delivery" + description: "Context correlation_id returned unchanged" + + - id: enforcement + title: "Property list enforcement" + narrative: | + These steps test behavioral compliance beyond schema validation. An agent that + returns well-shaped responses but does not actually enforce property constraints + would pass CRUD tests but fail here. + + steps: + - id: validate_all_compliant_delivery + title: "Validate fully compliant delivery" + narrative: | + The buyer submits delivery data where all properties are on the inclusion list. + The governance agent must return records with status: compliant and no failed features. + task: validate_property_delivery + schema_ref: "property/validate-property-delivery-request.json" + response_schema_ref: "property/validate-property-delivery-response.json" + doc_ref: "/governance/property/tasks/validate_property_delivery" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return validation results: + - record status: compliant + - No failed or warning features + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + list_id: "$context.property_list_id" + records: + - record_id: "delivery_outdoor_001" + identifier: + type: "domain" + value: "outdoormagazine.example" + impressions: 50000 + - record_id: "delivery_hiking_001" + identifier: + type: "domain" + value: "hikingtrails.example" + impressions: 30000 + + validations: + - check: response_schema + description: "Response matches validate-property-delivery-response.json schema" + - check: field_value + path: "compliant" + value: true + description: "Delivery is compliant when all properties are on inclusion list" + + - id: validate_unauthorized_publisher + title: "Reject delivery on unlisted domain" + narrative: | + The buyer submits delivery data that includes randomsite.example — a domain + not on the inclusion list. The governance agent must flag this as a violation + with specific violation details, not just return a generic failure. + task: validate_property_delivery + schema_ref: "property/validate-property-delivery-request.json" + response_schema_ref: "property/validate-property-delivery-response.json" + doc_ref: "/governance/property/tasks/validate_property_delivery" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return validation results: + - record status: non_compliant + - features array with at least one failed feature entry for randomsite.example + - feature entry references the inclusion list that was breached (via policy_id or code) + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + list_id: "$context.property_list_id" + records: + - record_id: "delivery_random_001" + identifier: + type: "domain" + value: "randomsite.example" + impressions: 500 + + validations: + - check: response_schema + description: "Response matches validate-property-delivery-response.json schema" + - check: field_value + path: "compliant" + value: false + description: "Delivery is non-compliant for unlisted domain" + + - id: delete_list + title: "Delete a property list" + narrative: | + The buyer removes a property list that is no longer needed. Delete runs after + validation phases so they can reference the list created earlier. + + steps: + - id: delete_property_list + title: "Delete a property list" + narrative: | + The buyer deletes a property list. The governance agent removes the list and + returns confirmation. + task: delete_property_list + schema_ref: "property/delete-property-list-request.json" + response_schema_ref: "property/delete-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Confirm deletion: + - list_id: the deleted list + - status: deleted + + sample_request: + list_id: "$context.property_list_id" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + idempotency_key: "$generate:uuid_v4#property_lists_delete_list_delete_property_list" + context: + correlation_id: "property_lists--delete_property_list" + validations: + - check: response_schema + description: "Response matches delete-property-list-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "property_lists--delete_property_list" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/sales-broadcast-tv/index.yaml b/dist/compliance/3.0.1/specialisms/sales-broadcast-tv/index.yaml new file mode 100644 index 0000000000..deb907b99d --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/sales-broadcast-tv/index.yaml @@ -0,0 +1,689 @@ +id: sales_broadcast_tv +version: "1.0.0" +title: "Broadcast linear TV seller agent" +protocol: media-buy +category: sales_broadcast_tv +summary: "Seller agent for broadcast linear TV inventory — primetime and fringe spots with measurement windows, agency estimate numbers, Ad-ID-based creative sync, and delayed delivery reporting." +track: media_buy +required_tools: + - get_products + - create_media_buy +requires_scenarios: + - media_buy_seller/refine_products + - media_buy_seller/delivery_reporting + - media_buy_seller/measurement_terms_rejected + - media_buy_seller/pending_creatives_to_start + - media_buy_seller/inventory_list_targeting + - media_buy_seller/inventory_list_no_match + - media_buy_seller/invalid_transitions + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. active → pending_creatives on a media_buy. +invariants: + - status.monotonic + +narrative: | + You run a broadcast television platform — a local TV station group, network affiliate, + or television rep firm that sells linear advertising inventory. A buyer agent connects + to discover your dayparts and programs, negotiate guaranteed buys, sync broadcast spot + files, and monitor delivery against measurement windows. + + Broadcast buying differs from digital in several ways. Products are organized by daypart + and program rather than audience segment. Pricing is unit-based (cost per spot) rather + than impression-based. Measurement accumulates over time through Live, C3, and C7 + windows as DVR playback is counted. Creative assets are broadcast-grade video files + identified by Ad-ID — no VAST wrappers, no impression trackers, no click URLs. + + Delivery data arrives on a delay. Live ratings are available within a day, but C3 and + C7 data take 4 and 8 days respectively after broadcast. Final reconciliation happens + against C7 numbers, which means billing data is not complete until ~15 days after the + last air date. + + This storyboard walks through the broadcast buying cycle from product discovery through + reconciliation, exercising the protocol fields that distinguish linear TV from digital. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - supports_guaranteed + examples: + - "Local TV station groups" + - "Broadcast network affiliates" + - "Television rep firms" + +caller: + role: buyer_agent + example: "Pinnacle Agency" + +prerequisites: + description: | + The caller needs an established account relationship with the seller. The test + kit provides the Nova Motors Volta EV launch campaign — an automotive brand with + national broadcast reach goals and budget appropriate for primetime linear TV. + test_kit: "test-kits/nova-motors.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "primetime_30s_mf" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "broadcast_spot_30s" + - product_id: "late_fringe_15s_mf" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "broadcast_spot_15s" + pricing_options: + - product_id: "primetime_30s_mf" + pricing_option_id: "unit_primetime_30" + pricing_model: "flat_rate" + currency: "USD" + fixed_price: 4500.0 + - product_id: "late_fringe_15s_mf" + pricing_option_id: "unit_fringe_15" + pricing_model: "flat_rate" + currency: "USD" + fixed_price: 1200.0 + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "sales_broadcast_tv--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: "sales_broadcast_tv--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: product_discovery + title: "Product discovery" + narrative: | + The buyer sends a brief describing what they want to buy on linear TV. The seller + interprets the brief against their program schedule, ratings estimates, and + available inventory to return products organized by daypart. + + Broadcast products include measurement windows — Live, C3, and C7 — that describe + how ratings data accumulates over time. The buyer uses these windows to understand + when delivery data will be available and which window will serve as the guarantee + basis for reconciliation. + + steps: + - id: get_products_brief + title: "Send a broadcast brief" + narrative: | + The buyer describes their linear TV goals in natural language. The seller + returns products representing available dayparts and programs, each with + unit-based pricing, audience delivery estimates, creative format requirements, + and measurement windows. + + Measurement windows on each product tell the buyer: "Here is when data + becomes available and how it accumulates." Live ratings arrive within a day. + C3 (live + 3 days of DVR) arrives ~4 days after broadcast. C7 (live + 7 days + of DVR) arrives ~8 days after broadcast. The buyer decides which window to + use as the guarantee basis when creating the media buy. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products matching the brief. Each product should include: + - product_id: unique identifier for the daypart or program package + - name: descriptive name (e.g., "Primetime :30 — M-F 8-11pm") + - delivery_type: guaranteed (standard for broadcast upfront and scatter) + - pricing_models: unit-based pricing (cost per spot or cost per unit) + - forecast: estimated impressions by demo, GRPs, reach + - creative_format_ids: broadcast spot formats (:15, :30, :60) + - reporting_capabilities with measurement_windows: + - live: real-time linear viewing, available within 24 hours + - c3: live + 3 days DVR playback, available ~4 days after air + - c7: live + 7 days DVR playback, available ~8 days after air + + sample_request: + buying_mode: "brief" + brief: "Primetime and late fringe broadcast spots for an automotive EV launch. Q4 flight, $400K budget. Adults 25-54, national footprint. Need :30 and :15 spot lengths." + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "sales_broadcast_tv--get_products_brief" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + - check: field_present + path: "products[0].product_id" + description: "Each product has a product_id" + - check: field_present + path: "products[0].delivery_type" + description: "Each product declares guaranteed delivery" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_broadcast_tv--get_products_brief" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: create_buy + title: "Create the media buy" + narrative: | + The buyer commits to specific daypart packages with unit counts and flight dates. + Broadcast buys carry an agency estimate number — the financial reference that links + the order to the agency's media plan and billing system. This number travels with + the order through traffic, delivery, and invoicing. + + The buyer also specifies measurement terms, declaring which measurement window + (typically C7) serves as the guarantee basis. This tells the seller: "Bill me + based on C7 ratings, not live." + + steps: + - id: create_media_buy + title: "Create a broadcast media buy" + narrative: | + The buyer places the order with an agency estimate number at the buy level. + Each package references a product from discovery. The measurement_terms + specify C7 as the guarantee window — the seller will reconcile delivery + and billing against C7 ratings. + + The response may be synchronous (buy confirmed) or — when traffic-manager + review is needed — the A2A task returns submitted with a task_id, and the + buyer waits on a webhook or tasks/get poll until the order is scheduled. + 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: | + Process the broadcast media buy and return: + - media_buy_id: the seller's order identifier + - agency_estimate_number: echoed from the request + - status: pending_creatives (awaiting spot files) or active + - packages: line items with confirmed units, rates, and flight dates + - measurement_terms: confirmed guarantee window (c7) + - valid_actions: sync_creatives as the next step + + If traffic-manager review is needed, return an A2A task envelope instead: + - status: submitted (task-level — not a MediaBuy status) + - task_id / taskId: handle the buyer polls or receives webhooks on + - message (optional): explanation that the traffic manager is reviewing + + Do NOT use a "pending_approval" media buy status — that value is not in the + MediaBuy.status enum. IO / traffic-manager review is modelled at the task layer. + + sample_request: + brand: + domain: "novamotors.example" + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + agency_estimate_number: "PNNL-NM-2026-Q4-0847" + start_time: "2026-10-01T00:00:00Z" + end_time: "2026-12-31T23:59:59Z" + packages: + - product_id: "primetime_30s_mf" + budget: 280000 + pricing_option_id: "unit_primetime_30" + creative_assignments: + - creative_id: "volta_ev_launch_30s" + measurement_terms: + billing_measurement: + vendor: + domain: "videoamp.example" + measurement_window: "c7" + max_variance_percent: 10 + - product_id: "late_fringe_15s_mf" + budget: 120000 + pricing_option_id: "unit_fringe_15" + creative_assignments: + - creative_id: "volta_ev_launch_15s" + measurement_terms: + billing_measurement: + vendor: + domain: "videoamp.example" + measurement_window: "c7" + max_variance_percent: 10 + push_notification_config: + url: "{{runner.webhook_url:create_media_buy}}" + authentication: + schemes: + - "HMAC-SHA256" + credentials: "pinnacle-broadcast-tv-webhook-secret-token" + + idempotency_key: "$generate:uuid_v4#sales_broadcast_tv_create_buy_create_media_buy" + context: + correlation_id: "sales_broadcast_tv--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_broadcast_tv--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: check_buy_status + title: "Check media buy status" + narrative: | + If create_media_buy returned working or submitted, the buyer polls for status + updates. For broadcast, the seller's traffic department may need to confirm + scheduling availability before the buy is active. + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Return the current state of the media buy: + - media_buy_id: matches what was returned from create_media_buy + - status: pending_creatives, pending_start, active, paused, completed + - packages: line items with scheduling confirmation + - valid_actions: what operations are available in this state + + If pending_creatives: + - Include message explaining that broadcast spot files are needed + - valid_actions should include sync_creatives + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "sales_broadcast_tv--check_buy_status" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_present + path: "media_buys[0].status" + description: "Each media buy has a status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_broadcast_tv--check_buy_status" + description: "Context correlation_id returned unchanged" + - id: creative_sync + title: "Creative sync" + narrative: | + Broadcast creative sync differs from digital in three ways. First, assets are + broadcast-grade video files — no VAST wrappers, no impression trackers, no click + URLs. The seller ingests the video file directly into their traffic and playout + system. Second, each creative carries an Ad-ID in industry_identifiers, which ties + the spot to rotation instructions and downstream traffic systems. Third, format + requirements are defined by spot length (:15, :30, :60) rather than pixel dimensions. + + steps: + - id: list_formats + title: "Check broadcast format requirements" + narrative: | + The buyer confirms what broadcast spot formats the seller accepts. The seller + returns format specs defined by spot length, codec requirements, and file + delivery specifications. No VAST or tracker-related formats appear. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return broadcast spot formats the platform accepts. Each format should define: + - format_id with your agent_url and unique id (e.g., "broadcast_30s", "broadcast_15s") + - Asset requirements: video codec, container format, bitrate, frame rate + - Duration constraints matching the spot length + - No VAST, VPAID, or tracker-related asset slots + + sample_request: + context: + correlation_id: "sales_broadcast_tv--list_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_broadcast_tv--list_formats" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - id: sync_creatives + title: "Push broadcast spot files" + narrative: | + The buyer uploads broadcast spot files with Ad-ID identifiers. Each creative + has a single video asset and one or more industry_identifiers with type ad_id. + There are no impression_tracker or click_url assets — broadcast spots are + self-contained video files that the seller loads into playout. + + The Ad-ID is the critical link. It connects the creative asset to rotation + instructions, traffic logs, and post-log reconciliation. Without a valid + Ad-ID, the spot cannot be scheduled. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept and validate broadcast spot files: + - Per-creative action: created or updated + - Per-creative status: accepted, pending_review, or rejected + - Validation of video technical specs (codec, bitrate, duration) + - Confirmation that Ad-ID is recognized and valid + - Rejection if Ad-ID is missing or malformed + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "volta_ev_launch_30s" + name: "Nova Volta EV Launch - :30" + format_id: + agent_url: "https://your-station.example.com" + id: "broadcast_30s" + industry_identifiers: + - type: "ad_id" + value: "NOVA0042000H" + assets: + video_file: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/nova-volta-30s.mp4" + width: 1920 + height: 1080 + duration_ms: 30000 + container_format: "mp4" + video_codec: "h264" + - creative_id: "volta_ev_launch_15s" + name: "Nova Volta EV Launch - :15" + format_id: + agent_url: "https://your-station.example.com" + id: "broadcast_15s" + industry_identifiers: + - type: "ad_id" + value: "NOVA0042001H" + assets: + video_file: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/nova-volta-15s.mp4" + width: 1920 + height: 1080 + duration_ms: 15000 + container_format: "mp4" + video_codec: "h264" + + idempotency_key: "$generate:uuid_v4#sales_broadcast_tv_creative_sync_sync_creatives" + context: + correlation_id: "sales_broadcast_tv--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_broadcast_tv--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: delivery_monitoring + title: "Delivery and reporting" + narrative: | + Broadcast delivery reporting operates on a fundamentally different timeline than + digital. Live ratings are available within 24 hours, but the numbers that matter + for billing — C7 — are not final until 8 days after each air date. A buyer + checking delivery mid-flight will see data at different maturation stages: recent + airings show only live numbers, while airings from two weeks ago have full C7. + + The seller sends delivery data with measurement window context so the buyer + understands which numbers are preliminary and which are final. As windows mature, + the seller sends window_update notifications with supersedes_window indicating + which prior data is being replaced. The buyer should not make pacing decisions + based on live-only data for a C7-guaranteed buy. + + Window maturation is graded by observing the webhooks the seller emits to the + push_notification_config registered on create_media_buy. An agent that emits + no webhooks at all scores identically to one that emits the correct C3 and C7 + progression — closing that gap requires the webhook_receiver_runner test-kit + contract (so the runner has somewhere to observe deliveries); without it, the + grading step below grades as not_applicable rather than failing. + + steps: + - id: expect_window_update_webhook + title: "Seller emits at least one window_update delivery webhook" + narrative: | + After create_media_buy, the seller emits delivery-notification webhooks + as each measurement window matures (C3 superseding live with is_final: + false, then C7 superseding C3 with is_final: true). The envelope payload + MUST carry a valid idempotency_key and validate against the canonical + MCP webhook payload schema; the nested delivery result SHOULD carry + notification_type: window_update with the supersedes_window progression + appropriate to the window that has matured. + + The filter currently matches on operation_id only — runner extensions + for nested payload matching (e.g. result.notification_type) will allow + separating the C3 and C7 deliveries into distinct assertions in a + follow-up. Runs only when the webhook_receiver_runner contract is in + scope. + task: expect_webhook + triggered_by: create_media_buy + filter: + operation_id: "{{prior_step.create_media_buy.operation_id}}" + timeout_seconds: 30 + expect_idempotency_key: true + webhook_payload_schema_ref: "core/mcp-webhook-payload.json" + requires_contract: webhook_receiver_runner + stateful: true + expected: | + A webhook arrives within 30 seconds whose envelope validates against + mcp-webhook-payload.json and carries an idempotency_key matching + ^[A-Za-z0-9_.:-]{16,255}$. The nested delivery result (per + get-media-buy-delivery-response.json) SHOULD carry notification_type: + window_update with supersedes_window reflecting window progression + (e.g. "live" for C3, "c3" for C7). Sellers that emit no delivery + webhooks at all fail with no_webhook_received. + + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data for the active broadcast buy. The seller + returns ratings-based metrics broken down by package, with each data point + tagged by measurement window. + + Data arrives on a delay. For a spot that aired on October 15: + - Live data: available October 16 + - C3 data: available ~October 19 + - C7 data: available ~October 23 + + The buyer should expect that the most recent 8 days of the flight will not + have final C7 numbers. The response should make this clear through the + measurement window tagging on each data point. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics for the broadcast buy: + - Per-package: impressions, GRPs, spots aired, spend + - measurement_window on each package (live, c3, or c7) + - is_final: false for packages with data still maturing + - Pacing information relative to the guaranteed unit count + + For webhook delivery, use notification_type: window_update when + sending updated data for a period with a wider window. Include + supersedes_window to indicate which prior data is being replaced + (e.g., supersedes_window: "live" when sending C3 data). + + The buyer uses this to understand: + - How many spots have aired vs. the guaranteed count + - What the preliminary (live) and final (C7) audience delivery looks like + - Whether makegoods may be needed if C7 delivery is under-performing + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "sales_broadcast_tv--get_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_broadcast_tv--get_delivery" + description: "Context correlation_id returned unchanged" + - id: reconciliation + title: "Post-flight reconciliation" + narrative: | + Broadcast reconciliation cannot happen until C7 data has matured for every air + date in the flight. For a flight ending December 31, final C7 data for the last + air dates arrives around January 8. The seller then has approximately one week to + compile final delivery numbers and issue a post-log. + + The buyer pulls final delivery data ~15 days after the last air date. This is the + authoritative data set for billing reconciliation. If C7 delivery fell short of + the guaranteed audience levels, the buyer requests makegoods — additional spots + to close the delivery gap. + + steps: + - id: get_final_delivery + title: "Pull final reconciliation data" + narrative: | + The buyer requests delivery data after C7 has fully matured for all air dates. + This is the same get_media_buy_delivery task used during monitoring, but now + all data points reflect final C7 measurement. + + The buyer compares final C7 delivery against the guaranteed audience levels + from the media buy. If delivery is short, the buyer contacts the seller to + negotiate makegoods. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return final delivery metrics with all data points at C7 maturation: + - Per-package: final impressions, GRPs, spots aired, spend + - measurement_window: "c7" on all packages + - is_final: true on all packages (all windows fully matured) + - supersedes_window: "c3" (this C7 data replaced the prior C3 data) + - Final pacing: delivered vs. guaranteed audience levels + - Budget reconciliation: actual spend vs. committed + + If delivery fell short of guaranteed levels: + - Shortfall amount by package + - The buyer uses this data to negotiate makegoods with the seller + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "sales_broadcast_tv--get_final_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains final delivery data" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_broadcast_tv--get_final_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/sales-catalog-driven/index.yaml b/dist/compliance/3.0.1/specialisms/sales-catalog-driven/index.yaml new file mode 100644 index 0000000000..20715b1c0c --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/sales-catalog-driven/index.yaml @@ -0,0 +1,779 @@ +id: sales_catalog_driven +version: "1.0.0" +title: "Catalog-driven creative and conversion tracking" +protocol: media-buy +category: sales_catalog_driven +summary: "Seller that renders dynamic ads from product catalogs, tracks conversions, and optimizes delivery based on performance feedback." +track: media_buy +required_tools: + - get_products + - create_media_buy +requires_scenarios: + - media_buy_seller/refine_products + - media_buy_seller/delivery_reporting + - media_buy_seller/pending_creatives_to_start + - media_buy_seller/invalid_transitions + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. active → pending_creatives on a media_buy or +# approved → pending on a catalog_item. +invariants: + - status.monotonic + +narrative: | + You run a platform that supports catalog-driven advertising — think Snap Dynamic Ads, + Meta Product Catalogs, or retail media product listing ads. The buyer pushes a product + catalog (menu items, retail products, hotel listings), and your platform renders ads + dynamically from that feed. When a user converts, events are logged back and attributed + to catalog items, closing the optimization loop. + + This storyboard walks through the full catalog-to-conversion flow: account setup, + catalog sync, creative formats for catalog items, media buy with catalog packages, + event source configuration, conversion logging, and performance feedback. + + The key difference from the standard media buy flow is that creatives are catalog-driven — + the buyer doesn't build individual ads. They push a feed, and your platform renders + the right item to the right user at the right time. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_catalogs + - supports_conversion_tracking + - catalog_driven_creative + examples: + - "Snap (Dynamic Ads)" + - "Retail media networks" + - "Travel platforms" + - "Local commerce platforms" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a product catalog (feed URL or inline items) and an event source + for conversion tracking. The test kit provides a sample brand with catalog-compatible + assets. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "sales_catalog_driven--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: "sales_catalog_driven--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account setup" + narrative: | + The buyer establishes an account relationship with your platform. For catalog-driven + campaigns, the account must be active before any catalog sync can happen. + + steps: + - id: sync_accounts + title: "Establish account" + narrative: | + The buyer registers their brand and operator. Sandbox accounts are provisioned + instantly for testing catalog flows. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with account_id, status (active for sandbox), and billing terms. + + sample_request: + accounts: + - brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + billing: "operator" + sandbox: true + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_account_setup_sync_accounts" + context: + correlation_id: "sales_catalog_driven--sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--sync_accounts" + description: "Context correlation_id returned unchanged" + - id: catalog_sync + title: "Product catalog sync" + narrative: | + The buyer pushes their product catalog to your platform. This is the foundation of + catalog-driven advertising — every ad your platform renders comes from this feed. + + For a restaurant, this is the menu. For retail, it is the product feed. For travel, + it is hotel or flight listings. Your platform validates each item against your + format requirements (image dimensions, required fields, pricing format) and returns + per-item approval status. + + Large feeds may go async — the buyer gets back a submitted status and waits for + your platform to finish processing. Small feeds (inline items) are processed + synchronously. + + steps: + - id: discover_catalog_formats + title: "Check catalog format requirements" + narrative: | + Before pushing catalog items, the buyer checks what creative formats your + platform supports for catalog-driven ads. This tells the buyer what image + dimensions, text lengths, and required fields each catalog item needs. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return creative formats that accept catalog assets. Look for formats with + catalog-specific asset slots (product_image, product_title, price, description). + + sample_request: + channels: ["display", "native"] + + context: + correlation_id: "sales_catalog_driven--discover_catalog_formats" + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--discover_catalog_formats" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - id: sync_catalogs + title: "Push product catalog" + narrative: | + The buyer pushes their product feed. This can be a URL to an existing feed + (your platform fetches and re-fetches on a schedule) or inline items for + small catalogs. + + Your platform validates each item: images meet minimum dimensions, required + fields are present, prices are formatted correctly. Items that fail validation + are rejected with specific reasons. Items that pass are approved and available + for dynamic creative rendering. + task: sync_catalogs + schema_ref: "media-buy/sync-catalogs-request.json" + response_schema_ref: "media-buy/sync-catalogs-response.json" + doc_ref: "/media-buy/task-reference/sync_catalogs" + stateful: true + expected: | + Return per-catalog results with: + - catalog_id and action (created/updated) + - item_count, items_approved, items_pending, items_rejected + - item_issues for rejected items with specific reasons + - next_fetch_at for URL-based feeds + + sample_request: + account: + brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + catalogs: + - catalog_id: "menu_spring_2026" + type: "product" + name: "Spring 2026 Menu" + items: + - item_id: "ribeye_36oz" + title: "36oz Tomahawk Ribeye" + description: "Dry-aged 45 days, served with truffle butter and roasted bone marrow" + url: "https://amsterdam-steakhouse.example/menu/ribeye-36oz" + image_url: "https://cdn.amsterdam-steakhouse.example/menu/ribeye-36oz-hero.jpg" + price: + amount: 89.00 + currency: "USD" + - item_id: "seafood_tower" + title: "Grand Seafood Tower" + description: "Oysters, king crab, lobster tail, shrimp cocktail, tuna tartare" + url: "https://amsterdam-steakhouse.example/menu/seafood-tower" + image_url: "https://cdn.amsterdam-steakhouse.example/menu/seafood-tower-hero.jpg" + price: + amount: 145.00 + currency: "USD" + - item_id: "wagyu_flight" + title: "A5 Wagyu Tasting Flight" + description: "Three cuts of Japanese A5 Wagyu — striploin, ribeye cap, tenderloin" + url: "https://amsterdam-steakhouse.example/menu/wagyu-flight" + image_url: "https://cdn.amsterdam-steakhouse.example/menu/wagyu-flight-hero.jpg" + price: + amount: 195.00 + currency: "USD" + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_catalog_sync_sync_catalogs" + context: + correlation_id: "sales_catalog_driven--sync_catalogs" + validations: + - check: response_schema + description: "Response matches sync-catalogs-response.json schema" + - check: field_present + path: "catalogs[0].catalog_id" + description: "Catalog has an ID" + - check: field_present + path: "catalogs[0].item_count" + description: "Catalog reports item count" + - check: field_present + path: "catalogs[0].items_approved" + description: "Catalog reports approved item count" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--sync_catalogs" + description: "Context correlation_id returned unchanged" + - id: substitution_safety + title: "Catalog-item macro substitution safety" + narrative: | + Per docs/creative/universal-macros#substitution-safety-catalog-item-macros, + sales agents MUST percent-encode catalog-item macro values such that only + RFC 3986 `unreserved` characters remain unescaped before substituting them + into a URL context. Nested macro expansion is prohibited. + + This phase exercises the rule with three attacker-shaped catalog values + drawn from the unit-test fixture at `static/test-vectors/catalog-macro-substitution.json`: + reserved-char breakout, nested-expansion preservation, and non-ASCII + UTF-8. The remaining canonical vectors (CRLF injection, bidi-override, + mixed path/query, url-scheme injection) are exercised at the unit-test + layer and, where a specialism's template shape supports them, in + specialism-specific substitution-safety phases (see + `creative-generative/index.yaml` for CRLF + bidi coverage). + + The `expect_substitution_safe` step is gated on the + `substitution_observer_runner` test-kit contract (see + `test-kits/substitution-observer-runner.yaml`). Runners that do not + advertise the contract grade the step as `not_applicable` — the earlier + `sync_attacker_shaped_catalog` and `build_catalog_aware_creative` steps + still run (they exercise the catalog-acceptance and build paths), but + the substituted-URL assertion is skipped. + + steps: + - id: sync_attacker_shaped_catalog + title: "Push a catalog with attacker-shaped values" + narrative: | + Push a small catalog whose fields contain the six canonical + attacker-shaped values from the substitution-safety fixture. The + seller MUST accept the payload at sync_catalogs (the rule applies at + substitution time, not at ingest — the seller is free to pass values + through, then encode them at serve time). + task: sync_catalogs + schema_ref: "media-buy/sync-catalogs-request.json" + response_schema_ref: "media-buy/sync-catalogs-response.json" + doc_ref: "/media-buy/task-reference/sync_catalogs" + stateful: true + expected: | + Catalog accepted with per-item counts. Runner captures the item_ids + for downstream assertion binding. + + sample_request: + account: + brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + catalogs: + - catalog_id: "substitution_safety_probe_v1" + type: "product" + content_id_type: "sku" + name: "Substitution safety probe" + items: + # item_id is the vector name; the `sku` field carries the + # attacker-shaped value that substitution must encode. + - item_id: "reserved_char_breakout" + sku: "00013&cmd=drop" + title: "Reserved-char breakout probe" + url: "https://amsterdam-steakhouse.example/probe/reserved" + image_url: "https://cdn.amsterdam-steakhouse.example/probe/reserved.jpg" + price: { amount: 1.00, currency: "USD" } + - item_id: "nested_expansion" + sku: "vacancy-{DEVICE_ID}-42" + title: "Nested-expansion probe" + url: "https://amsterdam-steakhouse.example/probe/nested" + image_url: "https://cdn.amsterdam-steakhouse.example/probe/nested.jpg" + price: { amount: 1.00, currency: "USD" } + - item_id: "non_ascii" + sku: "café-amsterdam" + title: "Non-ASCII UTF-8 probe" + url: "https://amsterdam-steakhouse.example/probe/non-ascii" + image_url: "https://cdn.amsterdam-steakhouse.example/probe/non-ascii.jpg" + price: { amount: 1.00, currency: "USD" } + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_substitution_safety_sync_attacker_shaped_catalog" + context: + correlation_id: "sales_catalog_driven--sync_attacker_shaped_catalog" + validations: + - check: response_schema + description: "Response matches sync-catalogs-response.json schema" + - check: field_present + path: "catalogs[0].catalog_id" + description: "Catalog accepted" + + - id: build_catalog_aware_creative + title: "Build a creative with catalog-item macros in tracker URLs" + narrative: | + Build a creative whose impression and click trackers include + catalog-item macros bound to the `sku` field of the attacker-shaped + catalog items above. Request `include_preview: true` so the + substitution-observer runner has a preview surface to inspect. + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Creative manifest returned with preview_html or preview_url populated. + + sample_request: + message: "Build a catalog-driven display ad for the substitution_safety_probe_v1 catalog. Use {SKU} in impression and click tracker URLs." + target_format_id: + agent_url: "https://your-agent.example.com" + id: "display_300x250_catalog" + account: + brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + quality: "draft" + include_preview: true + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_substitution_safety_build_catalog_aware_creative" + context: + correlation_id: "sales_catalog_driven--build_catalog_aware_creative" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_present + path: "creative_manifest.format_id" + description: "Creative manifest returned" + + - id: expect_substitution_safe + title: "Assert substituted tracker URLs percent-encode attacker shapes" + narrative: | + The runner inspects the preview artifact from the previous step, + extracts tracker URLs that bind `{SKU}` to a catalog item from the + attacker-shaped catalog, and asserts each value is percent-encoded + per RFC 3986 (unreserved-whitelist). Raw-byte leakage fails. Every + declared binding MUST be observed — a seller that silently strips + `{SKU}` rather than substituting it fails with + `substitution_binding_missing`. + task: expect_substitution_safe + requires_contract: substitution_observer_runner + source: html_inline + source_path: "/creative_manifest/preview_html" + macro_template: "https://track.example/imp?sku={SKU}" + require_every_binding_observed: true + catalog_bindings: + # Each binding: `catalog_item_id` is the item_id in the synced + # catalog; `vector_name` is the fixture entry whose raw_value and + # expected_encoded the runner loads from + # static/test-vectors/catalog-macro-substitution.json. + - macro: "{SKU}" + catalog_item_id: "reserved_char_breakout" + vector_name: "reserved-character-breakout" + - macro: "{SKU}" + catalog_item_id: "nested_expansion" + vector_name: "nested-expansion-preserved-as-literal" + - macro: "{SKU}" + catalog_item_id: "non_ascii" + vector_name: "non-ascii-utf8-percent-encoding" + - id: create_buy + title: "Create catalog-driven media buy" + narrative: | + The buyer creates a media buy with catalog-driven packages. Instead of assigning + individual creatives, the buyer references the synced catalog. Your platform + renders the right catalog items dynamically based on user context, intent signals, + and inventory availability. + + The key schema difference: packages include a catalogs[] array instead of (or + alongside) creative_assignments[]. + + steps: + - id: get_products + title: "Discover catalog-compatible products" + narrative: | + The buyer finds products that support catalog-driven delivery. These products + accept catalog references and render dynamic ads from the feed. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products that support catalog-driven creative. Products should indicate + catalog compatibility in their format requirements. + + sample_request: + buying_mode: "brief" + brief: "Dynamic product ads for a high-end steakhouse. Geo-targeted to 10 miles around Amsterdam location. Drive reservations and foot traffic." + account: + brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "sales_catalog_driven--get_products" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains products" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--get_products" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: create_media_buy + title: "Create media buy with catalog packages" + narrative: | + The buyer creates the media buy with packages that reference the synced catalog. + Each package has a catalogs[] array specifying which catalog feeds drive the + dynamic creative for that line item. + + Your platform optimizes across catalog items within each package's budget + envelope — showing the ribeye to steak lovers and the seafood tower to + seafood enthusiasts. + 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: | + Create the media buy with catalog-driven packages. Return: + - media_buy_id + - status: active + - packages with catalog references preserved + + sample_request: + brand: + domain: "amsterdam-steakhouse.example" + account: + brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-07T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "local_display_dynamic" + pricing_option_id: "cpm_standard" + budget: 5000 + catalogs: + - catalog_id: "menu_spring_2026" + type: "product" + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_create_buy_create_media_buy" + context: + correlation_id: "sales_catalog_driven--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: event_setup + title: "Conversion tracking setup" + narrative: | + The buyer configures event sources so your platform can attribute conversions to + catalog items. For a restaurant, the events are reservations and walk-ins. For + retail, they are purchases and add-to-carts. + + Your platform returns setup snippets (pixel code, SDK instructions) that the + buyer installs on their conversion surfaces. + + steps: + - id: sync_event_sources + title: "Configure event sources" + narrative: | + The buyer tells your platform where conversion events will come from — + a website pixel, a mobile SDK, or a server-to-server integration. Your + platform returns the integration code. + task: sync_event_sources + schema_ref: "media-buy/sync-event-sources-request.json" + response_schema_ref: "media-buy/sync-event-sources-response.json" + doc_ref: "/media-buy/task-reference/sync_event_sources" + stateful: true + expected: | + Return event sources with: + - event_source_id and seller_id + - setup.snippet: integration code (JavaScript pixel, HTML tag, or pixel URL) + - setup.instructions: human-readable integration guide + - action: created or updated + + sample_request: + account: + brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + event_sources: + - event_source_id: "amsterdam_website" + name: "Amsterdam Steakhouse Website" + event_types: ["purchase", "add_to_cart", "page_view", "lead"] + allowed_domains: ["amsterdam-steakhouse.example", "book.amsterdam-steakhouse.example"] + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_event_setup_sync_event_sources" + context: + correlation_id: "sales_catalog_driven--sync_event_sources" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches sync-event-sources-response.json schema" + - check: field_present + path: "event_sources[0].setup.snippet" + description: "Event source includes setup snippet" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--sync_event_sources" + description: "Context correlation_id returned unchanged" + - id: conversion_tracking + title: "Log conversions" + narrative: | + The campaign is running and customers are converting. The buyer logs conversion + events back to your platform, attributing them to catalog items via content_ids. + + When someone books a reservation after seeing the ribeye ad, the event includes + content_ids: ["ribeye_36oz"] — linking the conversion to the catalog item that + drove it. Your platform uses this signal to optimize which items to show. + + steps: + - id: log_events + title: "Send conversion events" + narrative: | + The buyer sends a batch of conversion events. Each event includes the event + type, value, and content_ids linking to catalog items. Your platform processes + these for attribution and optimization. + task: log_event + schema_ref: "media-buy/log-event-request.json" + response_schema_ref: "media-buy/log-event-response.json" + doc_ref: "/media-buy/task-reference/log_event" + stateful: true + expected: | + Process the events and return: + - events_received and events_processed counts + - partial_failures for events that failed validation + - match_quality: how well events matched to ad exposures (0.0-1.0) + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + event_source_id: "amsterdam_website" + events: + - event_id: "evt_001" + event_type: "purchase" + event_time: "2026-04-15T19:30:00Z" + content_ids: ["ribeye_36oz"] + value: 89.00 + currency: "USD" + - event_id: "evt_002" + event_type: "lead" + event_time: "2026-04-15T20:15:00Z" + content_ids: ["wagyu_flight"] + value: 195.00 + currency: "USD" + - event_id: "evt_003" + event_type: "page_view" + event_time: "2026-04-15T20:45:00Z" + content_ids: ["seafood_tower"] + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_conversion_tracking_log_events" + context: + correlation_id: "sales_catalog_driven--log_events" + validations: + - check: response_schema + description: "Response matches log-event-response.json schema" + - check: field_present + path: "events_received" + description: "Response reports events received" + - check: field_present + path: "match_quality" + description: "Response includes match quality score" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--log_events" + description: "Context correlation_id returned unchanged" + - id: optimization_loop + title: "Performance feedback and optimization" + narrative: | + The buyer closes the optimization loop by telling your platform how the campaign + is performing against their goals. A performance_index above 1.0 means the + campaign is exceeding expectations — your platform should maintain or increase + delivery. Below 1.0 means underperforming — your platform should adjust targeting, + item selection, or pacing. + + steps: + - id: provide_feedback + title: "Submit performance feedback" + narrative: | + The buyer reports that the campaign is driving 1.4x the expected reservation + rate. Your platform uses this signal to optimize delivery — showing more of + the high-performing catalog items and adjusting bid strategies. + task: provide_performance_feedback + schema_ref: "media-buy/provide-performance-feedback-request.json" + response_schema_ref: "media-buy/provide-performance-feedback-response.json" + doc_ref: "/media-buy/task-reference/provide_performance_feedback" + stateful: true + expected: | + Acknowledge the feedback. The seller should adjust delivery optimization + based on the performance_index signal. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + measurement_period: + start: "2026-04-07T00:00:00Z" + end: "2026-04-14T23:59:59Z" + performance_index: 1.4 + metric_type: "conversion_rate" + feedback_source: "buyer_attribution" + + idempotency_key: "$generate:uuid_v4#sales_catalog_driven_optimization_loop_provide_feedback" + context: + correlation_id: "sales_catalog_driven--provide_feedback" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches provide-performance-feedback-response.json schema" + - check: field_value + path: "success" + value: true + description: "Feedback accepted" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--provide_feedback" + description: "Context correlation_id returned unchanged" + - id: check_delivery + title: "Monitor delivery with catalog attribution" + narrative: | + The buyer checks delivery metrics to see which catalog items are driving + performance and how spend is allocated across the product feed. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics including impressions, clicks, spend, and + conversion data attributed to catalog items. + + sample_request: + account: + brand: + domain: "amsterdam-steakhouse.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "sales_catalog_driven--check_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_catalog_driven--check_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/sales-guaranteed/index.yaml b/dist/compliance/3.0.1/specialisms/sales-guaranteed/index.yaml new file mode 100644 index 0000000000..81bad5d0ed --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/sales-guaranteed/index.yaml @@ -0,0 +1,504 @@ +id: sales_guaranteed +version: "1.0.0" +title: "Guaranteed media buy with human IO approval" +protocol: media-buy +category: sales_guaranteed +summary: "Seller agent that requires human-in-the-loop IO signing before guaranteed media buys go live." +track: media_buy +required_tools: + - get_products + - create_media_buy +requires_scenarios: + - media_buy_seller/refine_products + - media_buy_seller/delivery_reporting + - media_buy_seller/measurement_terms_rejected + - media_buy_seller/pending_creatives_to_start + - media_buy_seller/inventory_list_targeting + - media_buy_seller/inventory_list_no_match + - media_buy_seller/invalid_transitions + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. active → pending_creatives on a media_buy. +invariants: + - status.monotonic + +narrative: | + You run a sell-side platform that requires human approval before guaranteed media buys go + live. When a buyer creates a guaranteed buy, your platform returns an A2A task in the + submitted state with a task_id — no media_buy_id is issued yet because IO signing may fail. + A human reviewer on your side reviews the deal terms and signs the IO through your own + internal workflow. + + The buyer either polls tasks/get with the task_id or configures a push_notification_config + webhook to receive a callback when IO signing completes. Only on task completion does your + platform issue a media_buy_id and the final CreateMediaBuy result; the buyer then calls + get_media_buys to confirm the buy is active and sync creatives. + + This storyboard isolates the guaranteed approval path — the async handshake between agent + automation and human decision-making that makes guaranteed buys work in practice. IO review + is modelled entirely at the A2A task layer; there is no interim "pending_approval" media buy + status (that value only exists on Account.status, not MediaBuy.status). + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - supports_guaranteed + - requires_io_approval + examples: + - "Premium publisher with IO requirements" + - "Retail media network with sales approval" + - "CTV platform with guaranteed deals" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials for account setup. + The test kit provides a sample brand (Acme Outdoor) with campaign parameters + suitable for testing the guaranteed approval flow. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "sports_preroll_q2_guaranteed" + delivery_type: "guaranteed" + channels: ["video"] + format_ids: + - id: "video_30s" + - product_id: "outdoor_ctv_q2_guaranteed" + delivery_type: "guaranteed" + channels: ["ctv"] + format_ids: + - id: "video_30s" + pricing_options: + - product_id: "sports_preroll_q2_guaranteed" + pricing_option_id: "cpm_guaranteed_fixed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 35.0 + - product_id: "outdoor_ctv_q2_guaranteed" + pricing_option_id: "cpm_guaranteed_fixed" + pricing_model: "cpm" + currency: "USD" + fixed_price: 45.0 + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "sales_guaranteed--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: "sales_guaranteed--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account setup" + narrative: | + Before buying anything, the buyer establishes an account relationship with + your platform. This is the handshake: the buyer tells you which brand and + agency (operator) they represent, and you return an account ID, status, and + any setup requirements. + + steps: + - id: sync_accounts + title: "Establish account relationship" + narrative: | + The buyer registers their brand and operator with your platform. This is + the first call in any new relationship. Your platform validates the request, + provisions the account, and returns its status. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with: + - account_id: your platform's identifier for this relationship + - action: created or updated + - status: active or pending_approval + - account_scope: operator, brand, operator_brand, or agent + - setup: URL and message if pending_approval + - payment_terms: net_30, prepay, etc. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#sales_guaranteed_account_setup_sync_accounts" + context: + correlation_id: "sales_guaranteed--sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + - check: field_present + path: "accounts[0].status" + description: "Account has a status (active or pending_approval)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_guaranteed--sync_accounts" + description: "Context correlation_id returned unchanged" + - id: product_discovery + title: "Discover guaranteed products" + narrative: | + The buyer sends a brief to discover your guaranteed inventory. The emphasis + here is on products with delivery_type: guaranteed — fixed-price, reserved + inventory that requires an IO commitment. Your platform returns products with + pricing, delivery forecasts, and SLA commitments. + + steps: + - id: get_products_brief + title: "Send a brief targeting guaranteed inventory" + narrative: | + The buyer describes what they want, emphasizing guaranteed delivery. Your + platform returns products with delivery_type: guaranteed, including SLA + commitments, minimum spend requirements, and IO terms. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return guaranteed products matching the brief. Each product should include: + - product_id: unique identifier + - name and description + - delivery_type: guaranteed + - pricing_models: fixed CPM or flat-rate pricing + - forecast: committed impressions with SLA guarantees + - creative_format_ids: required creative formats + - minimum_spend or commitment terms if applicable + + sample_request: + buying_mode: "brief" + brief: "Guaranteed premium video on sports and outdoor lifestyle publishers. Q2 flight, $50K budget. Adults 25-54, US only. Need completion rate SLA." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "sales_guaranteed--get_products_brief" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + - check: field_present + path: "products[0].product_id" + description: "Each product has a product_id" + - check: field_present + path: "products[0].delivery_type" + description: "Each product declares delivery_type: guaranteed" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_guaranteed--get_products_brief" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: create_buy_submitted + title: "Create guaranteed buy (task submitted for approval)" + narrative: | + The buyer creates a guaranteed media buy. Because your platform requires human + IO signing, the A2A task transitions to submitted rather than completed. The + buyer gets back a task_id and configures a webhook (or polls tasks/get) to be + notified when IO review finishes. + + steps: + - id: create_media_buy + title: "Create a guaranteed media buy" + narrative: | + The buyer commits to guaranteed products with budgets and flight dates. Your + platform accepts the request but does not create the media buy yet. Instead, + the A2A task enters the submitted state — no media_buy_id is issued because + IO signing may fail. The buyer receives a task_id to watch. + + The buyer includes push_notification_config so your platform can call back + when the IO is signed (completed) or rejected (failed). + 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 an A2A task envelope in submitted state: + - status: submitted (task-level — the CreateMediaBuy success artifact is not yet produced) + - task_id / taskId: the handle the buyer polls or receives webhooks on + - message (optional): human-readable explanation (e.g., "Awaiting IO signature from sales team; typical turnaround 2–4 hours") + + Do NOT return media_buy_id or packages yet — those land on the task's final artifact + when the task transitions to completed. Do NOT return completed status for guaranteed + buys that require IO signing. Do NOT use a "pending_approval" media buy status; that + value is not in MediaBuy.status — IO review is modelled at the task layer only. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_preroll_q2_guaranteed" + budget: 30000 + pricing_option_id: "cpm_guaranteed_fixed" + creative_assignments: + - creative_id: "video_30s_trail_pro" + - product_id: "outdoor_ctv_q2_guaranteed" + budget: 20000 + pricing_option_id: "cpm_guaranteed_fixed" + push_notification_config: + url: "https://buyer.example/webhooks/adcp" + authentication: + schemes: + - "HMAC-SHA256" + credentials: "sales-guaranteed-webhook-secret-token" + + idempotency_key: "$generate:uuid_v4#sales_guaranteed_create_buy_submitted_create_media_buy" + context: + correlation_id: "sales_guaranteed--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_guaranteed--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: confirm_active + title: "Confirm active after IO signing" + narrative: | + The human on your side reviews and signs the IO through your internal workflow. + Your platform then transitions the A2A task to completed and emits the final + CreateMediaBuy result — including the newly-issued media_buy_id — to the buyer's + push_notification webhook (or to the next tasks/get poll). The buyer now calls + get_media_buys with that media_buy_id and sees the buy active. There is no + intermediate "pending_approval" media buy status in this flow; the buy does not + exist as a queryable MediaBuy until the task completes. + + steps: + - id: get_media_buys_active + title: "Check media buy status (active)" + narrative: | + After the task completes and your platform issues a media_buy_id, the buyer + calls get_media_buys to confirm the buy is live. Your platform returns active + (or pending_creatives when creatives are still outstanding), indicating the + buy is approved and inventory is reserved. + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Return the media buy in active status: + - media_buy_id: matches the buy created earlier + - status: active (IO has been signed) + - confirmed_at: timestamp when the IO was signed + - packages: line items with reserved inventory + - valid_actions: updated for active state (creative sync, pause, etc.) + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "sales_guaranteed--get_media_buys_active" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_present + path: "media_buys[0].status" + description: "Media buy status is active" + - check: field_present + path: "media_buys[0].confirmed_at" + description: "Active buy includes a confirmed_at timestamp" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_guaranteed--get_media_buys_active" + description: "Context correlation_id returned unchanged" + - id: creative_sync + title: "Creative sync" + narrative: | + With the IO signed and the media buy active, the buyer syncs creative assets + to your platform. Each package has creative format requirements that the buyer + discovered during product discovery. + + steps: + - id: sync_creatives + title: "Push creative assets" + narrative: | + The buyer uploads creative assets for the confirmed packages. Your platform + validates each creative against the format specs and returns per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept and validate creatives: + - Per-creative action: created or updated + - Per-creative status: accepted, pending_review, or rejected + - Validation errors for rejected creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "video_30s_trail_pro" + name: "Trail Pro 3000 - 30s CTV Spot" + format_id: + agent_url: "https://your-platform.example.com" + id: "ssai_30s" + assets: + video: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + width: 1920 + height: 1080 + duration_ms: 30000 + mime_type: "video/mp4" + + idempotency_key: "$generate:uuid_v4#sales_guaranteed_creative_sync_sync_creatives" + context: + correlation_id: "sales_guaranteed--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_guaranteed--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: delivery_monitoring + title: "Delivery and reporting" + narrative: | + The campaign is live with guaranteed delivery commitments. The buyer monitors + delivery to ensure the seller is meeting the SLA guarantees from the IO. + + steps: + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data for the active guaranteed media buy. Your + platform returns performance metrics with pacing against the guaranteed + commitment. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics for the guaranteed media buy: + - Per-package delivery: impressions, clicks, spend, completion rates + - Pacing against guaranteed commitment: on track, ahead, behind + - Budget utilization: spent vs. committed + - SLA compliance: completion rate vs. guaranteed threshold + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "sales_guaranteed--get_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_guaranteed--get_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/sales-non-guaranteed/index.yaml b/dist/compliance/3.0.1/specialisms/sales-non-guaranteed/index.yaml new file mode 100644 index 0000000000..517fcd67fa --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/sales-non-guaranteed/index.yaml @@ -0,0 +1,428 @@ +id: sales_non_guaranteed +version: "1.0.0" +title: "Non-guaranteed auction-based media buy" +protocol: media-buy +category: sales_non_guaranteed +summary: "Seller agent for auction-based, non-guaranteed buying where the buyer sets bid prices and budgets." +track: media_buy +required_tools: + - get_products + - create_media_buy +requires_scenarios: + - media_buy_seller/delivery_reporting + - media_buy_seller/pending_creatives_to_start + - media_buy_seller/inventory_list_targeting + - media_buy_seller/inventory_list_no_match + - media_buy_seller/invalid_transitions + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. active → pending_creatives on a media_buy. +invariants: + - status.monotonic + +narrative: | + You run a sell-side platform with auction-based inventory. Non-guaranteed buys don't + require IOs or human approval — the buyer sets a bid price and budget, and your platform + runs the auction. Delivery is best-effort based on bid competitiveness. + + The buyer discovers products with floor prices and bid guidance, creates a buy with bid + prices per package, monitors win rates and pacing, and adjusts bids or budgets in-flight + to optimize performance. + + This storyboard covers the non-guaranteed buying path — fast setup, no human approval, + real-time optimization through bid and budget adjustments. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - supports_non_guaranteed + - auction_based + examples: + - "SSP with programmatic auction inventory" + - "Exchange-based publisher platform" + - "Open marketplace seller" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials. Non-guaranteed buys + typically have lower barriers to entry — no IO signing required. The test kit + provides a sample brand (Acme Outdoor) with bid parameters suitable for testing + the auction-based flow. + test_kit: "test-kits/acme-outdoor.yaml" + controller_seeding: true + +fixtures: + products: + - product_id: "sports_display_auction" + delivery_type: "non_guaranteed" + channels: ["display"] + format_ids: + - id: "display_300x250" + - product_id: "outdoor_video_auction" + delivery_type: "non_guaranteed" + channels: ["video"] + format_ids: + - id: "video_30s" + pricing_options: + - product_id: "sports_display_auction" + pricing_option_id: "cpm_auction" + pricing_model: "cpm" + currency: "USD" + floor_price: 5.0 + - product_id: "outdoor_video_auction" + pricing_option_id: "cpm_auction" + pricing_model: "cpm" + currency: "USD" + floor_price: 15.0 + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "sales_non_guaranteed--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: "sales_non_guaranteed--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: product_discovery + title: "Discover auction-based products" + narrative: | + The buyer sends a brief to discover your non-guaranteed inventory. Products + come back with delivery_type: non_guaranteed, floor prices, and bid guidance + that helps the buyer set competitive bids. + + steps: + - id: get_products_brief + title: "Send a brief for non-guaranteed inventory" + narrative: | + The buyer describes what they want. Your platform returns non-guaranteed + products with auction mechanics: floor prices, recommended bid ranges, + estimated win rates at different bid levels, and available audience segments. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return non-guaranteed products matching the brief. Each product should include: + - product_id: unique identifier + - name and description + - delivery_type: non_guaranteed + - pricing_models: auction-based pricing with floor_price and recommended bid range + - forecast: estimated impressions at different bid levels (best-effort) + - creative_format_ids: required creative formats + - targeting: available audiences and contexts + + sample_request: + buying_mode: "brief" + brief: "Display and video inventory across sports and outdoor lifestyle sites. Q2 flight, $25K budget. Adults 25-54, US. Auction-based, looking for competitive CPMs." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "sales_non_guaranteed--get_products_brief" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + - check: field_present + path: "products[0].product_id" + description: "Each product has a product_id" + - check: field_present + path: "products[0].delivery_type" + description: "Each product declares delivery_type: non_guaranteed" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_non_guaranteed--get_products_brief" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: create_buy + title: "Create non-guaranteed buy" + narrative: | + The buyer creates a media buy with bid prices per package. Since this is + non-guaranteed, no IO is required and no human approval is needed. The response + comes back as completed — the buy is immediately active and your platform starts + bidding in auctions on the buyer's behalf. + + steps: + - id: create_media_buy + title: "Create a media buy with bid prices" + narrative: | + The buyer commits to non-guaranteed products with bid prices and budgets. + Your platform validates the bids against floor prices, confirms the buy + immediately (completed status), and begins auction participation. + + Each package includes a bid_price that the buyer is willing to pay per unit + (CPM, CPC, etc.). The platform uses this to compete in auctions. + 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 media buy in completed status: + - media_buy_id: your platform's identifier + - status: active (no async approval needed) + - confirmed_at: timestamp + - packages: confirmed line items with bid prices acknowledged + - valid_actions: pause, update_bid, get_delivery + + Bids below floor_price should be rejected with a clear error. + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_display_auction" + budget: 10000 + bid_price: 8.50 + pricing_option_id: "cpm_auction" + creative_assignments: + - creative_id: "display_trail_pro_300x250" + - product_id: "outdoor_video_auction" + budget: 15000 + bid_price: 22.00 + pricing_option_id: "cpm_auction" + creative_assignments: + - creative_id: "video_30s_trail_pro" + + idempotency_key: "$generate:uuid_v4#sales_non_guaranteed_create_buy_create_media_buy" + context: + correlation_id: "sales_non_guaranteed--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + - name: first_package_id + path: "packages[0].package_id" + - name: second_package_id + path: "packages[1].package_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_non_guaranteed--create_media_buy" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "packages[0].package_id" + description: "Seller assigns package_id — must be echoed in update_media_buy" + - id: monitor_pacing + title: "Monitor win rates and pacing" + narrative: | + The non-guaranteed buy is live and bidding in auctions. The buyer checks + pacing to understand how competitive their bids are and whether the budget + is spending at the desired rate. + + steps: + - id: get_media_buys_pacing + title: "Check buy status and pacing" + narrative: | + The buyer polls for the media buy status. Your platform returns the active + buy with pacing data — how much budget has been spent, win rates, and whether + the buy is on pace to exhaust the budget by the end of the flight. + task: get_media_buys + schema_ref: "media-buy/get-media-buys-request.json" + response_schema_ref: "media-buy/get-media-buys-response.json" + doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Return the media buy with pacing data: + - media_buy_id: matches the buy created earlier + - status: active + - packages: line items with current spend, win rate, and pacing status + - valid_actions: update, pause, get_delivery + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + + context: + correlation_id: "sales_non_guaranteed--get_media_buys_pacing" + validations: + - check: response_schema + description: "Response matches get-media-buys-response.json schema" + - check: field_present + path: "media_buys[0].status" + description: "Media buy has a status" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_non_guaranteed--get_media_buys_pacing" + description: "Context correlation_id returned unchanged" + - id: adjust_bids + title: "Adjust bids and budget" + narrative: | + Based on pacing data, the buyer adjusts bids or budgets in-flight. If a package + is underspending (low win rate), the buyer increases the bid. If a package is + overspending, the buyer decreases the bid or caps the daily budget. + + steps: + - id: update_media_buy + title: "Update bid prices and budget" + narrative: | + The buyer modifies the active media buy — adjusting bid prices, reallocating + budget between packages, or changing daily spend caps. Your platform applies + the updates immediately to the live auction participation. + task: update_media_buy + schema_ref: "media-buy/update-media-buy-request.json" + response_schema_ref: "media-buy/update-media-buy-response.json" + doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle + stateful: true + expected: | + Apply the updates and return the modified media buy: + - media_buy_id: matches the existing buy + - status: active (still running) + - packages: updated line items reflecting new bids and budgets + - Changes take effect immediately for subsequent auctions + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_id: "$context.media_buy_id" + packages: + - package_id: "$context.first_package_id" + bid_price: 10.00 + budget: 12000 + - package_id: "$context.second_package_id" + bid_price: 20.00 + budget: 13000 + + idempotency_key: "$generate:uuid_v4#sales_non_guaranteed_adjust_bids_update_media_buy" + context: + correlation_id: "sales_non_guaranteed--update_media_buy" + validations: + - check: response_schema + description: "Response matches update-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_non_guaranteed--update_media_buy" + description: "Context correlation_id returned unchanged" + - id: delivery + title: "Delivery and auction metrics" + narrative: | + The buyer reviews delivery data including auction-specific metrics: win rates, + average clearing prices, and bid competitiveness across packages. + + steps: + - id: get_delivery + title: "Check delivery with auction metrics" + narrative: | + The buyer requests delivery data for the active non-guaranteed media buy. + Your platform returns standard delivery metrics plus auction-specific data: + win rates, average clearing prices, bid-to-win ratios, and budget pacing. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics for the non-guaranteed media buy: + - Per-package delivery: impressions, clicks, spend + - Win rate: percentage of auctions won per package + - Average clearing price: what the buyer actually paid vs. bid price + - Pacing: on track, ahead, behind relative to budget and flight dates + - Budget utilization: spent vs. committed + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "sales_non_guaranteed--get_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_non_guaranteed--get_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/sales-proposal-mode/index.yaml b/dist/compliance/3.0.1/specialisms/sales-proposal-mode/index.yaml new file mode 100644 index 0000000000..5b26504dcc --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/sales-proposal-mode/index.yaml @@ -0,0 +1,520 @@ +id: sales_proposal_mode +version: "1.0.0" +title: "Media buy via proposal acceptance" +protocol: media-buy +category: sales_proposal_mode +summary: "Seller agent that generates curated media plan proposals the buyer can review, refine, and accept." +track: media_buy +required_tools: + - get_products + - create_media_buy +requires_scenarios: + - media_buy_seller/proposal_finalize + - media_buy_seller/delivery_reporting + - media_buy_seller/measurement_terms_rejected + - media_buy_seller/pending_creatives_to_start + - media_buy_seller/inventory_list_targeting + - media_buy_seller/inventory_list_no_match + - media_buy_seller/invalid_transitions + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. committed → draft on a proposal or active → +# pending_creatives on a media_buy. +invariants: + - status.monotonic + +narrative: | + Your seller generates curated media plan proposals. The buyer sends a brief, your platform + returns products alongside proposals — curated bundles with budget allocations and rationale + for each recommendation. The buyer reviews, optionally refines, and then accepts a proposal + as-is instead of manually building packages. + + Proposal mode is the recommended flow for buyers who trust the seller's product + recommendations. Instead of the buyer cherry-picking individual products and assembling + packages, the seller's AI builds an optimized media plan that the buyer can accept with a + single call. + + This storyboard walks through the proposal lifecycle: brief, proposal generation, optional + refinement, acceptance, creative sync, and delivery monitoring. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - generates_proposals + examples: + - "Full-service publisher with AI planning" + - "Retail media network with curated packages" + - "Premium video platform with proposal engine" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and operator credentials for account setup. + The test kit provides a sample brand (Acme Outdoor) with campaign parameters + suitable for testing the proposal acceptance flow. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before sending briefs or creating buys. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "sales_proposal_mode--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: "sales_proposal_mode--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account setup" + narrative: | + The buyer establishes an account relationship with your platform before + requesting proposals. + + steps: + - id: sync_accounts + title: "Establish account relationship" + narrative: | + The buyer registers their brand and operator with your platform. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the account with: + - account_id: your platform's identifier for this relationship + - action: created or updated + - status: active or pending_approval + - payment_terms: net_30, prepay, etc. + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + + idempotency_key: "$generate:uuid_v4#sales_proposal_mode_account_setup_sync_accounts" + context: + correlation_id: "sales_proposal_mode--sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_proposal_mode--sync_accounts" + description: "Context correlation_id returned unchanged" + - id: brief_with_proposals + title: "Brief with proposals" + narrative: | + The buyer sends a brief. Your platform returns products and — because you support + proposal mode — also returns proposals: curated media plans with budget allocations, + product selections, and rationale for each recommendation. The buyer can review + multiple proposals side by side. + + steps: + - id: get_products_brief + title: "Send a brief and receive proposals" + narrative: | + The buyer describes what they want. Your platform returns products alongside + one or more proposals. Each proposal bundles products with budget allocations + and explains why those products were selected and how the budget was distributed. + + Proposals give the buyer a ready-to-accept media plan instead of requiring + manual package assembly. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: false + expected: | + Return products and proposals matching the brief: + - products: individual products with pricing and forecasts + - proposals: curated media plans, each containing: + - proposal_id: unique identifier for this proposal + - name: descriptive label (e.g., "Balanced Reach Plan") + - budget_allocations: how the total budget is split across products + - rationale: why these products were selected and how the budget was distributed + - total_budget: sum of allocations + - forecast: aggregate impressions, reach, frequency + + sample_request: + buying_mode: "brief" + brief: "Premium video and display across outdoor lifestyle and sports. Q2 flight, $50K total budget. Adults 25-54, US and Canada. Looking for a balanced plan across CTV, online video, and display." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "sales_proposal_mode--get_products_brief" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "products" + description: "Response contains a products array" + - check: field_present + path: "proposals" + description: "Response contains a proposals array" + - check: field_present + path: "proposals[0].proposal_id" + description: "Each proposal has a proposal_id" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_proposal_mode--get_products_brief" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: review_refine + title: "Refine a proposal" + narrative: | + The buyer reviews the proposals and wants to adjust one. They call get_products + in refine mode targeting a specific proposal_id. The refinement might adjust + budget splits, swap a product, or add constraints. Your platform returns the + updated proposal. + + steps: + - id: get_products_refine + title: "Refine a specific proposal" + narrative: | + The buyer targets a specific proposal for refinement. They reference the + proposal_id and describe what they want to change. Your platform applies + the refinements to that proposal and returns the updated version. + task: get_products + schema_ref: "media-buy/get-products-request.json" + response_schema_ref: "media-buy/get-products-response.json" + doc_ref: "/media-buy/task-reference/get_products" + comply_scenario: full_sales_flow + stateful: true + expected: | + Return the refined proposal: + - proposals: updated proposal reflecting the requested changes + - refinement_applied: how each refinement was handled + - Updated budget allocations, product selections, and forecasts + - products: updated product set if products were swapped + + sample_request: + buying_mode: "refine" + refine: + - scope: "proposal" + proposal_id: "balanced_reach_q2" + ask: "Shift 60% of budget to CTV. Drop the display product and redistribute that budget to video." + - scope: "request" + ask: "All products must support frequency capping at 3 per day." + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "sales_proposal_mode--get_products_refine" + validations: + - check: response_schema + description: "Response matches get-products-response.json schema" + - check: field_present + path: "proposals" + description: "Response contains updated proposals" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_proposal_mode--get_products_refine" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "products[0].format_ids" + description: "Products include format_ids for creative requirements" + - check: field_present + path: "products[0].format_ids[0].agent_url" + description: "Format IDs include agent_url — must match this agent's URL" + - check: field_present + path: "products[0].format_ids[0].id" + description: "Format IDs include id — must be accepted back in sync_creatives" + - id: accept_proposal + title: "Accept the proposal" + narrative: | + The buyer is satisfied with the refined proposal and accepts it by creating a + media buy with the proposal_id. Instead of specifying individual packages, the + buyer passes the proposal_id and total_budget. Your platform converts the proposal + into an active media buy with the exact product selections and budget allocations + from the proposal. + + steps: + - id: create_media_buy + title: "Create a media buy from proposal" + narrative: | + The buyer accepts a proposal by passing proposal_id to create_media_buy. The + buyer does NOT specify a packages array — the platform uses the proposal's + product selections and budget allocations to build the packages automatically. + + This is the key difference from manual package creation: the buyer trusts the + seller's recommendation and accepts the plan as-is. + 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: | + Convert the proposal into an active media buy: + - media_buy_id: your platform's identifier + - status: active + - confirmed_at: timestamp + - packages: line items derived from the proposal's budget allocations + - proposal_id: echoed back to confirm which proposal was accepted + - valid_actions: creative sync, pause, get_delivery + + sample_request: + brand: + domain: "acmeoutdoor.example" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + proposal_id: "balanced_reach_q2" + total_budget: + amount: 50000 + currency: "USD" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + + idempotency_key: "$generate:uuid_v4#sales_proposal_mode_accept_proposal_create_media_buy" + context: + correlation_id: "sales_proposal_mode--create_media_buy" + context_outputs: + - name: media_buy_id + path: "media_buy_id" + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_proposal_mode--create_media_buy" + description: "Context correlation_id returned unchanged" + - id: creative_sync + title: "Creative sync" + narrative: | + With the proposal accepted and the media buy confirmed, the buyer syncs creative + assets. The buyer first checks what formats the accepted products require, then + pushes matching creative assets. + + steps: + - id: list_formats + title: "Check creative format requirements" + narrative: | + The buyer confirms what creative formats the accepted proposal's products + require. Your platform returns format specs with asset requirements. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return creative formats your platform accepts. Each format should define: + - format_id with your agent_url and unique id + - Asset requirements (dimensions, file sizes, mime types) + - Render dimensions + + sample_request: + context: + correlation_id: "sales_proposal_mode--list_formats" + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_proposal_mode--list_formats" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "formats[0].format_id.agent_url" + description: "Format IDs include agent_url" + - check: field_present + path: "formats[0].format_id.id" + description: "Format IDs include id — must match those in get_products" + - id: sync_creatives + title: "Push creative assets" + narrative: | + The buyer uploads creative assets for the products in the accepted proposal. + Your platform validates each creative against the format specs and returns + per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept and validate creatives: + - Per-creative action: created or updated + - Per-creative status: accepted, pending_review, or rejected + - Validation errors for rejected creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "video_30s_trail_pro" + name: "Trail Pro 3000 - 30s CTV Spot" + format_id: + agent_url: "https://your-platform.example.com" + id: "ssai_30s" + assets: + video: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + width: 1920 + height: 1080 + duration_ms: 30000 + mime_type: "video/mp4" + - creative_id: "video_15s_trail_pro" + name: "Trail Pro 3000 - 15s Online Video" + format_id: + agent_url: "https://your-platform.example.com" + id: "preroll_15s" + assets: + video: + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-15s.mp4" + width: 1920 + height: 1080 + duration_ms: 15000 + mime_type: "video/mp4" + + idempotency_key: "$generate:uuid_v4#sales_proposal_mode_creative_sync_sync_creatives" + context: + correlation_id: "sales_proposal_mode--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "creatives[0].action" + description: "Each creative has an action (created/updated)" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_proposal_mode--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: delivery + title: "Delivery and reporting" + narrative: | + The campaign from the accepted proposal is live. The buyer monitors delivery + to verify the proposal's forecasts are tracking. + + steps: + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data for the media buy created from the proposal. + Your platform returns performance metrics that the buyer can compare against + the proposal's original forecasts. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow + stateful: true + expected: | + Return delivery metrics for the media buy: + - Per-package delivery: impressions, clicks, spend, completion rates + - Pacing against the proposal's original forecast + - Budget utilization: spent vs. committed per package + - Daily breakdown if requested + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + media_buy_ids: + - "$context.media_buy_id" + include_package_daily_breakdown: true + + context: + correlation_id: "sales_proposal_mode--get_delivery" + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buy_deliveries" + description: "Response contains media buy delivery data" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_proposal_mode--get_delivery" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/sales-social/index.yaml b/dist/compliance/3.0.1/specialisms/sales-social/index.yaml new file mode 100644 index 0000000000..269300549c --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/sales-social/index.yaml @@ -0,0 +1,577 @@ +id: sales_social +version: "1.0.0" +title: "Social platform" +protocol: media-buy +category: sales_social +summary: "Social media platform that accepts audience segments, native creatives, and conversion events from buyer agents." +track: audiences +required_tools: + - sync_audiences + - sync_catalogs + - sync_creatives + - sync_event_sources + +# Cross-step assertion (adcp#2664). status.monotonic rejects resource +# status transitions observed across steps that aren't on the spec +# lifecycle graph — e.g. approved → processing on a creative asset or +# active → pending_creatives on a media_buy. +invariants: + - status.monotonic + +narrative: | + You run a social media platform — Snap, Meta, TikTok, Pinterest, or any walled garden that + sells advertising through audience-based targeting and native creative formats. A buyer agent + connects to set up an account, push audience segments, sync native creatives, track conversion + events, and monitor spend. + + Unlike open-web media buys, social platforms require the buyer to push assets into the + platform's environment. Audiences are activated via sync_audiences, creatives are pushed via + sync_creatives in platform-native formats, and conversion events flow back via log_event. + + This storyboard covers the social platform integration from the buyer's perspective: + account setup, audience activation, native creative push, event tracking, and financial + monitoring. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - supports_non_guaranteed + examples: + - "Snap" + - "Meta" + - "TikTok" + - "Pinterest" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, audience segment definitions, + and native creative assets. The test kit provides a sample brand with creative assets + suitable for social formats. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports media buying before syncing audiences and native creatives. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media. + sample_request: + context: + correlation_id: "sales_social--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: "sales_social--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: account_setup + title: "Account setup" + narrative: | + The buyer establishes an account with the social platform and verifies its status. + Social platforms often require advertiser verification before accepting ad spend. + + steps: + - id: sync_accounts + title: "Register advertiser account" + narrative: | + The buyer registers their brand and operator with the social platform. The platform + provisions an advertiser account and returns its status. Social platforms may require + identity verification before the account goes active. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the advertiser account with: + - account_id: platform's identifier + - status: active or pending_approval (if verification required) + - account_scope: how the platform scopes this relationship + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + + idempotency_key: "$generate:uuid_v4#sales_social_account_setup_sync_accounts" + context: + correlation_id: "sales_social--sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--sync_accounts" + description: "Context correlation_id returned unchanged" + - id: list_accounts + title: "Verify account status" + narrative: | + The buyer checks which accounts exist on the platform and their current status. + This confirms the account is active and shows any pending setup requirements. + task: list_accounts + schema_ref: "account/list-accounts-request.json" + response_schema_ref: "account/list-accounts-response.json" + doc_ref: "/accounts/tasks/list_accounts" + stateful: true + expected: | + Return accounts matching the query: + - accounts array with status, account_id, brand, operator + - Active accounts ready for ad operations + - Pending accounts with accounts[].setup.url populated if verification is needed + + sample_request: + context: + correlation_id: "sales_social--list_accounts" + validations: + - check: response_schema + description: "Response matches list-accounts-response.json schema" + - check: field_present + path: "accounts" + description: "Response contains accounts array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--list_accounts" + description: "Context correlation_id returned unchanged" + - id: audience_sync + title: "Audience activation" + narrative: | + The buyer pushes audience segments to the platform. Social platforms use these segments + for targeting — the buyer defines who to reach, and the platform matches against its + user base. + + steps: + - id: sync_audiences + title: "Push audience segments" + narrative: | + The buyer syncs audience segment definitions to the platform. Each segment includes + targeting criteria that the platform evaluates against its user graph. The platform + returns match rates and segment status. + task: sync_audiences + schema_ref: "media-buy/sync-audiences-request.json" + response_schema_ref: "media-buy/sync-audiences-response.json" + doc_ref: "/media-buy/task-reference/sync_audiences" + comply_scenario: sync_audiences + stateful: true + expected: | + Accept and process audience segments: + - Per-segment status: active, processing, or rejected + - Match rate estimates where available + - Platform-assigned segment IDs + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + audiences: + - audience_id: "outdoor_enthusiasts_25_54" + name: "Outdoor enthusiasts 25-54" + description: "Adults 25-54 interested in hiking, camping, and outdoor gear" + + idempotency_key: "$generate:uuid_v4#sales_social_audience_sync_sync_audiences" + context: + correlation_id: "sales_social--sync_audiences" + validations: + - check: response_schema + description: "Response matches sync-audiences-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--sync_audiences" + description: "Context correlation_id returned unchanged" + - id: creative_push + title: "Native creative sync" + narrative: | + The buyer pushes native creative assets to the platform. Social platforms render ads + in their native format — the buyer provides assets (images, headlines, descriptions) + and the platform assembles them into the native ad unit. + + steps: + - id: sync_creatives + title: "Push native creative assets" + narrative: | + The buyer syncs creative assets for native ad formats. The platform validates + each creative against its format requirements and returns per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept and validate native creatives: + - Per-creative action: created or updated + - Per-creative status: accepted, pending_review, or rejected + - Validation errors for rejected creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "native_trail_pro" + name: "Trail Pro 3000 - Native" + format_id: + agent_url: "https://social-platform.example.com" + id: "native_feed" + assets: + image: + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-native.png" + width: 1200 + height: 628 + mime_type: "image/png" + headline: + asset_type: "text" + content: "Trail Pro 3000 — Built for the Summit" + + idempotency_key: "$generate:uuid_v4#sales_social_creative_push_sync_creatives" + context: + correlation_id: "sales_social--sync_creatives" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--sync_creatives" + description: "Context correlation_id returned unchanged" + - id: catalog_driven_dynamic_ads + title: "Catalog-driven dynamic product ads" + narrative: | + Social platforms routinely ship dynamic product ads (Snap Dynamic Ads, Meta DPA, + TikTok Dynamic Showcase): the buyer pushes a product catalog and the platform + renders per-impression creative pulling product images, titles, and tracker URLs + from catalog items. The creative template references catalog-item macros + (`{SKU}`, `{GTIN}`) whose values resolve to the specific item shown at + impression time. + + This phase exercises the catalog-acceptance leg of that flow: push a small inline + product catalog (with `content_id_type: "sku"` declared so macro resolution binds + to the right field), then push a DPA creative template using the AdCP-native + `product_carousel_3_to_10` format on `creative.adcontextprotocol.org`. Real + social-platform formats (Meta `native_carousel`, Snap dynamic ad set, TikTok + dynamic showcase) are platform-specific refinements tracked as follow-ups on + #2640; the AdCP-native format is the interop baseline. + + Runtime substitution-safety checks (that the emitted tracker URLs percent-encode + the macro values per `docs/creative/universal-macros#substitution-safety-catalog-item-macros`) + require the substitution-observer contract tracked in #2638; phases gated on + that contract activate when runners advertise it. + + steps: + - id: sync_product_catalog + title: "Push a product catalog" + narrative: | + The buyer pushes a small inline product catalog — the shape social platforms + accept for dynamic product ads. Your platform validates each item and + returns per-item approval status. + task: sync_catalogs + schema_ref: "media-buy/sync-catalogs-request.json" + response_schema_ref: "media-buy/sync-catalogs-response.json" + doc_ref: "/media-buy/task-reference/sync_catalogs" + stateful: true + expected: | + Return per-catalog results with catalog_id, action, item_count, and + items_approved counts. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + catalogs: + - catalog_id: "acme_gear_spring_2026" + type: "product" + content_id_type: "sku" + name: "Acme Outdoor — Spring 2026 Gear" + items: + - item_id: "trail_pro_3000" + title: "Trail Pro 3000 Backpack" + description: "65L expedition pack with torso-length adjustment." + url: "https://acmeoutdoor.example/gear/trail-pro-3000" + image_url: "https://cdn.acmeoutdoor.example/gear/trail-pro-3000.jpg" + price: + amount: 289.00 + currency: "USD" + - item_id: "summit_tent_2p" + title: "Summit 2P Ultralight Tent" + description: "2-person four-season tent, 1.8kg packed." + url: "https://acmeoutdoor.example/gear/summit-tent-2p" + image_url: "https://cdn.acmeoutdoor.example/gear/summit-tent-2p.jpg" + price: + amount: 549.00 + currency: "USD" + + idempotency_key: "$generate:uuid_v4#sales_social_catalog_driven_dynamic_ads_sync_product_catalog" + context: + correlation_id: "sales_social--sync_product_catalog" + validations: + - check: response_schema + description: "Response matches sync-catalogs-response.json schema" + - check: field_present + path: "catalogs[0].catalog_id" + description: "Catalog has an ID" + - check: field_present + path: "catalogs[0].item_count" + description: "Catalog reports item count" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--sync_product_catalog" + description: "Context correlation_id returned unchanged" + + - id: sync_dpa_creative + title: "Push a dynamic product ad creative with catalog-item macros" + narrative: | + The buyer pushes a creative template whose tracker URLs reference + catalog-item macros (`{SKU}`, `{GTIN}`). At impression time, your platform + substitutes each catalog item's values into the template to render the + per-impression ad. + + The template uses the AdCP-native `product_carousel_3_to_10` format + (see `docs/creative/channels/carousels.mdx`). Per #2620, values substituted + into URL contexts MUST be percent-encoded such that only RFC 3986 unreserved + characters remain unescaped; nested macro expansion is prohibited. This step + validates that the template is accepted into the library; runtime + substitution validation is tracked under #2638. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept the DPA creative template. Per-creative action: created or updated. + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + creatives: + - creative_id: "acme_dpa_spring_2026" + name: "Acme Outdoor — Spring DPA template" + format_id: + agent_url: "https://creative.adcontextprotocol.org" + id: "product_carousel_3_to_10" + assets: + impression_pixel: + asset_type: "url" + url: "https://track.acmeoutdoor.example/imp?sku={SKU}>in={GTIN}&mb={MEDIA_BUY_ID}" + click_url: + asset_type: "url" + url: "https://track.acmeoutdoor.example/click?sku={SKU}" + + idempotency_key: "$generate:uuid_v4#sales_social_catalog_driven_dynamic_ads_sync_dpa_creative" + context: + correlation_id: "sales_social--sync_dpa_creative" + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--sync_dpa_creative" + description: "Context correlation_id returned unchanged" + - id: event_setup + title: "Event source setup" + narrative: | + Before sending conversion events, the buyer registers the event sources the platform + should expect events from — a website pixel, a mobile SDK, or a server-to-server feed. + The platform returns setup instructions and binds the event_source_id that later + log_event calls will reference. + + steps: + - id: sync_event_sources + title: "Register conversion event sources" + narrative: | + The buyer tells the platform where conversion events will come from. The + platform records each event_source_id, returns integration code, and will + accept log_event calls that reference it. + task: sync_event_sources + schema_ref: "media-buy/sync-event-sources-request.json" + response_schema_ref: "media-buy/sync-event-sources-response.json" + doc_ref: "/media-buy/task-reference/sync_event_sources" + stateful: true + expected: | + Return event sources with: + - event_source_id and seller_id + - setup.snippet: integration code (JavaScript pixel, HTML tag, or pixel URL) + - setup.instructions: human-readable integration guide + - action: created or updated + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + event_sources: + - event_source_id: "acmeoutdoor_website" + name: "Acme Outdoor Website" + event_types: ["purchase", "add_to_cart", "page_view", "lead"] + allowed_domains: ["acmeoutdoor.example"] + + idempotency_key: "$generate:uuid_v4#sales_social_event_setup_sync_event_sources" + context: + correlation_id: "sales_social--sync_event_sources" + validations: + - check: response_schema + description: "Response matches sync-event-sources-response.json schema" + - check: field_present + path: "event_sources[0].setup.snippet" + description: "Event source includes setup snippet" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--sync_event_sources" + description: "Context correlation_id returned unchanged" + - id: event_logging + title: "Conversion event tracking" + narrative: | + The buyer sends conversion events back to the platform for measurement and optimization. + Events include purchases, signups, and other post-click actions that the platform uses + to optimize delivery and report on campaign performance. + + steps: + - id: log_event + title: "Send conversion events" + narrative: | + The buyer logs conversion events that occurred after ad exposure. The platform + records these events for attribution, reporting, and delivery optimization. + task: log_event + schema_ref: "media-buy/log-event-request.json" + response_schema_ref: "media-buy/log-event-response.json" + doc_ref: "/media-buy/task-reference/log_event" + stateful: true + expected: | + Acknowledge the events: + - Per-event status: accepted or rejected + - Event IDs for deduplication + - Attribution window validation + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + event_source_id: "acmeoutdoor_website" + events: + - event_type: "purchase" + event_id: "evt_trail_pro_001" + event_time: "2026-04-05T14:30:00Z" + value: 149.99 + currency: "USD" + + idempotency_key: "$generate:uuid_v4#sales_social_event_logging_log_event" + context: + correlation_id: "sales_social--log_event" + validations: + - check: response_schema + description: "Response matches log-event-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--log_event" + description: "Context correlation_id returned unchanged" + - id: financials + title: "Account financials" + narrative: | + The buyer checks account financials — spending, balance, and payment status. This is + essential for budget monitoring across multiple social platforms. + + steps: + - id: get_account_financials + title: "Check account spending and balance" + narrative: | + The buyer retrieves financial information for the advertiser account. The platform + returns current spend, remaining balance, and payment status. + task: get_account_financials + schema_ref: "account/get-account-financials-request.json" + response_schema_ref: "account/get-account-financials-response.json" + doc_ref: "/accounts/tasks/get_account_financials" + stateful: true + expected: | + Return account financial data: + - Current spend to date + - Remaining balance or credit + - Payment status and terms + - Budget utilization metrics + + sample_request: + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + + context: + correlation_id: "sales_social--get_account_financials" + validations: + - check: response_schema + description: "Response matches get-account-financials-response.json schema" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "sales_social--get_account_financials" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/signal-marketplace/index.yaml b/dist/compliance/3.0.1/specialisms/signal-marketplace/index.yaml new file mode 100644 index 0000000000..a733f17abf --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/signal-marketplace/index.yaml @@ -0,0 +1,415 @@ +id: signal_marketplace +version: "1.0.0" +title: "Marketplace signal agent" +protocol: signals +category: signal_marketplace +summary: "Signal agent that resells third-party data provider signals with verifiable catalog provenance." +track: signals +required_tools: + - get_signals +requires_scenarios: + - signal_marketplace/governance_denied + +narrative: | + You operate a signal marketplace — an intermediary that aggregates audience data from + multiple third-party providers and makes it available to buyers through a single interface. + Think LiveRamp Data Marketplace, Oracle Data Cloud, or Lotame. + + Your agent searches across catalogs published by data providers in their adagents.json + files. Buyers discover signals through natural language queries, verify provenance by + checking the data provider's catalog directly, and activate signals on DSPs or sales + agents for campaign targeting. + + The key property of marketplace signals: provenance is independently verifiable. Each + signal traces back to a data_provider_domain whose adagents.json lists your agent as + authorized. Buyers can (and should) verify this before spending. + + This storyboard walks through discovery, verification, and both activation patterns — + activating directly on a DSP (buyer manages targeting) and activating on a sales agent + (SA handles downstream coordination). + + Pricing is hard-required for signal marketplaces. Signals are rate-carded goods by + definition — the value exchange is paying for access to audience data. Unlike creative + ad servers (where billing can be handled via out-of-band enterprise contracts), a signal + marketplace without pricing_options is either non-commercial or misconfigured; buyers + cannot activate without a pricing_option_id to anchor billing. + +agent: + interaction_model: marketplace_catalog + capabilities: + - catalog_signals + examples: + - "LiveRamp Data Marketplace" + - "Oracle Data Cloud" + - "Lotame" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The buyer has a campaign brief with targeting objectives. The test kit provides + sample signal definitions, pricing options, and destination configurations that + match the training agent's signal providers. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports signals before discovering or activating audience data. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring signals in supported_protocols, confirming the agent serves audience signals. + sample_request: + context: + correlation_id: "signal_marketplace--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: "signal_marketplace--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: discovery + title: "Signal discovery" + narrative: | + The buyer describes what they need in natural language. Your agent searches + across all authorized data provider catalogs and returns matching signals with + pricing, coverage estimates, and value types. + + This is where marketplace agents earn their keep — the buyer doesn't need to + know which providers exist or what taxonomies they use. One query, many sources. + + steps: + - id: search_by_spec + title: "Discover signals from a campaign brief" + narrative: | + The buyer's platform translates a campaign brief into a get_signals call. + Your agent searches catalogs from every authorized data provider and returns + what matches — automotive intent from one provider, geo data from another, + retail purchase history from a third. + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow + stateful: false + expected: | + Return matching signals from multiple data providers. Each signal must include: + - signal_agent_segment_id for activation + - signal_id with source, data_provider_domain, and id + - name, description, and value_type (binary, categorical, or numeric) + - coverage_percentage (audience reach estimate) + - pricing_options with at least one pricing model + - signal_type: "marketplace" + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_spec: "In-market EV buyers with high purchase propensity, near auto dealerships" + + context: + correlation_id: "signal_marketplace--search_by_spec" + + context_outputs: + - name: first_signal_id + path: "signals[0].signal_id" + - name: first_signal_agent_segment_id + path: "signals[0].signal_agent_segment_id" + - name: first_signal_pricing_option_id + path: "signals[0].pricing_options[0].pricing_option_id" + - name: second_signal_id + path: "signals[1].signal_id" + validations: + - check: response_schema + description: "Response matches get-signals-response.json schema" + - check: field_present + path: "signals[0].signal_agent_segment_id" + description: "Each signal has a signal_agent_segment_id" + - check: field_present + path: "signals[0].signal_id.data_provider_domain" + description: "Each signal traces to a data provider domain" + - check: field_present + path: "signals[0].pricing_options" + description: "Each signal has pricing options" + - check: field_present + path: "signals[0].coverage_percentage" + description: "Each signal has a coverage estimate" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_marketplace--search_by_spec" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "signals[0].signal_id.source" + description: "Signal ID includes source discriminator" + - check: field_present + path: "signals[0].signal_id.data_provider_domain" + description: "Signal ID includes data_provider_domain for provenance" + - id: search_by_ids + title: "Look up specific signals by ID" + narrative: | + The buyer already knows which signals they want — discovered in the prior + step. They pass signal_ids directly instead of a natural language query. + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow + stateful: true + expected: | + Return the exact signals requested, with full metadata and pricing. + If a signal_id doesn't exist, omit it from results — don't error. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_ids: + - "$context.first_signal_id" + + context: + correlation_id: "signal_marketplace--search_by_ids" + validations: + - check: response_schema + description: "Response matches schema" + - check: field_present + path: "signals" + description: "Response contains a signals array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_marketplace--search_by_ids" + description: "Context correlation_id returned unchanged" + - id: verification + title: "Catalog verification" + narrative: | + Before activating third-party data, the buyer verifies provenance. They fetch + the data provider's adagents.json directly and confirm the signal exists and + your agent is authorized. This independent check is outside the AdCP protocol — + but your agent must return the metadata that makes it possible. + + This phase tests that your get_signals responses include verifiable provenance + data: signal_id.source is "catalog" and signal_id.data_provider_domain points + to a real domain whose adagents.json the buyer can fetch independently. + + steps: + - id: verify_provenance_metadata + title: "Confirm signals carry verifiable provenance" + narrative: | + The buyer looks up a specific signal by ID (discovered earlier) and checks + that the response includes the metadata needed for independent verification — + source is "catalog" and data_provider_domain points to a fetchable adagents.json. + The actual HTTP fetch of adagents.json is the buyer's responsibility, but + your agent must provide the domain to fetch from. + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/data-providers" + comply_scenario: signals_flow + stateful: true + expected: | + Return the requested signal with verifiable provenance metadata: + - signal_id.source is "catalog" + - signal_id.data_provider_domain matches a real domain + The buyer will independently fetch that domain's adagents.json to confirm + your agent is listed in authorized_agents. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_ids: + - "$context.first_signal_id" + + context: + correlation_id: "signal_marketplace--verify_provenance_metadata" + validations: + - check: field_value + path: "signals[0].signal_id.source" + value: "catalog" + description: "Signal source is 'catalog' (verifiable via adagents.json)" + - check: field_present + path: "signals[0].signal_id.data_provider_domain" + description: "Data provider domain is present for independent verification" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_marketplace--verify_provenance_metadata" + description: "Context correlation_id returned unchanged" + - id: platform_activation + title: "Activate on a DSP" + narrative: | + The buyer activates a signal directly on a DSP platform. The signal agent pushes + segment data to the platform, and the buyer gets back a segment_id they can + reference when configuring campaign targeting. + + Use platform destinations when the buyer is managing DSP campaigns directly — + not through a sales agent. + + steps: + - id: activate_on_platform + title: "Activate signal on a DSP" + narrative: | + The buyer selects a signal and a DSP destination. The signal agent pushes + the segment to the platform. This is typically asynchronous — the initial + response shows is_live: false with an estimated duration, and the buyer + polls for completion. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployment with: + - type: "platform" matching the requested destination + - is_live: false initially (async activation) + - estimated_activation_duration_minutes + After polling, the deployment should show: + - is_live: true + - activation_key with type: "segment_id" and a platform-native segment ID + - deployed_at timestamp + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "$context.first_signal_agent_segment_id" + pricing_option_id: "$context.first_signal_pricing_option_id" + destinations: + - type: "platform" + platform: "the-trade-desk" + account: "agency-123-ttd" + idempotency_key: "$generate:uuid_v4#signal_marketplace_activate_platform" + context: + correlation_id: "signal_marketplace--activate_on_platform" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].type" + description: "Deployment includes type" + - check: field_value + path: "deployments[0].type" + value: "platform" + description: "Deployment type is 'platform'" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_marketplace--activate_on_platform" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "deployments[0].activation_key" + description: "Deployment includes activation_key for targeting" + - id: agent_activation + title: "Activate on a sales agent" + narrative: | + The buyer activates a signal on a sales agent instead of a DSP. This is the + right pattern when the buyer is purchasing media through the SA — the SA handles + its own DSP coordination. + + The buyer doesn't need to know which DSP the SA uses. The activation key confirms + the signal is live on the SA, and the SA applies targeting when fulfilling media + buys through create_media_buy. + + steps: + - id: activate_on_agent + title: "Activate signal on a sales agent" + narrative: | + The buyer activates a signal with the sales agent's URL as the destination. + Agent activations are typically synchronous — the SA records the activation + immediately and returns a key_value activation key. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployment with: + - type: "agent" matching the requested destination + - agent_url matching the SA's URL + - is_live: true (sync activation) + - activation_key with type: "key_value" + - deployed_at timestamp + + The SA records the activation internally. When the buyer later calls + create_media_buy through this SA, signal-based targeting is already + in place. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "$context.first_signal_agent_segment_id" + pricing_option_id: "$context.first_signal_pricing_option_id" + destinations: + - type: "agent" + agent_url: "https://wonderstruck.salesagents.example" + idempotency_key: "$generate:uuid_v4#signal_marketplace_activate_agent" + context: + correlation_id: "signal_marketplace--activate_on_agent" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].activation_key" + description: "Deployment includes activation key" + - check: field_value + path: "deployments[0].type" + value: "agent" + description: "Deployment type is 'agent'" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_marketplace--activate_on_agent" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/specialisms/signal-marketplace/scenarios/governance_denied.yaml b/dist/compliance/3.0.1/specialisms/signal-marketplace/scenarios/governance_denied.yaml new file mode 100644 index 0000000000..5a6e38e782 --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/signal-marketplace/scenarios/governance_denied.yaml @@ -0,0 +1,207 @@ +id: signal_marketplace/governance_denied +version: "1.0.0" +title: "Signal agent rejects activation when governance denies" +category: signal_marketplace +summary: "Verifies that a signal agent propagates GOVERNANCE_DENIED when the buyer's governance plan denies activation." +track: signals +required_tools: + - sync_governance + - get_signals + - activate_signal + +narrative: | + Signal activation is a spending event: activating a third-party segment costs per-user + or per-activation fees that must be authorized against the buyer's governance plan. The + signal agent must consult the registered governance agent before activation and deny + the request when governance returns denied. + + This scenario sets up a strict $100 plan, registers that governance with the signal + agent via sync_governance, then attempts to activate a signal whose pricing exceeds + the plan. The signal agent must return GOVERNANCE_DENIED with findings propagated + from the governance agent. + + By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. + Override with --governance-agent-url to use a custom governance agent. + +agent: + interaction_model: marketplace_catalog + capabilities: + - catalog_signals + - governance_aware + examples: + - "Any signal agent that honors governance before activation" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + A governance agent that supports sync_plans and check_governance, and a signal + agent that supports sync_governance + activate_signal. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: governance_plan_setup + title: "Set up strict governance plan" + narrative: | + Create a governance plan with a $100 budget — enough to block even a single + activation at typical CPM-for-segment pricing. + + steps: + - id: sync_plans + title: "Create strict governance plan" + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + stateful: true + expected: | + The governance agent acknowledges the plan. + sample_request: + plans: + - plan_id: "comply-signal-gov-denied" + brand: + domain: "acmeoutdoor.example" + objectives: "Restricted plan — signals activation test" + budget: + total: 100 + currency: "USD" + reallocation_threshold: 50 + flight: + start: "2026-04-01T00:00:00Z" + end: "2026-06-30T23:59:59Z" + countries: ["US"] + idempotency_key: "$generate:uuid_v4#signal_marketplace_governance_denied_governance_plan_setup_sync_plans" + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: signal_agent_setup + title: "Register account and governance with the signal agent" + steps: + - id: sync_accounts + title: "Establish account with signal agent" + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Signal agent returns the account with account_id active. + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + billing: "operator" + payment_terms: "net_30" + idempotency_key: "$generate:uuid_v4#signal_marketplace_governance_denied_signal_agent_setup_sync_accounts" + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: sync_governance + title: "Register governance agent with signal agent" + task: sync_governance + schema_ref: "account/sync-governance-request.json" + response_schema_ref: "account/sync-governance-response.json" + doc_ref: "/accounts/tasks/sync_governance" + stateful: true + expected: | + Signal agent acknowledges governance registration. + sample_request: + accounts: + - account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + governance_agents: + - url: "$context.governance_agent_url" + authentication: + schemes: ["Bearer"] + credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + categories: ["budget_authority"] + idempotency_key: "$generate:uuid_v4#signal_marketplace_governance_denied_signal_agent_setup_sync_governance" + validations: + - check: response_schema + description: "Response matches sync-governance-response.json schema" + + - id: activation_denied + title: "Attempt activation — governance denies" + steps: + - id: get_signals_list + title: "Discover a signal to activate" + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/task-reference/get_signals" + comply_scenario: signal_discovery + stateful: false + expected: | + Return at least one signal with a signal_agent_segment_id and pricing. + sample_request: + signal_spec: "outdoor enthusiasts, age 25-54, US" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + context_outputs: + - path: "signals[0].signal_agent_segment_id" + key: "signal_agent_segment_id" + validations: + - check: response_schema + description: "Response matches get-signals-response.json schema" + - check: field_present + path: "signals[0].signal_agent_segment_id" + description: "Signal has an activation identifier" + + - id: activate_signal_denied + title: "activate_signal — governance denies" + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/task-reference/activate_signal" + comply_scenario: signal_activation + expect_error: true + negative_path: payload_well_formed + stateful: true + expected: | + Signal agent rejects with: + - code: GOVERNANCE_DENIED + - findings propagated from the governance agent + + sample_request: + signal_agent_segment_id: "$context.signal_agent_segment_id" + destinations: + - type: "platform" + platform: "the-trade-desk" + account: "acmeoutdoor-ttd-seat" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + idempotency_key: "signal-gov-denied-v1" + + context: + correlation_id: "signal_marketplace--governance_denied--activate" + sample_response: + errors: + - code: "GOVERNANCE_DENIED" + message: "Signal activation requires governance approval. Call check_governance first — a governance plan is registered for this account." + details: + findings: + - category_id: "budget_authority" + severity: "critical" + explanation: "Signal activation requires governance approval. Call check_governance first — a governance plan is registered for this account." + plan_id: "comply-signal-gov-denied" + validations: + - check: error_code + value: "GOVERNANCE_DENIED" + description: "Error code is GOVERNANCE_DENIED" + - check: field_present + path: "context" + description: "Response echoes back the context object even on errors" diff --git a/dist/compliance/3.0.1/specialisms/signal-owned/index.yaml b/dist/compliance/3.0.1/specialisms/signal-owned/index.yaml new file mode 100644 index 0000000000..6687ae2923 --- /dev/null +++ b/dist/compliance/3.0.1/specialisms/signal-owned/index.yaml @@ -0,0 +1,316 @@ +id: signal_owned +version: "1.0.0" +title: "Owned signal agent" +protocol: signals +category: signal_owned +summary: "Signal agent serving first-party or proprietary audience data without external catalog verification." +track: signals +required_tools: + - get_signals + +narrative: | + You operate a first-party data platform — a retailer CDP, publisher contextual data + provider, or proprietary audience platform. Your signals are agent-native: they come + from your own data, not from third-party catalogs. + + Buyers trust your agent directly. There's no external adagents.json to verify against — + provenance is the agent itself. This simplifies the flow: discovery and activation, + without the verification step that marketplace agents require. + + Owned signal agents often have richer signal types. A retailer might expose purchase + frequency as a numeric signal (0-30 visits/month) or loyalty tier as categorical + (platinum/gold/silver/bronze). A publisher might expose content category, subscriber + tenure, or engagement scores. + + This storyboard walks through discovery and both activation patterns. + + Pricing is hard-required here for the same reason as signal marketplaces: signals + are rate-carded goods. Buyers cannot activate without a pricing_option_id to anchor + billing. Agents distributing first-party signals without commercial terms belong on + list_authorized_properties or a different specialism, not this one. + +agent: + interaction_model: owned_signals + capabilities: [] + examples: + - "Retailer CDPs (e.g., loyalty and purchase data)" + - "Publisher contextual platforms (e.g., content category, subscriber data)" + - "First-party audience platforms (e.g., CRM-derived segments)" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The buyer has a campaign brief with targeting objectives. The test kit provides + sample owned signal definitions with various value types (binary, categorical, + numeric) and pricing models. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: capability_discovery + title: "Capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to confirm the agent supports signals before discovering or activating audience data. + + steps: + - id: get_capabilities + title: "Check agent capabilities" + narrative: | + Verify that the agent declares the expected protocol support before + proceeding with domain-specific operations. + 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: capability_discovery + stateful: false + expected: | + Return capabilities declaring signals in supported_protocols, confirming the agent serves audience signals. + sample_request: + context: + correlation_id: "signal_owned--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: "signal_owned--get_capabilities" + description: "Context correlation_id returned unchanged" + - id: discovery + title: "Signal discovery" + narrative: | + The buyer describes targeting objectives and your agent returns matching signals + from your proprietary data. Unlike marketplace agents, these signals have + signal_type "owned" — provenance is the agent itself. + + Owned signals often include richer value types. A retailer can expose loyalty + tiers as categorical signals and purchase frequency as numeric signals, giving + buyers more precise targeting than binary include/exclude. + + steps: + - id: search_owned_signals + title: "Discover owned signals" + narrative: | + The buyer searches for audience segments using a natural language description. + Your agent returns signals from your proprietary data — loyalty tiers, purchase + history, engagement scores, contextual categories. + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow + stateful: false + expected: | + Return matching signals from your proprietary data. Each signal must include: + - signal_agent_segment_id for activation + - signal_id with source: "agent_native" + - name, description, and value_type + - coverage_percentage + - pricing_options + - signal_type: "owned" + + For categorical signals, include the allowed_values. + For numeric signals, include the range (min/max). + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_spec: "High-value customers likely to purchase electronics, with loyalty program data" + + context: + correlation_id: "signal_owned--search_owned_signals" + validations: + - check: response_schema + description: "Response matches get-signals-response.json schema" + - check: field_present + path: "signals[0].signal_agent_segment_id" + description: "Each signal has a signal_agent_segment_id" + - check: field_present + path: "signals[0].pricing_options" + description: "Each signal has pricing options" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_owned--search_owned_signals" + description: "Context correlation_id returned unchanged" + - check: field_present + path: "signals[0].signal_id.source" + description: "Signal ID includes source discriminator" + - id: filter_by_criteria + title: "Filter signals by criteria" + narrative: | + The buyer narrows the search using filters — maximum CPM, specific signal + types, or coverage thresholds. This tests that your agent handles filter + parameters correctly and returns only matching signals. + task: get_signals + schema_ref: "signals/get-signals-request.json" + response_schema_ref: "signals/get-signals-response.json" + doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow + stateful: false + expected: | + Return only signals matching the filter criteria. If no signals match, + return an empty signals array — not an error. + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_spec: "Purchase behavior signals" + filters: + max_cpm: 5.00 + + context: + correlation_id: "signal_owned--filter_by_criteria" + validations: + - check: response_schema + description: "Response matches schema" + - check: field_present + path: "signals" + description: "Response contains a signals array" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_owned--filter_by_criteria" + description: "Context correlation_id returned unchanged" + - id: platform_activation + title: "Activate on a DSP" + narrative: | + The buyer activates a signal directly on a DSP. Your agent pushes the segment + to the platform and returns a segment_id the buyer uses for campaign targeting. + + Use platform destinations when the buyer manages DSP campaigns directly. + + steps: + - id: activate_on_platform + title: "Activate owned signal on a DSP" + narrative: | + The buyer selects a signal and activates it on a DSP. For owned signals, + this means your platform pushes first-party segment data to the DSP. The + buyer gets back a segment_id for targeting. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployment with: + - type: "platform" + - is_live status (may be async) + - activation_key with type: "segment_id" when live + - estimated_activation_duration_minutes if not immediately live + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "prism_high_ltv" + pricing_option_id: "po_prism_flat_monthly" + destinations: + - type: "platform" + platform: "the-trade-desk" + account: "agency-123-ttd" + idempotency_key: "$generate:uuid_v4#signal_owned_activate_platform" + context: + correlation_id: "signal_owned--activate_on_platform" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].type" + description: "Deployment includes type" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_owned--activate_on_platform" + description: "Context correlation_id returned unchanged" + - id: agent_activation + title: "Activate on a sales agent" + narrative: | + The buyer activates a signal on a sales agent. The SA records the activation + and handles downstream DSP coordination — the buyer doesn't need to know which + platform the SA uses. + + This is the right pattern when buying media through a sales agent. + + steps: + - id: activate_on_agent + title: "Activate owned signal on a sales agent" + narrative: | + The buyer activates a signal with the SA's URL as the destination. The + activation is typically synchronous. The SA records it internally and + applies the targeting when the buyer later calls create_media_buy. + task: activate_signal + schema_ref: "signals/activate-signal-request.json" + response_schema_ref: "signals/activate-signal-response.json" + doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow + stateful: true + expected: | + Return a deployment with: + - type: "agent" + - agent_url matching the SA + - is_live: true (sync activation) + - activation_key with type: "key_value" + - deployed_at timestamp + + sample_request: + account: + brand: + domain: "novamotors.example" + operator: "pinnacle-agency.example" + signal_agent_segment_id: "prism_cart_abandoner" + pricing_option_id: "po_prism_abandoner_cpm" + destinations: + - type: "agent" + agent_url: "https://wonderstruck.salesagents.example" + idempotency_key: "$generate:uuid_v4#signal_owned_activate_agent" + context: + correlation_id: "signal_owned--activate_on_agent" + ext: + test_platform: + test_run: true + validations: + - check: response_schema + description: "Response matches activate-signal-response.json schema" + - check: field_present + path: "deployments[0].activation_key" + description: "Deployment includes activation key" + - check: field_value + path: "deployments[0].type" + value: "agent" + description: "Deployment type is 'agent'" + + - check: field_present + path: "context" + description: "Response echoes back the context object" + - check: field_value + path: "context.correlation_id" + value: "signal_owned--activate_on_agent" + description: "Context correlation_id returned unchanged" diff --git a/dist/compliance/3.0.1/test-kits/acme-outdoor.yaml b/dist/compliance/3.0.1/test-kits/acme-outdoor.yaml new file mode 100644 index 0000000000..131a4d64ff --- /dev/null +++ b/dist/compliance/3.0.1/test-kits/acme-outdoor.yaml @@ -0,0 +1,210 @@ +# Acme Outdoor — Sample Advertiser Test Kit +# +# Entity definition: fictional-entities.yaml → advertisers[acme_outdoor] +# +# Provides the "ingredients" needed to test creative agent storyboards: +# a brand identity, sample assets at common dimensions, and sample text. +# +# This brand is registered as a sandbox brand in AgenticAdvertising.org (AAO). +# Agents resolve acmeoutdoor.example via the standard AAO brand resolution path. +# AAO returns the brand.json below but excludes sandbox brands from production +# brand discovery results. + +id: acme_outdoor +name: "Acme Outdoor" +description: "Fictional outdoor gear brand for storyboard testing" +sandbox: true + +# Authentication fixture for the universal security_baseline storyboard +# (universal/security.yaml). Declares: +# - api_key: the demo Bearer the runner sends on the positive api-key +# probe. The conformance handle is the `demo--` prefix — agents +# SHOULD accept any Bearer matching that prefix (not this literal +# value) so the suffix can rotate across spec versions without +# breaking previously-conformant agents. +# - probe_task: the protected, auth-required read the runner calls under +# `auth: none` and with a random-invalid key. Default is list_creatives; +# declare explicitly so the api_key phase runs instead of being skipped +# via `skip_if: "!test_kit.auth.api_key"`. +auth: + api_key: "demo-acme-outdoor-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 + - url: "https://test-assets.adcontextprotocol.org/acme-outdoor/logo-icon.png" + orientation: "square" + background: "transparent-bg" + variant: "icon" + width: 200 + height: 200 + 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" + +# Sample assets at common ad dimensions. +# These are placeholder URLs — in production, these would point to actual hosted test images. +assets: + images: + - id: "hero_300x250" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-300x250.jpg" + width: 300 + height: 250 + mime_type: "image/jpeg" + description: "Medium rectangle hero — hiker on mountain trail" + + - id: "hero_728x90" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-728x90.jpg" + width: 728 + height: 90 + mime_type: "image/jpeg" + description: "Leaderboard hero — panoramic mountain vista" + + - id: "hero_320x50" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-320x50.jpg" + width: 320 + height: 50 + mime_type: "image/jpeg" + description: "Mobile banner hero — trail gear close-up" + + - id: "hero_160x600" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-160x600.jpg" + width: 160 + height: 600 + mime_type: "image/jpeg" + description: "Wide skyscraper hero — vertical trail scene" + + - id: "hero_master" + url: "https://test-assets.adcontextprotocol.org/acme-outdoor/hero-master.jpg" + width: 1200 + height: 628 + mime_type: "image/jpeg" + description: "Master hero image — high-res source for any size" + + text: + headlines: + - "Summer Sale — 40% Off All Gear" + - "Built for the Trail" + - "Adventure Starts Here" + descriptions: + - "Premium outdoor gear tested on the world's toughest trails. Shop the summer collection." + - "From daypacks to expedition tents, gear that performs when it matters." + cta: + - "Shop Now" + - "Explore the Collection" + - "Find Your Gear" + + click_url: "https://acmeoutdoor.example/summer-sale" + +# Inventory list fixtures for property_list / collection_list targeting tests. +# +# These reference lists a fictional governance/inventory agent would publish. +# They let storyboards exercise the full PropertyListReference / CollectionListReference +# contract without needing a live list server: scenarios point at list_id values +# and the seller's test engine returns canned contents. +# +# Both matching and non-matching sets are provided so a single test fixture +# covers targeting (buy should include listed inventory) and no-match behaviour +# (buy references a list that resolves to zero matches in this seller's catalog). +inventory_targets: + # --- Matching sets (Acme Outdoor's intended audience) --- + matching_properties: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_allowlist_v1" + description: "Outdoor lifestyle properties relevant to Acme Outdoor campaigns." + # Expected contents when the list is fetched via get_property_list. + expected_identifiers: + - type: domain + value: "outdoormagazine.example" + - type: domain + value: "hikingtrails.example" + - type: domain + value: "campinggear.example" + - type: domain + value: "mountaineering.example" + + matching_collections: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_collections_v1" + description: "Outdoor programming the buyer wants to run alongside." + # Expected collection entries when fetched via get_collection_list. + expected_collections: + - collection_rid: "rid:outdoor:trail_life" + name: "Trail Life" + kind: "series" + genre: "outdoor" + distribution_ids: + - type: imdb_id + value: "tt9100001" + - collection_rid: "rid:outdoor:summit_stories" + name: "Summit Stories" + kind: "series" + genre: "outdoor" + distribution_ids: + - type: imdb_id + value: "tt9100002" + + # --- No-match sets (nothing in this seller's catalog matches) --- + no_match_properties: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_v1" + description: "Properties outside this seller's inventory — should yield zero matches." + expected_identifiers: + - type: domain + value: "never-sold-here.example" + - type: domain + value: "also-not-indexed.example" + + no_match_collections: + agent_url: "https://governance.pinnacle-agency.example" + list_id: "acme_outdoor_no_match_collections_v1" + description: "Collections not carried by this seller — should yield zero matches." + expected_collections: + - collection_rid: "rid:food:bistro_kitchen" + name: "Bistro Kitchen" + kind: "series" + genre: "food" + distribution_ids: + - type: imdb_id + value: "tt9200001" diff --git a/dist/compliance/3.0.1/test-kits/bistro-oranje.yaml b/dist/compliance/3.0.1/test-kits/bistro-oranje.yaml new file mode 100644 index 0000000000..93f308d09f --- /dev/null +++ b/dist/compliance/3.0.1/test-kits/bistro-oranje.yaml @@ -0,0 +1,126 @@ +# Bistro Oranje — Hospitality Advertiser Test Kit +# +# Entity definition: fictional-entities.yaml → advertisers[bistro_oranje] +# +# Dutch restaurant chain used in rights licensing scenarios. +# +# This brand is registered as a sandbox brand in AgenticAdvertising.org (AAO). +# Agents resolve bistro-oranje.example via the standard AAO brand resolution path. +# AAO returns the brand.json below but excludes sandbox brands from production +# brand discovery results. + +id: bistro_oranje +name: "Bistro Oranje" +description: "Dutch restaurant chain for rights licensing storyboard testing" +sandbox: true + +# security_baseline auth fixture — see acme-outdoor.yaml for semantics. +auth: + api_key: "demo-bistro-oranje-v1" + probe_task: list_creatives + +brand: + house: + domain: "bistro-oranje.example" + name: "Bistro Oranje" + brand_id: "bistro_oranje" + names: + - en: "Bistro Oranje" + - nl: "Bistro Oranje" + description: "Modern Dutch dining — seasonal menus inspired by the Netherlands, served with warmth." + industry: "hospitality" + keller_type: "master" + logos: + - url: "https://test-assets.adcontextprotocol.org/bistro-oranje/logo-primary.png" + orientation: "horizontal" + background: "light-bg" + variant: "primary" + width: 400 + height: 100 + - url: "https://test-assets.adcontextprotocol.org/bistro-oranje/logo-icon.png" + orientation: "square" + background: "transparent-bg" + variant: "icon" + width: 200 + height: 200 + colors: + primary: "#E65100" + secondary: "#1B5E20" + accent: "#FFC107" + background: "#FFF8E1" + text: "#212121" + fonts: + heading: + family: "Playfair Display" + weight: 700 + url: "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700" + body: + family: "Source Sans Pro" + weight: 400 + url: "https://fonts.googleapis.com/css2?family=Source+Sans+Pro" + tone: + voice: "Warm and inviting, with understated confidence. We let the food speak — no excess, no pretension." + attributes: + - "welcoming" + - "seasonal" + - "unpretentious" + dos: + - "Mention seasonal ingredients by name" + - "Reference Dutch culinary traditions" + - "Keep descriptions sensory and specific" + donts: + - "Use fine-dining jargon" + - "Overuse superlatives" + - "Promise exclusivity — Bistro Oranje is for everyone" + +assets: + images: + - id: "hero_300x250" + url: "https://test-assets.adcontextprotocol.org/bistro-oranje/hero-300x250.jpg" + width: 300 + height: 250 + mime_type: "image/jpeg" + description: "Medium rectangle hero — seasonal dish on rustic table" + + - id: "hero_728x90" + url: "https://test-assets.adcontextprotocol.org/bistro-oranje/hero-728x90.jpg" + width: 728 + height: 90 + mime_type: "image/jpeg" + description: "Leaderboard hero — bistro exterior with canal backdrop" + + - id: "hero_320x50" + url: "https://test-assets.adcontextprotocol.org/bistro-oranje/hero-320x50.jpg" + width: 320 + height: 50 + mime_type: "image/jpeg" + description: "Mobile banner hero — close-up of signature stamppot" + + - id: "hero_160x600" + url: "https://test-assets.adcontextprotocol.org/bistro-oranje/hero-160x600.jpg" + width: 160 + height: 600 + mime_type: "image/jpeg" + description: "Wide skyscraper hero — vertical dining room scene" + + - id: "hero_master" + url: "https://test-assets.adcontextprotocol.org/bistro-oranje/hero-master.jpg" + width: 1200 + height: 628 + mime_type: "image/jpeg" + description: "Master hero image — high-res chef plating seasonal dish" + + text: + headlines: + - "Taste the Season at Bistro Oranje" + - "Dutch Comfort, Modern Kitchen" + - "Reserve Your Table Tonight" + descriptions: + - "Seasonal menus inspired by Dutch tradition, made with ingredients sourced within 50km. Bistro Oranje — Amsterdam, Rotterdam, Utrecht." + - "From stamppot to stroopwafel, every dish tells a story. Book your table at Bistro Oranje." + cta: + - "Reserve a Table" + - "View the Menu" + - "Find a Location" + + click_url: "https://bistro-oranje.example/reservations" diff --git a/dist/compliance/3.0.1/test-kits/nova-motors.yaml b/dist/compliance/3.0.1/test-kits/nova-motors.yaml new file mode 100644 index 0000000000..d89c863a90 --- /dev/null +++ b/dist/compliance/3.0.1/test-kits/nova-motors.yaml @@ -0,0 +1,262 @@ +# Nova Motors — Signal-Focused Advertiser Test Kit +# +# Entity definition: fictional-entities.yaml → advertisers[nova_motors] +# Data providers defined in this kit: see fictional-entities.yaml → data_providers +# +# This brand is registered as a sandbox brand in AgenticAdvertising.org (AAO). +# Agents resolve novamotors.example via the standard AAO brand resolution path. +# AAO returns the brand.json below but excludes sandbox brands from production +# brand discovery results. + +id: nova_motors +name: "Nova Motors" +description: | + Signal-focused test kit for the Nova Motors Volta EV launch campaign. + Provides sample signal definitions, pricing options, and destination + configurations for testing signal agent storyboards. +sandbox: true + +# security_baseline auth fixture — see acme-outdoor.yaml for semantics. +# +# probe_task is `get_signals` because this kit's primary consumers are +# signal agents (specialisms/signal-owned, specialisms/signal-marketplace) +# where list_creatives is not implemented. Non-signal storyboards that +# reuse this kit (collection-lists, sales-broadcast-tv, sponsored- +# intelligence) should select acme-outdoor for security_baseline, or +# override probe_task at the storyboard layer. +auth: + api_key: "demo-nova-motors-v1" + probe_task: get_signals + +brand: + house: + domain: "novamotors.example" + name: "Nova Motors" + brand_id: "nova_motors" + names: + - en: "Nova Motors" + description: "Electric vehicles for the next generation. The Volta EV — performance meets sustainability." + industry: "automotive" + keller_type: "master" + logos: + - url: "https://test-assets.adcontextprotocol.org/nova-motors/logo-primary.png" + orientation: "horizontal" + background: "light-bg" + variant: "primary" + width: 400 + height: 100 + - url: "https://test-assets.adcontextprotocol.org/nova-motors/logo-icon.png" + orientation: "square" + background: "transparent-bg" + variant: "icon" + width: 200 + height: 200 + colors: + primary: "#0D47A1" + secondary: "#00BFA5" + accent: "#FF6D00" + background: "#F5F5F5" + text: "#1A1A1A" + fonts: + heading: + family: "Inter" + weight: 700 + url: "https://fonts.googleapis.com/css2?family=Inter:wght@700" + body: + family: "Inter" + weight: 400 + url: "https://fonts.googleapis.com/css2?family=Inter" + tone: + voice: "Forward-thinking and precise. We speak to drivers who care about engineering and the planet — no greenwashing, no hype." + attributes: + - "innovative" + - "precise" + - "optimistic" + dos: + - "Lead with performance specs" + - "Reference sustainability with evidence" + - "Use clean, modern language" + donts: + - "Greenwash or overstate environmental claims" + - "Use legacy automotive clichés" + - "Condescend about range anxiety" + +campaign: + brand: "Nova Motors" + product: "Volta EV" + brief: "Reach in-market EV buyers with high purchase propensity, near dealerships, who haven't bought a Nova vehicle before." + budget: 250000 + currency: "USD" + markets: ["US"] + +signals: + marketplace: + - signal_agent_segment_id: "trident_likely_ev_buyers" + name: "Likely EV Buyers" + data_provider_domain: "tridentauto.example" + signal_id: "likely_ev_buyers" + value_type: "binary" + signal_type: "marketplace" + coverage_percentage: 8 + pricing: + - pricing_option_id: "po_trident_ev_cpm" + model: "cpm" + cpm: 3.50 + currency: "USD" + + - signal_agent_segment_id: "trident_purchase_propensity" + name: "Purchase Propensity" + data_provider_domain: "tridentauto.example" + signal_id: "purchase_propensity" + value_type: "numeric" + signal_type: "marketplace" + range: + min: 0 + max: 1 + coverage_percentage: 55 + pricing: + - pricing_option_id: "po_trident_propensity_cpm" + model: "cpm" + cpm: 4.00 + currency: "USD" + + - signal_agent_segment_id: "meridian_competitor_visitors" + name: "Competitor Visitors" + data_provider_domain: "meridiangeo.example" + signal_id: "competitor_visitors" + value_type: "binary" + signal_type: "marketplace" + coverage_percentage: 6 + pricing: + - pricing_option_id: "po_meridian_visitors_cpm" + model: "cpm" + cpm: 5.00 + currency: "USD" + + - signal_agent_segment_id: "shopgrid_new_to_brand" + name: "New to Brand" + data_provider_domain: "shopgrid.example" + signal_id: "new_to_brand" + value_type: "binary" + signal_type: "marketplace" + coverage_percentage: 25 + pricing: + - pricing_option_id: "po_shopgrid_retail_cpm" + model: "cpm" + cpm: 3.50 + currency: "USD" + + owned: + - signal_agent_segment_id: "prism_high_ltv" + name: "High LTV Customers" + value_type: "binary" + signal_type: "owned" + coverage_percentage: 12 + pricing: + - pricing_option_id: "po_prism_flat_monthly" + model: "flat_fee" + amount: 5000 + period: "monthly" + currency: "USD" + + - signal_agent_segment_id: "prism_cart_abandoner" + name: "Cart Abandoners" + value_type: "binary" + signal_type: "owned" + coverage_percentage: 18 + pricing: + - pricing_option_id: "po_prism_abandoner_cpm" + model: "cpm" + cpm: 8.00 + currency: "USD" + + - signal_agent_segment_id: "prism_engagement_score" + name: "Engagement Score" + value_type: "numeric" + signal_type: "owned" + range: + min: 0 + max: 100 + coverage_percentage: 65 + pricing: + - pricing_option_id: "po_prism_engagement_cpm" + model: "cpm" + cpm: 2.50 + currency: "USD" + + - signal_agent_segment_id: "prism_churn_risk" + name: "Churn Risk" + value_type: "categorical" + signal_type: "owned" + categories: ["low_risk", "medium_risk", "high_risk", "churned"] + coverage_percentage: 70 + pricing: + - pricing_option_id: "po_prism_churn_pom" + model: "percent_of_media" + percent: 12 + max_cpm: 3.00 + currency: "USD" + +assets: + images: + - id: "hero_300x250" + url: "https://test-assets.adcontextprotocol.org/nova-motors/hero-300x250.jpg" + width: 300 + height: 250 + mime_type: "image/jpeg" + description: "Medium rectangle hero — Volta EV on coastal highway" + + - id: "hero_728x90" + url: "https://test-assets.adcontextprotocol.org/nova-motors/hero-728x90.jpg" + width: 728 + height: 90 + mime_type: "image/jpeg" + description: "Leaderboard hero — Volta EV front profile with charging station" + + - id: "hero_320x50" + url: "https://test-assets.adcontextprotocol.org/nova-motors/hero-320x50.jpg" + width: 320 + height: 50 + mime_type: "image/jpeg" + description: "Mobile banner hero — Volta EV dashboard detail" + + - id: "hero_160x600" + url: "https://test-assets.adcontextprotocol.org/nova-motors/hero-160x600.jpg" + width: 160 + height: 600 + mime_type: "image/jpeg" + description: "Wide skyscraper hero — vertical Volta EV silhouette" + + - id: "hero_master" + url: "https://test-assets.adcontextprotocol.org/nova-motors/hero-master.jpg" + width: 1200 + height: 628 + mime_type: "image/jpeg" + description: "Master hero image — high-res Volta EV in motion" + + text: + headlines: + - "The Volta EV — 0 to 60 in 3.2s" + - "Performance Meets Sustainability" + - "Drive Electric. Drive Nova." + descriptions: + - "340 miles of range. 15-minute fast charge. The Volta EV is engineered for drivers who refuse to compromise." + - "Zero emissions, zero compromises. Test drive the Volta EV at your nearest Nova Motors dealer." + cta: + - "Reserve Yours" + - "Schedule a Test Drive" + - "Explore the Volta" + + click_url: "https://novamotors.example/volta-ev" + +destinations: + platforms: + - type: "platform" + platform: "the-trade-desk" + account: "agency-123-ttd" + - type: "platform" + platform: "streamhaus" + account: "agency-ctv-seat-456" + agents: + - type: "agent" + agent_url: "https://wonderstruck.salesagents.example" diff --git a/dist/compliance/3.0.1/test-kits/osei-natural.yaml b/dist/compliance/3.0.1/test-kits/osei-natural.yaml new file mode 100644 index 0000000000..840c428004 --- /dev/null +++ b/dist/compliance/3.0.1/test-kits/osei-natural.yaml @@ -0,0 +1,126 @@ +# Osei Natural — Beauty Advertiser Test Kit +# +# Entity definition: fictional-entities.yaml → advertisers[osei_natural] +# +# 8-person natural skincare company based in Nairobi. Represents the small +# business that advertising should serve but currently doesn't. +# +# This brand is registered as a sandbox brand in AgenticAdvertising.org (AAO). +# Agents resolve oseinatural.example via the standard AAO brand resolution path. +# AAO returns the brand.json below but excludes sandbox brands from production +# brand discovery results. + +id: osei_natural +name: "Osei Natural" +description: "Natural skincare brand for small-business advertiser storyboard testing" +sandbox: true + +# security_baseline auth fixture — see acme-outdoor.yaml for semantics. +auth: + api_key: "demo-osei-natural-v1" + probe_task: list_creatives + +brand: + house: + domain: "oseinatural.example" + name: "Osei Natural" + brand_id: "osei_natural" + names: + - en: "Osei Natural" + description: "Natural skincare rooted in East African botanicals. Small-batch, founder-led, and unapologetically simple." + industry: "beauty" + keller_type: "master" + logos: + - url: "https://test-assets.adcontextprotocol.org/osei-natural/logo-primary.png" + orientation: "horizontal" + background: "light-bg" + variant: "primary" + width: 400 + height: 100 + - url: "https://test-assets.adcontextprotocol.org/osei-natural/logo-icon.png" + orientation: "square" + background: "transparent-bg" + variant: "icon" + width: 200 + height: 200 + colors: + primary: "#5D4037" + secondary: "#C8E6C9" + accent: "#FF8F00" + background: "#FBF7F4" + text: "#2E2E2E" + fonts: + heading: + family: "Cormorant Garamond" + weight: 600 + url: "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600" + body: + family: "Lato" + weight: 400 + url: "https://fonts.googleapis.com/css2?family=Lato" + tone: + voice: "Calm, grounded, and personal. Amara speaks directly to customers like a trusted friend sharing what works." + attributes: + - "authentic" + - "gentle" + - "personal" + dos: + - "Name specific botanicals and their origins" + - "Use first-person when appropriate — the founder is the brand" + - "Keep it warm and conversational" + donts: + - "Use clinical or pharmaceutical language" + - "Make anti-aging claims — Osei Natural is about care, not correction" + - "Erase the brand's Kenyan identity — it's a feature, not a detail" + +assets: + images: + - id: "hero_300x250" + url: "https://test-assets.adcontextprotocol.org/osei-natural/hero-300x250.jpg" + width: 300 + height: 250 + mime_type: "image/jpeg" + description: "Medium rectangle hero — product jars with botanical ingredients" + + - id: "hero_728x90" + url: "https://test-assets.adcontextprotocol.org/osei-natural/hero-728x90.jpg" + width: 728 + height: 90 + mime_type: "image/jpeg" + description: "Leaderboard hero — Amara in workshop with shea butter" + + - id: "hero_320x50" + url: "https://test-assets.adcontextprotocol.org/osei-natural/hero-320x50.jpg" + width: 320 + height: 50 + mime_type: "image/jpeg" + description: "Mobile banner hero — close-up of baobab oil serum" + + - id: "hero_160x600" + url: "https://test-assets.adcontextprotocol.org/osei-natural/hero-160x600.jpg" + width: 160 + height: 600 + mime_type: "image/jpeg" + description: "Wide skyscraper hero — vertical product lineup on linen" + + - id: "hero_master" + url: "https://test-assets.adcontextprotocol.org/osei-natural/hero-master.jpg" + width: 1200 + height: 628 + mime_type: "image/jpeg" + description: "Master hero image — high-res flat lay of full product range" + + text: + headlines: + - "Your Skin Deserves Better Ingredients" + - "Small-Batch Skincare from Nairobi" + - "Rooted in East African Botanicals" + descriptions: + - "Shea butter, baobab oil, and aloe — sourced from East African growers, blended by hand in Nairobi. Osei Natural." + - "Eight people, twelve products, zero compromises. Natural skincare that actually works." + cta: + - "Shop the Collection" + - "Meet the Founder" + - "Try a Sample Kit" + + click_url: "https://oseinatural.example/shop" diff --git a/dist/compliance/3.0.1/test-kits/signed-requests-runner.yaml b/dist/compliance/3.0.1/test-kits/signed-requests-runner.yaml new file mode 100644 index 0000000000..e139113e1b --- /dev/null +++ b/dist/compliance/3.0.1/test-kits/signed-requests-runner.yaml @@ -0,0 +1,155 @@ +# Signed-Requests Runner — Harness Contract Test Kit +# +# Applies to: universal/signed-requests.yaml (gated on +# `request_signing.supported: true` in get_adcp_capabilities). +# +# The signed-requests storyboard grades an agent's RFC 9421 verifier against 28 +# conformance vectors at /compliance/{version}/test-vectors/request-signing/. +# Most vectors are pure black-box: the runner constructs a signed HTTP request, +# sends it, and checks the response. Three negative vectors assert behavior that +# depends on verifier state the runner cannot set from the outside: +# +# 016-replayed-nonce — replay cache must already contain (keyid, nonce) +# 017-key-revoked — keyid must already be in the revocation list +# 020-rate-abuse — per-keyid replay cache must be at its configured cap +# +# This test-kit defines the coordination contract between a black-box runner and +# a request-signing agent under test. Agents advertising +# `request_signing.supported: true` MUST pre-configure their verifier per this +# contract before the negative phase runs. The runner reads this file to know +# (a) the keyids it will sign with, (b) how to provoke each stateful negative +# vector, and (c) the grading-time cap values it will target (which are NOT +# production recommendations — see each field). See `scope` below for what the +# contract does NOT specify. + +id: signed_requests_runner +applies_to: + universal_storyboard: signed-requests + +description: | + Coordination contract between a black-box runner and a request-signing agent + under test. The runner signs vectors dynamically using the keypairs published + in keys.json. The agent pre-configures its verifier — accepted JWKS, pre-revoked + keyid, replay TTL, per-keyid cap — so that the three stateful negative vectors + produce their expected error codes without the runner injecting verifier state. + + Vectors requiring coordination declare `requires_contract` in their fixture; + the runner gates those vectors on this contract being in scope. + +endpoint_scope: sandbox +# The replay-window contract sends a live, validly-signed mutating request as +# its first step (the second copy is what the grader expects to be rejected). +# Running this against a production endpoint would create a real media buy. +# Graders MUST target a sandbox/staging endpoint or an idempotency-keyed +# sacrificial path the agent discards post-grading. Agents advertising +# `request_signing.supported: true` SHOULD expose a dedicated grading endpoint +# rather than grading in prod. + +harness_mode: black_box +# Agents participating in a white-box test harness (e.g., SDK internal tests +# against the reference verifier) MAY satisfy the stateful vectors via direct +# state injection using the vector's `test_harness_state` block, and declare +# `harness_mode: white_box` in their own runner configuration. AdCP Verified +# grading runs in black_box mode only. + +runner_signing_keys: + # Runner signs every non-negative-key vector with one of these keypairs. The + # agent's verifier MUST treat these keyids as a registered test counterparty + # whose JWKS contains the corresponding public keys with + # adcp_use: "request-signing". Private keys for signing live in keys.json + # under `_private_d_for_test_only`. + - keyid: test-ed25519-2026 + alg: ed25519 + jwks_source: /compliance/{version}/test-vectors/request-signing/keys.json + - keyid: test-es256-2026 + alg: ecdsa-p256-sha256 + jwks_source: /compliance/{version}/test-vectors/request-signing/keys.json + +stateful_vector_contract: + replay_window: + # Vector 016-replayed-nonce. + # + # The runner provokes the replay rejection by sending the same signed + # request twice in sequence. The agent MUST accept the first submission + # (standard positive path) and reject the second with + # request_signature_replayed at checklist step 12. + # + # The agent's replay-cache TTL for this test counterparty MUST be at least + # `min_replay_ttl_seconds`, which is strictly greater than + # `max_interval_seconds` to absorb clock skew between runner and agent and + # scheduler jitter on either side. Otherwise the cache entry for the first + # request may evict before the second arrives and the vector will pass + # spuriously (i.e., both requests accepted = no replay rejection). The + # runner's own interval between the two submissions MUST NOT exceed + # `max_interval_seconds`. + # + # This supersedes vector 016's `test_harness_state.replay_cache_entries` + # for black-box mode. In white-box mode, the harness MAY inject the cache + # entry directly and skip the first request. + vector_id: 016-replayed-nonce + black_box_behavior: repeat_request + max_interval_seconds: 5 + min_replay_ttl_seconds: 10 + + revocation: + # Vector 017-key-revoked. + # + # The runner cannot revoke a key on the agent's side. The agent MUST + # pre-configure its revocation list with `test-revoked-2026` before the + # negative phase runs. The runner signs vector 017 with this keyid and + # expects request_signature_key_revoked at checklist step 9. + # + # A dedicated revoked keypair (rather than reusing a runner signing key) + # keeps positive vectors and vector 017 independent: the positive phase + # remains runnable after vector 017 is graded. + vector_id: 017-key-revoked + pre_revoked_keyid: test-revoked-2026 + + rate_abuse: + # Vector 020-rate-abuse (checklist step 9a). + # + # The runner sends N+1 distinct-nonce requests signed by the same keyid + # within the window. The agent MUST reject the (N+1)th with + # request_signature_rate_abuse once the per-keyid cap is hit. + # + # `grading_target_per_keyid_cap_requests` is the cap the runner will + # target during grading — NOT a production recommendation. Agents MAY + # configure a lower cap for the test-kit counterparty only so grading + # finishes in a reasonable time. Production caps MUST follow the spec + # recommendation at docs/building/implementation/security.mdx + # §per-keyid cap (at least 1,000,000 entries per keyid). Implementers + # copying a value from this file into production code SHOULD use + # `production_min_per_keyid_cap_requests` below as the floor, not + # `grading_target_per_keyid_cap_requests`. + vector_id: 020-rate-abuse + grading_target_per_keyid_cap_requests: 100 + production_min_per_keyid_cap_requests: 1000000 + window_seconds: 60 + +scope: + in_scope: | + - Keyids the runner will sign with and their JWKS source. + - Agent-side preconditions for each stateful negative vector. + - Grading-time cap the runner will target for rate-abuse grading (NOT a + production recommendation; see rate_abuse block). + - Minimum replay-cache TTL so the replay-window contract is reliable. + - Black-box vs. white-box harness mode selection. + - Endpoint scope (sandbox only — see endpoint_scope). + out_of_scope: | + - Specific error-code strings — those live in each vector's + expected_outcome.error_code and are graded byte-for-byte. + - Checklist step numbers — informational only; grading is on the error + code, not the step. + - Pre-signed Signature bytes in the vectors — unchanged by this contract. + Black-box runners re-sign dynamically; pre-signed bytes remain valid + for white-box cross-SDK byte-equivalence checks. + - The agent's internal replay TTL or cap storage mechanism — the contract + specifies observable behavior, not implementation. + - Production verifier configuration — this contract configures a test + counterparty only. + +references: + universal_storyboard: static/compliance/source/universal/signed-requests.yaml + test_vectors: /compliance/{version}/test-vectors/request-signing/ + verifier_checklist: /docs/building/implementation/security.mdx#verifier-checklist-requests + runner_implementation: https://github.com/adcontextprotocol/adcp-client/issues/585 diff --git a/dist/compliance/3.0.1/test-kits/substitution-observer-runner.yaml b/dist/compliance/3.0.1/test-kits/substitution-observer-runner.yaml new file mode 100644 index 0000000000..2db5da6c49 --- /dev/null +++ b/dist/compliance/3.0.1/test-kits/substitution-observer-runner.yaml @@ -0,0 +1,690 @@ +# Substitution Observer Runner — Harness Contract Test Kit +# +# Applies to: +# - Any storyboard that exercises catalog-item macro substitution and needs +# to assert the RFC 3986 percent-encoding rule from +# docs/creative/universal-macros.mdx#substitution-safety-catalog-item-macros. +# - Current consumers (when phases are gated on this contract): +# - specialisms/sales-catalog-driven/index.yaml (substitution_safety phase) +# - specialisms/creative-generative/index.yaml (catalog_substitution_safety phase) +# - Deferred pending observation-hook design (#2651): +# - specialisms/sales-social/index.yaml — social platforms substitute at +# serve time in proprietary renderers; no AdCP-level preview hook. +# - Future consumers: sales-retail-media (post-retail-media-epic), +# sales-broadcast-tv dynamic-creative phases, and anywhere else catalog +# values expand into URL contexts through a previewable surface. +# +# The #2620 rule: sales agents MUST percent-encode catalog-item macro values +# such that only RFC 3986 `unreserved` characters remain unescaped before +# substitution into URL contexts. Nested macro expansion is prohibited. +# +# The ATTACK SURFACE is impression-time URL emission. AdCP's API surface does +# not normally expose substituted output — it happens at serve time outside +# the protocol. Two observable AdCP-layer hooks exist: +# +# preview_creative responses (preview_html / preview_url) +# build_creative responses with include_preview: true +# +# This contract defines how a runner consumes those preview artifacts, +# extracts tracker URLs with substituted catalog-item values, and asserts +# the values are encoded per the #2620 rule. +# +# Clean seam: the runner does NOT reimplement URL parsing, HTML extraction, +# or the encoding check. It delegates to @adcp/client primitives (proposed: +# `SubstitutionObserver` with `extract_tracker_urls` and `assert_rfc3986_safe` +# helpers) so the same library production receivers would use is what the +# conformance runner exercises. Library fixes cover both. + +id: substitution_observer_runner +# Shape extension vs webhook-receiver-runner: this contract applies to +# specialisms rather than universals, because catalog-macro substitution is a +# specialism-scoped behavior (only catalog-accepting sellers emit it). The +# `applies_to.specialisms` key is a deliberate structural addition for this +# class of contract. +applies_to: + specialisms: + - sales_catalog_driven + - creative_generative + # sales_social deferred — no AdCP-level preview hook (see #2651). + universals: [] + +description: | + Coordination contract between a runner that observes substituted tracker + URLs in preview artifacts and an agent under test that emits catalog-driven + creatives. The runner ingests preview_html or follows preview_url, extracts + tracker URLs bound to catalog-item macros, and asserts RFC 3986 percent- + encoding of catalog-item values per docs/creative/universal-macros#substitution-safety-catalog-item-macros. + +endpoint_scope: sandbox +# Storyboards consuming this contract synthesize attacker-shaped catalog +# values (e.g., title containing `abc&cmd=drop` or `\r\nHost: evil`) and +# push them via sync_catalogs. Running those against production would +# pollute live catalogs. Graders MUST target a sandbox/staging endpoint. + +harness_mode: black_box + +# --- Observation mechanism --- +# +# The runner captures preview output from two response shapes (either is +# sufficient — the storyboard author names which to observe): +# +# preview_html (inline HTML string in the response) +# The runner parses the HTML and extracts tracker URLs from the +# attribute set enumerated below. Zero network dependency. +# +# preview_url (HTTPS URL the runner fetches subject to the SSRF policy +# enumerated below) +# The runner performs a single GET against the URL and extracts +# identically. +# +# Both paths land at the same @adcp/client.SubstitutionObserver.extract_tracker_urls +# primitive, which returns a deterministic list of `{ url, source_attr, line_hint }` +# records that storyboards match against. + +observation_modes: + - mode: html_inline + default_for: [lint, fast, full_conformance] + description: | + Runner reads preview_html from the response and parses it. No network + fetch. Appropriate for CI lint gates and SDK self-tests. + runner_config: + source_path: preview_html + html_parser: "@adcp/client.SubstitutionObserver.parse_html" + + - mode: url_fetch + default_for: [adcp_verified] + description: | + Runner fetches preview_url over HTTPS, expects 200 + text/html, and + parses the body. Required for AdCP Verified grading where the preview + is a live asset. + runner_config: + source_path: preview_url + fetch: + method: GET + follow_redirects: false + max_body_bytes: 262144 # 256 KiB — matches typical creative preview sizes + max_connect_seconds: 3 + timeout_seconds: 10 + required_content_types: ["text/html", "application/xhtml+xml"] + # SSRF policy is NORMATIVE IN THIS CONTRACT (not deferred to a library). + # Verified graders MUST enforce every rule below. The runner MAY delegate + # the implementation to @adcp/client.SubstitutionObserver.enforce_ssrf_policy + # (or equivalent) provided the delegate implements this exact deny list. + ssrf_policy: + schemes_allowed: ["https"] + schemes_denied: ["http", "file", "gopher", "ftp", "ftps", "data", "javascript", "about", "ws", "wss"] + hosts_denied_ipv4_cidrs: + - "0.0.0.0/8" # "this network" + - "10.0.0.0/8" # RFC 1918 private + - "100.64.0.0/10" # CGNAT + - "127.0.0.0/8" # loopback + - "169.254.0.0/16" # link-local (incl. 169.254.169.254 IMDS v1/v2) + - "172.16.0.0/12" # RFC 1918 private + - "192.0.0.0/24" # IETF protocol assignments + - "192.168.0.0/16" # RFC 1918 private + - "224.0.0.0/4" # multicast + - "240.0.0.0/4" # reserved + hosts_denied_ipv6_cidrs: + - "::1/128" # loopback + - "::/128" # unspecified + - "::ffff:0:0/96" # IPv4-mapped (re-check as IPv4) + - "64:ff9b::/96" # IPv4/IPv6 translation + - "fc00::/7" # unique local + - "fe80::/10" # link-local + - "ff00::/8" # multicast + hosts_denied_metadata: + # Cloud metadata hosts by name — the IP CIDRs above catch the standard + # 169.254.169.254 cases, but some providers expose hostname aliases. + - "metadata.google.internal" + - "metadata" + - "metadata.packet.net" + - "fd00:ec2::254" # IMDS IPv6 + host_literal_policy_verified: reject + # In AdCP Verified grading, reject ANY bare IP literal in preview_url + # (both IPv4 and IPv6) regardless of range — forces resolution through + # a public DNS name the grader can audit. Local-dev flags MAY relax this. + dns_revalidation: required + # After DNS resolution, EVERY resolved address MUST be re-checked against + # hosts_denied_*. Resolve once, bind to the resolved address for the + # request — do NOT pass the hostname to the HTTP client (closes DNS + # rebinding between resolve and connect). + redirects: follow_false_strict + # follow_redirects: false is already set above; this field documents + # the companion: if the origin returns 3xx, treat it as a failure + # (`preview_url_unusable` + sub-reason `redirect_returned`), do NOT + # chase — redirect chasing would require re-running the full SSRF + # policy at each hop and is out of scope for v1. + +# --- HTML attribute extraction set (normative) --- +# +# The runner extracts tracker URLs from the following attributes only. This +# set is normative — runner MUST NOT under-extract (missing one of these +# attributes lets a seller hide an unencoded value); runner MUST NOT over- +# extract (e.g., arbitrary `data-*` attributes whose values happen to parse +# as URLs but are not trackers). Extension to additional attributes MAY +# happen in a future contract revision; today's set is closed. + +html_attribute_extraction_set: + tag_attribute_pairs: + - { tag: "a", attr: "href" } + - { tag: "img", attr: "src" } + - { tag: "img", attr: "srcset" } # may contain multiple URLs + - { tag: "iframe", attr: "src" } + - { tag: "source", attr: "src" } + - { tag: "source", attr: "srcset" } + - { tag: "link", attr: "href" } + - { tag: "meta", attr: "content" } # refresh redirects, og:image, etc. + - { tag: "*", attr: "data-impression-url" } + - { tag: "*", attr: "data-click-url" } + - { tag: "*", attr: "data-tracker-url" } + - { tag: "*", attr: "data-vast-url" } + srcset_handling: parse_per_descriptor + # srcset values are space-separated URL+descriptor pairs (e.g., + # "a.jpg 1x, b.jpg 2x"). The runner MUST extract every URL component. + comment_nodes: ignored + script_text_content: ignored + # Tracker URLs in `