Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/governance-multi-agent-envelope.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion dist/docs/3.0.1/contributing/storyboard-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<<PLACEHOLDER>>` 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.
Expand Down
62 changes: 33 additions & 29 deletions docs/accounts/tasks/sync_governance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:

<CodeGroup>

Expand All @@ -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"]
}
}
]
}
Expand All @@ -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)}`);
}
Expand All @@ -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"]
}
}
]
}
Expand All @@ -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}")

Expand All @@ -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

Expand All @@ -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.

<CodeGroup>

```javascript JavaScript
Expand All @@ -156,25 +157,23 @@ 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"]
}
}
]
},
{
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"]
}
}
]
}
Expand Down Expand Up @@ -207,25 +206,23 @@ 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"]
}
}
]
},
{
"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"]
}
}
]
}
Expand Down Expand Up @@ -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"]
}
}
]
}
Expand All @@ -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 |
Expand Down
Loading
Loading