diff --git a/.changeset/governance-multi-agent-envelope.md b/.changeset/governance-multi-agent-envelope.md new file mode 100644 index 0000000000..2468a9bf40 --- /dev/null +++ b/.changeset/governance-multi-agent-envelope.md @@ -0,0 +1,22 @@ +--- +"adcontextprotocol": minor +--- + +Single governance agent per account — reconcile 3.x governance schemas with a coherent semantic model (closes #3010). + +**The inconsistency.** 3.x registration (`sync_governance`) allowed up to 10 governance agents per account with per-agent `categories`, and the campaign-governance spec documented fan-out-and-unanimous-approval. But the protocol envelope and `check_governance` carried a single `governance_context` string, and the four-value `scope` enum on brand.json (`spend_authority | delivery_monitor | brand_safety | regulatory_compliance`) didn't carve the governance responsibility at its joints — those aren't independent specialisms held by different authorities, they're phases and facets of one evaluation over one plan. + +**Decision.** Commit to single-agent: an account binds to one governance agent that owns the full lifecycle. Multi-agent registration was aspirational and produced schema inconsistencies without a coherent semantic story. A plan is unitary (budget, policies, restricted attributes all live on the plan); `check_governance` already separates authorization / fidelity / drift on the `phase` axis (`purchase` / `modification` / `delivery`); internal specialist review (legal, brand safety, category) belongs inside the configured agent, not at the registration layer. + +**Changes.** + +- `account/sync-governance-request`: `governance_agents` constrained to `maxItems: 1`. `categories` field removed. Description makes the one-agent-per-account invariant explicit and explains why (phases, not specialisms; plan is unitary; specialist review composes inside the agent). +- `core/protocol-envelope`: `governance_context` stays a singular string. Description updated to state the single-agent invariant and why phased lifecycle (not split authority) means one token covers the full governed action. +- `brand.json`: remove the governance-agent `scope` enum (`spend_authority | delivery_monitor | brand_safety | regulatory_compliance`) — no longer meaningful under single-agent registration. P&G example updated to drop the stray `scope` array. +- `docs/governance/campaign/specification.mdx`: replace "Multi-agent composition" with "One governance agent per account" explaining the rationale (authorization/fidelity/drift are phases, regulatory rules are encoded in the plan, specialist review composes inside the agent, one lifecycle/one token/one audit trail). Fix the remaining `governance_agent(s)` plural residue. +- `governance/check-governance-request` / `response` / `report-plan-outcome-request`: revert any language implying per-agent fan-out; all three are single-agent calls as originally designed. +- `docs/governance/campaign/tasks/check_governance.mdx`, `report_plan_outcome.mdx`: revert to the single-agent prose. + +**Backwards compatibility.** Buyers with one agent registered (practically every 3.0 deployment per maintainer's reading of the ecosystem) are unaffected. Buyers that registered more than one agent per account against the previous `maxItems: 10` — if any exist — MUST collapse to a single agent; the protocol does not support routing or aggregating across multiple. Sellers that validated the `categories` field MUST treat registrations without it as valid (the field is removed, not deprecated). + +**What this is not.** This PR does not address specialist governance surfaces adjacent to campaign governance — brand-safety pre-screen of creatives, property-list policy, content-standards evaluation — those are separate governance domains with their own agents and their own lifecycle. Campaign governance speaks only for the plan. diff --git a/dist/docs/3.0.1/contributing/storyboard-authoring.md b/dist/docs/3.0.1/contributing/storyboard-authoring.md index 8822db8a68..91e710153a 100644 --- a/dist/docs/3.0.1/contributing/storyboard-authoring.md +++ b/dist/docs/3.0.1/contributing/storyboard-authoring.md @@ -287,7 +287,7 @@ SHOULD include a substitution-safety phase covering the rule set at **Start from the template, don't copy-paste from a sibling specialism.** The canonical three-step phase (`sync_*_probe_catalog` → `build_*_probe_creative` → `expect_substitution_safe`) lives as a `phase_template:` comment block in -[`static/compliance/source/test-kits/substitution-observer-runner.yaml`](../../static/compliance/source/test-kits/substitution-observer-runner.yaml). +[`static/compliance/source/test-kits/substitution-observer-runner.yaml`](../../../../static/compliance/source/test-kits/substitution-observer-runner.yaml). The block uses `<>` tokens for the specialism-specific bits (brand domain, catalog_id prefix, idempotency prefix) so you can materialize a new phase by doing a simple text substitution against those tokens. diff --git a/docs/accounts/tasks/sync_governance.mdx b/docs/accounts/tasks/sync_governance.mdx index ad1521b4ed..1a2772e2b9 100644 --- a/docs/accounts/tasks/sync_governance.mdx +++ b/docs/accounts/tasks/sync_governance.mdx @@ -5,9 +5,11 @@ description: "sync_governance syncs governance agent endpoints to specific accou testable: false --- -Sync governance agent endpoints to specific accounts. The seller persists these agents and calls them via `check_governance` during media buy lifecycle events. Each account entry pairs an [account reference](/docs/building/integration/accounts-and-agents#account-references) with the governance agents for that account, supporting both explicit accounts (`account_id`) and implicit accounts (`brand` + `operator`). +Sync the governance agent endpoint for specific accounts. The seller persists the agent and calls it via `check_governance` during media buy lifecycle events. Each account entry pairs an [account reference](/docs/building/integration/accounts-and-agents#account-references) with exactly one governance agent, supporting both explicit accounts (`account_id`) and implicit accounts (`brand` + `operator`). -This uses **replace semantics** — each call replaces any previously registered agents on the specified accounts. Accounts not included in the request keep their existing configuration. +An account binds to one governance agent that owns the full lifecycle. Authorization, delivery monitoring, and compliance are phases of the same evaluation against one plan, not specialisms held by separate authorities; specialist review (legal, brand safety, category) composes inside the governance agent rather than across multiple registrations. `governance_agents` is an array with `maxItems: 1` because the array shape is the shape 3.0 shipped with — the constraint is load-bearing and not a staging post toward loosening. The envelope's `governance_context` is singular below this layer; relaxing the cap would require a coordinated wire-shape change that is not planned. See [One governance agent per account](/docs/governance/campaign/specification#one-governance-agent-per-account). + +This uses **replace semantics** — each call replaces any previously registered agent on the specified accounts. Accounts not included in the request keep their existing configuration. **Response Time**: ~1s. @@ -16,7 +18,7 @@ This uses **replace semantics** — each call replaces any previously registered ## Quick Start -Sync a budget governance agent to an explicit account: +Sync the governance agent for an explicit account: @@ -30,12 +32,11 @@ const result = await testAgent.syncGovernance({ account: { account_id: "acct-social-001" }, governance_agents: [ { - url: "https://governance.pinnacle-media.com/budget", + url: "https://governance.pinnacle-media.com", authentication: { schemes: ["Bearer"], credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - categories: ["budget_authority"] + } } ] } @@ -54,7 +55,7 @@ if ("errors" in validated && validated.errors) { for (const entry of validated.accounts) { if (entry.status === "synced") { - console.log(`${JSON.stringify(entry.account)}: ${entry.governance_agents.length} agents registered`); + console.log(`${JSON.stringify(entry.account)}: ${entry.governance_agents.length} agent registered`); } else { console.log(`${JSON.stringify(entry.account)}: failed — ${JSON.stringify(entry.errors)}`); } @@ -72,12 +73,11 @@ async def main(): "account": {"account_id": "acct-social-001"}, "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/budget", + "url": "https://governance.pinnacle-media.com", "authentication": { "schemes": ["Bearer"], "credentials": "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - "categories": ["budget_authority"] + } } ] } @@ -89,7 +89,7 @@ async def main(): for entry in result.accounts: if entry.status == "synced": - print(f"{entry.account}: {len(entry.governance_agents)} agents registered") + print(f"{entry.account}: {len(entry.governance_agents)} agent registered") else: print(f"{entry.account}: failed — {entry.errors}") @@ -109,15 +109,14 @@ asyncio.run(main()) | Field | Type | Required | Description | |-------|------|----------|-------------| | `account` | object | Yes | [Account reference](/docs/building/integration/accounts-and-agents#account-references): `{account_id}` for explicit accounts or `{brand, operator}` for implicit accounts. | -| `governance_agents` | array | Yes | Governance agent endpoints for this account (1–10 per account). | +| `governance_agents` | array | Yes | Governance agent endpoint for this account. Array with exactly one entry (`minItems: 1`, `maxItems: 1`). | -**Each governance agent:** +**The governance agent:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `url` | string | Yes | HTTPS endpoint URL for the governance agent. | | `authentication` | object | Yes | Credentials the seller presents when calling this agent. Contains `schemes` (array with one auth scheme) and `credentials` (token, min 32 characters). | -| `categories` | array | No | Governance categories this agent handles (e.g., `["budget_authority", "geo_compliance"]`). When omitted, the agent handles all categories. Max 20 categories, each max 64 characters. | ## Response @@ -144,6 +143,8 @@ The seller MUST verify that the authenticated agent has authority over each refe ### Different governance agents per account +A single `sync_governance` call can register a distinct agent per account — each account still binds to exactly one agent, but accounts on the same call need not share it. + ```javascript JavaScript @@ -156,12 +157,11 @@ const result = await testAgent.syncGovernance({ account: { account_id: "acct-social-001" }, governance_agents: [ { - url: "https://governance.pinnacle-media.com/budget", + url: "https://governance.pinnacle-media.com", authentication: { schemes: ["Bearer"], credentials: "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - categories: ["budget_authority"] + } } ] }, @@ -169,12 +169,11 @@ const result = await testAgent.syncGovernance({ account: { account_id: "acct-social-002" }, governance_agents: [ { - url: "https://governance.pinnacle-media.com/compliance", + url: "https://governance.acme-buyer.com", authentication: { schemes: ["Bearer"], credentials: "gov-token-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" - }, - categories: ["geo_compliance"] + } } ] } @@ -207,12 +206,11 @@ async def main(): "account": {"account_id": "acct-social-001"}, "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/budget", + "url": "https://governance.pinnacle-media.com", "authentication": { "schemes": ["Bearer"], "credentials": "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - "categories": ["budget_authority"] + } } ] }, @@ -220,12 +218,11 @@ async def main(): "account": {"account_id": "acct-social-002"}, "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/compliance", + "url": "https://governance.acme-buyer.com", "authentication": { "schemes": ["Bearer"], "credentials": "gov-token-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" - }, - "categories": ["geo_compliance"] + } } ] } @@ -259,12 +256,11 @@ asyncio.run(main()) }, "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/compliance", + "url": "https://governance.pinnacle-media.com", "authentication": { "schemes": ["Bearer"], "credentials": "gov-token-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" - }, - "categories": ["geo_compliance", "strategic_alignment"] + } } ] } @@ -278,6 +274,14 @@ asyncio.run(main()) Call `sync_governance` again with updated `authentication`. Replace semantics means the new credentials overwrite the previous configuration. +### Migrating from pre-3.1 multi-agent registration + +Earlier drafts of 3.0 allowed up to 10 governance agents per account with per-agent `categories`. 3.1 constrains `governance_agents` to exactly one entry and removes `categories`. Buyers that registered more than one agent against the previous shape MUST collapse to a single agent on their next `sync_governance` call; the seller's persisted state is replaced. The new request schema rejects more than one agent outright, so no "mixed-mode" window exists. + +**Buyer-side collapse decision.** Which of the previously-registered agents becomes the single agent is a buyer-internal decision — the protocol does not rank or recommend. Typical paths: (a) keep the agent with the broadest policy coverage (usually the budget/spend-authority agent) and fold specialist logic (legal, brand safety, regulatory review) into it as internal workflow; (b) deploy a new "front-door" governance agent that fans out to the previous specialists internally, and register only that agent; (c) decide one specialist was the real governance surface and retire the others. Surface the internal decomposition to auditors via `categories_evaluated` and `findings[].details` on check responses so the audit trail retains what each internal reviewer contributed. + +**Seller-side.** Sellers MAY, on first boot under the new schema, collapse previously-persisted multi-agent state to the first entry (ordered by original sync position) and log the migration to their audit trail. Sellers SHOULD surface a clear error to buyers whose next `sync_governance` call attempts to re-register multiple agents, pointing at this migration guidance. + ## Error Handling | Error Code | Description | Resolution | diff --git a/docs/governance/campaign/specification.mdx b/docs/governance/campaign/specification.mdx index 2d5c618ee1..e931a3d74c 100644 --- a/docs/governance/campaign/specification.mdx +++ b/docs/governance/campaign/specification.mdx @@ -625,14 +625,14 @@ The seller's response includes `planned_delivery` -- what the seller will actual `planned_delivery` is the seller's interpretation of the request -- the actual delivery parameters they will use. It serves two purposes: -1. **Governance checks** -- When the account has `governance_agents`, the seller sends `planned_delivery` to the governance agent(s) for verification before confirming the media buy. +1. **Governance checks** -- When the account has a governance agent configured, the seller sends `planned_delivery` to the governance agent for verification before confirming the media buy. 2. **Transparency** -- The buyer can compare `planned_delivery` against what they requested to catch discrepancies early, before delivery begins. ## Governance checks Campaign Governance's buyer-side validation has a trust limitation: the buyer's orchestrator grades its own homework. An LLM agent could hallucinate governance approval, skip validation, or misrepresent what was validated. Seller-side governance checks close this gap by giving sellers an independent way to confirm that purchases are approved. -The seller POSTs to the buyer's `governance_agents` URLs when governed action events occur. The governance agent maintains all state and correlates requests by `plan_id` + `governance_context` -- the seller does not need to track governance history or chain IDs across calls. +The seller POSTs to the buyer's configured governance agent URL when governed action events occur. The governance agent maintains all state and correlates requests by `plan_id` + `governance_context` -- the seller does not need to track governance history or chain IDs across calls. ### Both checks must pass @@ -982,13 +982,20 @@ Governance agent implementations SHOULD respond to `check_governance` calls with The seller calls each governance agent at its registered URL using MCP over HTTP (Streamable HTTP transport). The request is an MCP `tools/call` invocation with tool name `check_governance` and the request arguments as the tool input. Authentication uses the Bearer token from the agent's `authentication.credentials` in the `Authorization` header. -### Multi-agent composition +### One governance agent per account -Accounts MAY register multiple governance agents via [`sync_governance`](/docs/accounts/tasks/sync_governance), each responsible for different validation categories. For example, one agent handles budget authority and strategic alignment while another handles regulatory compliance and brand policy. +An account binds to exactly one governance agent per [`sync_governance`](/docs/accounts/tasks/sync_governance). Registration is single-agent by schema — `governance_agents` is an array because that is what 3.0 shipped with, constrained to `maxItems: 1` as a load-bearing invariant (not a staging post toward relaxation). The envelope carries a single `governance_context` token; all lifecycle calls route to that one agent. Loosening the cap would require a coordinated change across `sync_governance`, the protocol envelope, and every lifecycle task that threads the token — that change is not planned. -When multiple governance agents are registered, the seller MUST call each agent whose `categories` overlap with the action being validated. All applicable agents must approve for the action to proceed (unanimous approval). If any agent returns `denied`, the action is blocked. +This is deliberate. A governance plan is unitary — budget authority, delivery monitoring, brand safety, and regulatory compliance are not independent specialisms held by different authorities. They are phases and facets of the same evaluation against the same plan state: -For accounts with a single governance agent, pass a one-element array. +- **Authorization, fidelity, and drift are phases, not specialisms.** `check_governance` already separates them on the `phase` axis (`purchase` / `modification` / `delivery`). Splitting them across agents partitions the same plan state across separate authorities, which can only produce drift, disagreement, or duplicated re-reads of the same plan. +- **Regulatory rules are encoded in the plan, not held by a separate agent.** `enforced_policies`, `restricted_attributes`, `policy_ids`, and `human_review_required` live on the plan itself. A "regulatory compliance" agent separate from the "spend authority" agent would re-evaluate the same plan and reach the same decision, or diverge — neither is useful. +- **Internal specialist review belongs inside the governance agent.** A buyer that wants legal, brand-safety, or category-specialist review composes those reviewers behind a single governance-agent endpoint (human review, internal routing, multi-reviewer consensus are all governance-agent-internal concerns). The protocol sees one agent; the agent's internal organization is the agent's business. +- **One lifecycle, one token, one audit trail.** Plan binding (`plan_hash`), signed context (`governance_context`), and `get_plan_audit_logs` are all single-agent designs. A single agent is what makes post-hoc accountability ("this transaction was authorized under plan state X by agent Y at time T") a clean, verifiable claim. + +Buyers that need internal specialist review (legal, brand safety, category) compose those reviewers inside the governance agent they configure — the protocol does not surface the split. + +Related specialist reviews that sit adjacent to (not inside) campaign governance — brand-safety pre-screen of creatives, property-list policy, content-standards evaluation — are separate governance surfaces with their own agents and their own lifecycle (see [`build_creative`](/docs/creative/task-reference/build_creative), property governance, content-standards governance). Campaign governance speaks only for the plan. ### Governance checks and the governance loop @@ -1005,7 +1012,7 @@ Governance checks complement the buyer-side governance loop, they do not replace The `delivery` phase gives the governance agent real-time visibility into what sellers are actually delivering. The buyer-side `report_plan_outcome` depends on the orchestrator reporting honestly; the `delivery` phase gets reports directly from the seller. -The buyer-side and seller-side governance checks MAY be handled by the same agent or by separate agents. The protocol does not prescribe the relationship -- only that the seller can call the `governance_agents` URLs registered on the account. +The buyer-side and seller-side governance checks hit the same agent — the one registered on the account via `sync_governance`. The orchestrator calls it for intent checks and the seller calls it for execution checks; both conversations reach the same authority with the same plan state. ## Orchestrator integration pattern diff --git a/docs/governance/campaign/tasks/check_governance.mdx b/docs/governance/campaign/tasks/check_governance.mdx index f28bb51916..3fb76a893e 100644 --- a/docs/governance/campaign/tasks/check_governance.mdx +++ b/docs/governance/campaign/tasks/check_governance.mdx @@ -20,6 +20,8 @@ Universal governance check for campaign actions. Both the orchestrator (buyer-si The governance agent maintains all state. Callers do not chain check IDs or track conversation history -- they post the action, and the governance agent correlates by `plan_id`. On subsequent lifecycle checks, callers include `governance_context` from the prior response for continuity. +An account binds to one governance agent (see [`sync_governance`](/docs/accounts/tasks/sync_governance) and [One governance agent per account](/docs/governance/campaign/specification#one-governance-agent-per-account)). All lifecycle calls for a governed action go to that same agent. + ## Check types ### Intent checks (orchestrator) @@ -35,7 +37,7 @@ The orchestrator calls `check_governance` with `tool` and `payload` before sendi ### Execution checks (seller) -The seller calls `check_governance` with `governance_context` and `planned_delivery` when processing a request on an account that has `governance_agents` (set via [`sync_governance`](/docs/accounts/tasks/sync_governance)). Execution checks are always binding — if the governance agent denies, the seller must not proceed. +The seller calls `check_governance` with `governance_context` and `planned_delivery` when processing a request on an account that has a governance agent configured (set via [`sync_governance`](/docs/accounts/tasks/sync_governance)). Execution checks are always binding — if the governance agent denies, the seller must not proceed. Before executing the check, the seller verifies the signed `governance_context` token that arrived on the protocol envelope from the buyer. The buyer produces an **intent-phase** token (per the [JWS profile](/docs/building/implementation/security#adcp-jws-profile)); the seller's own execution check produces the `purchase`/`modification`/`delivery`-phase tokens bound to the assigned `media_buy_id` for the rest of the lifecycle. @@ -457,9 +459,9 @@ The seller MUST adjust pacing and re-call `check_governance` immediately. The `n | `status` | enum | `approved`, `denied`, or `conditions`. | | `plan_id` | string | Echoed from request. | | `explanation` | string | Human-readable explanation of the decision. | -| `findings` | array | Per-category issues found. Present when status is `denied` or `conditions`. MAY also be present on `approved` for informational findings. Each finding has `category_id`, `severity`, `explanation`, and optionally `policy_id`, `details`, `confidence` (0-1), and `uncertainty_reason`. | +| `findings` | array | Per-category issues found. Present when status is `denied` or `conditions`. MAY also be present on `approved` for informational findings. Each finding has `category_id`, `severity`, `explanation`, and optionally `policy_id`, `details`, `confidence` (0-1), and `uncertainty_reason`. `category_id` is an **agent-internal label**, not a protocol-level enum — treat as opaque for display/audit, not for machine pattern-matching. | | `conditions` | array | Present when status is `conditions`. Adjustments the caller must make before re-calling. | -| `categories_evaluated` | string[] | Governance categories evaluated during this check (e.g., `budget_authority`, `geo_compliance`, `channel_compliance`). Useful for verifying which validations ran. | +| `categories_evaluated` | string[] | Governance categories evaluated during this check (e.g., `budget_authority`, `geo_compliance`, `channel_compliance`). **Agent-internal labels** — each string is defined by the governance agent's policy model and is how internal specialist review (legal, brand safety, category) surfaces for audit from behind the agent's single endpoint. Not a protocol enum; not safe to pattern-match against a fixed list. | | `policies_evaluated` | string[] | Registry policy IDs evaluated during this check. | | `mode` | enum | `audit`, `advisory`, or `enforce` — governance mode active when this check was evaluated. Recorded by the governance agent from its runtime configuration at check time, not from a plan field. Lets counterparties, regulators, and auditors distinguish whether an `approved` decision reflects deliberate `enforce` enforcement or `audit`-mode silent logging. | | `expires_at` | string | Present when status is `approved` or `conditions`. The caller must act before this time or re-call. A lapsed approval is no approval. | diff --git a/docs/governance/campaign/tasks/get_plan_audit_logs.mdx b/docs/governance/campaign/tasks/get_plan_audit_logs.mdx index 2c8dccfb7d..201d0ceb58 100644 --- a/docs/governance/campaign/tasks/get_plan_audit_logs.mdx +++ b/docs/governance/campaign/tasks/get_plan_audit_logs.mdx @@ -255,7 +255,7 @@ You can combine `plan_ids` and `portfolio_plan_ids` to query both specific plans | `plans[].entries[].mode` | enum | `audit`, `advisory`, or `enforce` — governance mode active when this check was evaluated. Recorded by the governance agent from its runtime configuration at check time, not from a plan field. Present on check entries; absent on outcome entries and on governance agents that have not yet adopted this field. Lets auditors distinguish `approved` decisions made under `enforce` from those made under `audit` (where the agent could not have blocked anything). | | `plans[].entries[].explanation` | string | Human-readable explanation of the governance decision (present for `check` entries). | | `plans[].entries[].policies_evaluated` | array | Registry policy IDs evaluated during this check. | -| `plans[].entries[].categories_evaluated` | array | Governance categories evaluated (e.g., `budget_authority`, `regulatory_compliance`). | +| `plans[].entries[].categories_evaluated` | array | Governance categories evaluated (e.g., `budget_authority`, `regulatory_compliance`). **Agent-internal labels**, not a protocol-level enum — each string is defined by the governance agent's policy model. Since one governance agent per account composes all specialist review (legal, brand safety, category-specialist reviewers) behind its single endpoint, `categories_evaluated` and `findings[].details` are how internal decomposition surfaces for audit. Consumers MUST treat values as opaque labels: pattern-matching against a fixed list is not safe across agents. | | `plans[].entries[].findings` | array | Findings from this check, including category, severity, policy ID, explanation, and confidence. | | `plans[].entries[].governance_context` | string | Opaque governance context identifying the governed action this entry belongs to. | | `plans[].entries[].plan_hash` | string | Audit-layer binding to the plan revision this attestation was evaluated over — `base64url_no_pad(SHA-256(JCS(plan_payload)))` per [Plan binding and audit](/docs/governance/campaign/specification#plan-binding-and-audit). Present on `check` entries. Auditors and buyer-side compliance tooling verify by recomputing over the retained plan revision and byte-comparing the decoded 32-byte digests. | diff --git a/static/compliance/source/protocols/governance/index.yaml b/static/compliance/source/protocols/governance/index.yaml index 8839db4847..539222534c 100644 --- a/static/compliance/source/protocols/governance/index.yaml +++ b/static/compliance/source/protocols/governance/index.yaml @@ -200,7 +200,6 @@ phases: 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: diff --git a/static/compliance/source/protocols/media-buy/index.yaml b/static/compliance/source/protocols/media-buy/index.yaml index 38c421a694..17d6036eb2 100644 --- a/static/compliance/source/protocols/media-buy/index.yaml +++ b/static/compliance/source/protocols/media-buy/index.yaml @@ -219,7 +219,6 @@ phases: 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" diff --git a/static/compliance/source/protocols/media-buy/scenarios/governance_approved.yaml b/static/compliance/source/protocols/media-buy/scenarios/governance_approved.yaml index 1d7a43b041..64480d85af 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/governance_approved.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/governance_approved.yaml @@ -145,7 +145,6 @@ phases: 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 diff --git a/static/compliance/source/protocols/media-buy/scenarios/governance_conditions.yaml b/static/compliance/source/protocols/media-buy/scenarios/governance_conditions.yaml index 876db0da4e..9ee102b689 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/governance_conditions.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/governance_conditions.yaml @@ -129,7 +129,6 @@ phases: 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 diff --git a/static/compliance/source/protocols/media-buy/scenarios/governance_denied.yaml b/static/compliance/source/protocols/media-buy/scenarios/governance_denied.yaml index c93b8fdf46..6e89e275d3 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/governance_denied.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/governance_denied.yaml @@ -122,7 +122,6 @@ phases: 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 diff --git a/static/compliance/source/protocols/media-buy/scenarios/governance_denied_recovery.yaml b/static/compliance/source/protocols/media-buy/scenarios/governance_denied_recovery.yaml index c16e8780f5..9f954962bf 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/governance_denied_recovery.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/governance_denied_recovery.yaml @@ -122,7 +122,6 @@ phases: 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 diff --git a/static/compliance/source/specialisms/brand-rights/scenarios/governance_denied.yaml b/static/compliance/source/specialisms/brand-rights/scenarios/governance_denied.yaml index e65c6547e9..393fb22a97 100644 --- a/static/compliance/source/specialisms/brand-rights/scenarios/governance_denied.yaml +++ b/static/compliance/source/specialisms/brand-rights/scenarios/governance_denied.yaml @@ -121,7 +121,6 @@ phases: 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 diff --git a/static/compliance/source/specialisms/governance-aware-seller/index.yaml b/static/compliance/source/specialisms/governance-aware-seller/index.yaml index ce4be2d241..406f53a857 100644 --- a/static/compliance/source/specialisms/governance-aware-seller/index.yaml +++ b/static/compliance/source/specialisms/governance-aware-seller/index.yaml @@ -59,8 +59,9 @@ narrative: | 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, + - accepts `sync_governance` to register the buyer's single governance agent + URL for the account (one agent per account; internal specialist review + composes inside the agent, not at registration — see #3010 / #3015), - calls `check_governance` on the registered agent before confirming a spend-committing request, - returns `GOVERNANCE_DENIED` with the governance findings propagated diff --git a/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml b/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml index 5a6e38e782..acef63520e 100644 --- a/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml +++ b/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml @@ -124,7 +124,6 @@ phases: 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 diff --git a/static/schemas/source/account/sync-governance-request.json b/static/schemas/source/account/sync-governance-request.json index 9003d2b3ab..52d6d6e4cd 100644 --- a/static/schemas/source/account/sync-governance-request.json +++ b/static/schemas/source/account/sync-governance-request.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/account/sync-governance-request.json", "title": "Sync Governance Request", - "description": "Sync governance agent endpoints against specific accounts. The seller persists these governance agents and calls them for approval during media buy lifecycle events via check_governance. Uses replace semantics: each call replaces any previously synced agents on the specified accounts. The seller MUST verify that the authenticated agent has authority over each referenced account before persisting governance agents.", + "description": "Sync the governance agent endpoint against specific accounts. The seller persists the governance agent and calls it for approval during media buy lifecycle events via check_governance. Uses replace semantics: each call replaces any previously synced agent on the specified accounts. The seller MUST verify that the authenticated agent has authority over each referenced account before persisting the governance agent.\n\nEach account binds to exactly one governance agent. A plan is unitary — budget authority, delivery monitoring, and regulatory compliance are phases of the same evaluation (`purchase` / `modification` / `delivery` on check_governance), not specialisms held by different agents — so a single agent owns the full lifecycle. Buyers that need internal specialist review (e.g., a separate legal reviewer) compose that inside the governance agent, not at the registration layer. `governance_agents` is an array (not a scalar) because that is the shape 3.0 shipped with and existing senders MUST continue to work; the `maxItems: 1` constraint is load-bearing and not anticipated to relax. The single-agent rule is also baked into the wire below this layer (`protocol-envelope.governance_context` is singular), so loosening `maxItems` here would require a coordinated change across the envelope and every task that threads the context.", "x-mutates-state": true, "type": "object", "properties": { @@ -31,7 +31,7 @@ }, "governance_agents": { "type": "array", - "description": "Governance agent endpoints for this account. The seller calls these agents via check_governance during media buy lifecycle events.", + "description": "Governance agent endpoint for this account. Exactly one entry — the single agent that owns the account's full governance lifecycle. The seller calls this agent via check_governance during media buy lifecycle events. The array shape is preserved for wire compatibility with 3.0 senders; `maxItems: 1` is load-bearing and mirrors the singular `governance_context` on the protocol envelope.", "items": { "type": "object", "properties": { @@ -64,16 +64,6 @@ "credentials" ], "additionalProperties": false - }, - "categories": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64, - "pattern": "^[a-z][a-z0-9_]*$" - }, - "description": "Governance categories this agent handles (e.g., ['budget_authority', 'strategic_alignment']). When omitted, the agent handles all categories.", - "maxItems": 20 } }, "required": [ @@ -83,7 +73,7 @@ "additionalProperties": false }, "minItems": 1, - "maxItems": 10 + "maxItems": 1 } }, "required": [ @@ -109,7 +99,7 @@ "additionalProperties": true, "examples": [ { - "description": "Sync governance agents on explicit accounts (by account_id)", + "description": "Sync the governance agent on an explicit account (by account_id)", "data": { "idempotency_key": "e1b3a6c8-5678-489a-bcde-f01234567891", "accounts": [ @@ -119,16 +109,13 @@ }, "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/budget", + "url": "https://governance.pinnacle-media.com", "authentication": { "schemes": [ "Bearer" ], "credentials": "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - "categories": [ - "budget_authority" - ] + } } ] } @@ -136,7 +123,7 @@ } }, { - "description": "Sync governance agents on implicit accounts (by brand + operator)", + "description": "Sync the governance agent on an implicit account (by brand + operator)", "data": { "idempotency_key": "f2c4b7d9-6789-489b-cdef-012345678902", "accounts": [ @@ -150,16 +137,13 @@ }, "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/compliance", + "url": "https://governance.pinnacle-media.com", "authentication": { "schemes": [ "Bearer" ], "credentials": "gov-token-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" - }, - "categories": [ - "geo_compliance" - ] + } } ] } diff --git a/static/schemas/source/account/sync-governance-response.json b/static/schemas/source/account/sync-governance-response.json index dff2b0e25a..f93c77ebf5 100644 --- a/static/schemas/source/account/sync-governance-response.json +++ b/static/schemas/source/account/sync-governance-response.json @@ -27,7 +27,7 @@ }, "governance_agents": { "type": "array", - "description": "Governance agents now synced on this account. Reflects the persisted state after sync.", + "description": "Governance agent now synced on this account. Reflects the persisted state after sync. Exactly one entry; the array shape mirrors the request schema and the one-agent-per-account invariant. See sync_governance request schema.", "items": { "type": "object", "properties": { @@ -36,17 +36,13 @@ "format": "uri", "pattern": "^https://", "description": "Governance agent endpoint URL." - }, - "categories": { - "type": "array", - "items": { "type": "string", "maxLength": 64, "pattern": "^[a-z][a-z0-9_]*$" }, - "description": "Governance categories this agent handles.", - "maxItems": 20 } }, "required": ["url"], "additionalProperties": false - } + }, + "minItems": 1, + "maxItems": 1 }, "errors": { "type": "array", @@ -110,8 +106,7 @@ "status": "synced", "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/budget", - "categories": ["budget_authority"] + "url": "https://governance.pinnacle-media.com" } ] }, @@ -120,8 +115,7 @@ "status": "synced", "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/compliance", - "categories": ["geo_compliance"] + "url": "https://governance.acme-buyer.com" } ] } @@ -137,8 +131,7 @@ "status": "synced", "governance_agents": [ { - "url": "https://governance.pinnacle-media.com/budget", - "categories": ["budget_authority"] + "url": "https://governance.pinnacle-media.com" } ] }, diff --git a/static/schemas/source/brand.json b/static/schemas/source/brand.json index af75c794de..1562703e2f 100644 --- a/static/schemas/source/brand.json +++ b/static/schemas/source/brand.json @@ -630,20 +630,6 @@ "pattern": "^https://", "description": "HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`." }, - "scope": { - "type": "array", - "description": "For governance agents: which governance responsibilities this agent handles. Values correspond to compliance specialisms. When a house declares multiple governance agents, scope distinguishes them. When absent on a governance agent, the agent is assumed to handle all governance categories.", - "items": { - "type": "string", - "enum": [ - "spend_authority", - "delivery_monitor", - "brand_safety", - "regulatory_compliance" - ] - }, - "uniqueItems": true - }, "available_uses": { "type": "array", "description": "For rights agents: rights uses available for licensing", @@ -1434,8 +1420,7 @@ "url": "https://agents.pg.com/governance", "id": "pg_governance", "description": "Brand safety and compliance for all P&G brands", - "jwks_uri": "https://agents.pg.com/.well-known/jwks.json", - "scope": ["spend_authority", "delivery_monitor", "brand_safety"] + "jwks_uri": "https://agents.pg.com/.well-known/jwks.json" } ] }, diff --git a/static/schemas/source/core/account.json b/static/schemas/source/core/account.json index e745ea5a73..7cfbb327c9 100644 --- a/static/schemas/source/core/account.json +++ b/static/schemas/source/core/account.json @@ -94,7 +94,7 @@ }, "governance_agents": { "type": "array", - "description": "Governance agent endpoints registered on this account. Authentication credentials are write-only and not included in responses — use sync_governance to set or update credentials.", + "description": "Governance agent endpoint registered on this account. Exactly one entry per sync_governance's one-agent-per-account invariant. The array shape is preserved for wire compatibility with 3.0; `maxItems: 1` is load-bearing and mirrors the singular `governance_context` on the protocol envelope. Authentication credentials are write-only and not included in responses — use sync_governance to set or update credentials.", "items": { "type": "object", "properties": { @@ -103,18 +103,13 @@ "format": "uri", "pattern": "^https://", "description": "Governance agent endpoint URL. Must use HTTPS." - }, - "categories": { - "type": "array", - "items": { "type": "string", "maxLength": 64, "pattern": "^[a-z][a-z0-9_]*$" }, - "description": "Governance categories this agent handles (e.g., ['budget_authority', 'strategic_alignment']). When omitted, the agent handles all categories.", - "maxItems": 20 } }, "required": ["url"], "additionalProperties": false }, - "maxItems": 10 + "minItems": 1, + "maxItems": 1 }, "reporting_bucket": { "type": "object", diff --git a/static/schemas/source/core/protocol-envelope.json b/static/schemas/source/core/protocol-envelope.json index 603e585ce5..c3944b0aba 100644 --- a/static/schemas/source/core/protocol-envelope.json +++ b/static/schemas/source/core/protocol-envelope.json @@ -38,7 +38,7 @@ }, "governance_context": { "type": "string", - "description": "Governance context token issued by a governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle.\n\nValue format: in 3.0 governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security — Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security — Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", "minLength": 1, "maxLength": 4096, "pattern": "^[\\x20-\\x7E]+$" diff --git a/static/schemas/source/governance/check-governance-response.json b/static/schemas/source/governance/check-governance-response.json index 7971eb6be2..567c21b5e5 100644 --- a/static/schemas/source/governance/check-governance-response.json +++ b/static/schemas/source/governance/check-governance-response.json @@ -31,7 +31,7 @@ "properties": { "category_id": { "type": "string", - "description": "Validation category that flagged the issue (e.g., 'budget_compliance', 'regulatory_compliance', 'brand_safety')." + "description": "Validation category that flagged the issue (e.g., 'budget_compliance', 'regulatory_compliance', 'brand_safety'). This is an **agent-internal** taxonomy: the string is defined by the governance agent's own policy model and is not constrained to any protocol-level enum. Sellers and buyers MUST NOT pattern-match `category_id` values against a fixed list — treat them as opaque labels with human-readable significance for audit but no machine-level contract. See the Campaign Governance specification for how an agent composes internal specialist review behind one endpoint." }, "policy_id": { "type": "string", @@ -114,7 +114,7 @@ "items": { "type": "string" }, - "description": "Governance categories evaluated during this check." + "description": "Governance categories evaluated during this check. Each value is an **agent-internal** label (e.g., `budget_authority`, `regulatory_compliance`, or any internal-reviewer key the agent's policy model defines) — not a protocol-level enum. Since one governance agent per account composes all specialist review behind its single endpoint, `categories_evaluated` is how that internal decomposition surfaces to auditors. Consumers MUST treat values as opaque labels for display and audit, not as a machine-level contract." }, "policies_evaluated": { "type": "array",