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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,31 @@

Directory scenarios in `scenarios.json` can carry `requests`, which a harness signs with a named key and verifies against that directory. D03 and D11 use them: a request signed with a key that has no valid directory signature gets `403`. D15 adds malformed extra Ed25519 keys, one with no `x` and one with a 31-byte `x`, which a verifier ignores.

## 3.2.11

### Patch Changes

- a07826a: Open the 3.2 schemas that were closed by accident, so a 3.2 validator no longer rejects a newer 3.x payload because it carries a field 3.2 does not know. Backports the 3.3 opening decided by the working group on 2026-11-09.

`additionalProperties` goes from `false` to `true` at 674 of the 775 closed locations in the 3.2 source tree.

101 locations stay closed, matching 3.3: most for a structural reason, and the commercial terms as a hold until 4.0.

- `oneOf` branches told apart only by which keys are present (`a2ui/bound-value.json`, `brand.json`).
- Metric qualifiers and forecast dimensions, which consumers join on.
- The JWS response envelope.
- Privacy boundaries: the TMP match requests, the not-found error, the per-agent permission-denied and billing-not-permitted details, the `get_principal` recognized, unconfigured and failed results, the failed `sync_principal` result, and the `get_reporting_status` unavailable-lookup result.
- The credential-free reporting destinations and the destination `setup` action.
- `placement-presentation`, which carries no key material or markup.
- `patternProperties` maps keyed by IDs.
- Held closed until 4.0: the commercial terms and the hard `proposal-budget-constraint`. This is a hold, not a structural reason; 4.0 gives `commercial_terms` a declared extension point with must-understand.

Where closure was the only thing rejecting a specific field that the protocol defines elsewhere or forbids (for example `trackers` on `unassign`, `staged_update` on `creative.status_changed`, `auth_token` on list-application receipts, or credentials on a notification-config readback), the opened schema now rejects that field by name with `not`. `access_token` on the delivery-ready webhook is rejected as a credential clamp. The same applies to unions whose branches were told apart only by which keys are present: an account ref, compact or not, carries `account_id` or the natural key, not both; a geo-region country entry carries `all_values` or `values`; image bleed is uniform or per-side; a deprecated fused-system postal area carries no `country`; and a reporting-receipt result carries `receipt` or `adjustment_receipt`, a failed one neither. Each node with a named rejection says in its description what it rejects and why, since generated types cannot express `not`. Descriptions of opened schemas no longer say they are closed.

The MCP model-context projection now omits `additionalProperties: true` as well as `false`, since `true` is the JSON Schema default.
- 8558eef: Stop `creative/policy_backed_rejections` failing sellers that do not enforce registry policies (adcp#8850). The storyboard seeds a product with `enforced_policies` through the controller and then asserted that the seller published and enforced both fixture policies, so a seller that never claimed registry policy enforcement failed it. The 3.2 schemas have no capability a seller can use to opt out, so this patch gates on what the seller publishes instead: the discovery read now asserts only a schema-valid `get_products` response and captures the returned products, and the policy-universe confirmation and the dual-violation `sync_creatives` phases run only when the first discovered product carries a non-empty `enforced_policies` list. The gate reads the seller's response, not the controller seed. A seller that omits the field, publishes an empty list, or returns no products under `required_policies` is not sent the violating creative and is not failed; its enforcement phases are skipped. A seller that publishes any enforced policy runs every step and is still graded on the exact two-policy universe and the one-`CREATIVE_REJECTED`-entry-per-policy result. No schema fields change. The 3.3 line gates the storyboard on the new `creative.supports_registry_policy_enforcement` capability instead (adcp#8857).
- 8558eef: Stop signed functional storyboard requests from exhausting the per-keyid cap that the signed-requests `rate_abuse` vector grades (adcp#8877). Functional dispatch in `test-kits/signed-requests-runner.yaml` signed with `test-ed25519-2026`, the same keyid vector 020-rate-abuse caps at 100 for the test counterparty, so a full compliance run could hit the cap before the signed-requests vectors started and fail them with `request_signature_rate_abuse`. Functional dispatch now signs with a dedicated `test-functional-2026` Ed25519 key that no conformance vector uses, published in the request-signing `keys.json` and declared in `runner_signing_keys`. The `rate_abuse` block now states that agents MAY lower the per-keyid cap to the grading target for the vector 020 keyid (`test-ed25519-2026`) only, and MUST NOT apply the lowered cap to the functional-dispatch keyid. Sellers that register the runner signing keys individually rather than loading the published `keys.json` must add `test-functional-2026` to their test counterparty. Sellers that configure one global per-keyid cap on their replay store (as the SDK reference stores take, e.g. `maxEntriesPerKeyid: 100`) must scope the lowered cap to `test-ed25519-2026`, for example with a keyid-aware wrapper, or functional storyboards hit the cap on `test-functional-2026` instead.

## 3.2.10

### Patch Changes
Expand Down
196 changes: 196 additions & 0 deletions dist/compliance/3.2.11/domains/brand/index.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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
value: "REFERENCE_NOT_FOUND"
description: "Error code indicates brand-not-found. REFERENCE_NOT_FOUND is the canonical fallback per error-handling.mdx (brands lack a dedicated *_NOT_FOUND code)."

- id: brand_discovery
title: "Optional brand discovery"
narrative: |
Agents that implement the experimental `search_brands` task expose a
schema-valid discovery surface without making that task part of the
baseline brand protocol requirement.

steps:
- id: search_brands
title: "Search the agent's brand roster"
task: search_brands
schema_ref: "brand/search-brands-request.json"
response_schema_ref: "brand/search-brands-response.json"
doc_ref: "/brand-protocol/tasks/search_brands"
comply_scenario: brand_discovery
requires_tool: search_brands
stateful: false
expected: |
Return a schema-valid ranked array of brand stubs. An empty array is
valid when the agent has no matching roster entry.
sample_request:
query: "automotive brands with electric vehicles"
countries: ["US"]
context:
correlation_id: "brand_baseline--search_brands"
validations:
- check: response_schema
description: "Response matches search-brands-response.json schema"
- check: field_present
path: "brands"
description: "Discovery response includes the brands array"
- check: field_value
path: "context.correlation_id"
value: "brand_baseline--search_brands"
description: "Context correlation_id returned unchanged"
Loading
Loading