From 81b823cfc32d750b68ebdd062ee636e6eda7becc Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 29 Apr 2026 11:00:02 -0400 Subject: [PATCH 01/13] docs(brand-protocol): RFC for distributed brand.json (#3409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Draft RFC proposing per-brand canonical brand.json documents linked by mutual-assertion pointers, replacing the monolithic-with-inline-children shape. Hosting (static / CDN / brand-agent / AAO / self) stays an implementation choice independent of the data model. Key proposed changes (subject to discussion): - Each brand publishes one canonical brand.json owning its own attributes - New `house` pointer for declaring the immediate parent (multi-level chains via recursion) - New `brand_refs[]` (pointer-only) replacing inline `brands[]` content - New `house_attributes` block for inheritable house-wide metadata - Mutual-assertion as the trust primitive — child's `house` must be reciprocated by parent's `brand_refs[]` Migration path: 3.x accepts both shapes with deprecation warnings; brand-protocol 2.0 (decoupled from AdCP major) cuts over. Lives at docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx under a new "Proposals" subgroup. Not yet normative — needs spec-owner sign-off before any code or schema changes land. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/distributed-brand-json-rfc.md | 16 + docs.json | 6 + .../proposals/distributed-brand-json-rfc.mdx | 337 ++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 .changeset/distributed-brand-json-rfc.md create mode 100644 docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx diff --git a/.changeset/distributed-brand-json-rfc.md b/.changeset/distributed-brand-json-rfc.md new file mode 100644 index 0000000000..d6b520e6d0 --- /dev/null +++ b/.changeset/distributed-brand-json-rfc.md @@ -0,0 +1,16 @@ +--- +--- + +Draft RFC for distributed brand.json — propose evolving from monolithic house portfolio (one big document containing inline child brand definitions) to a collection of canonical per-brand documents linked by mutual-assertion pointers. + +The RFC lives at `docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx` (linked from the brand protocol nav under "Proposals"). Tracking discussion is in [#3409](https://github.com/adcontextprotocol/adcp/issues/3409). Not yet normative — needs spec-owner sign-off before any code or schema changes land. + +Key proposed changes (subject to discussion): +- Each brand publishes one canonical brand.json owning its own attributes +- New `house` pointer field for declaring an immediate parent (multi-level chains via recursion) +- New `brand_refs[]` field replacing inline `brands[]` content (pointer-only `{id, domain}`) +- New `house_attributes` block for inheritable house-wide metadata (privacy, compliance, corporate entity) +- Mutual-assertion as the canonical trust primitive — child's `house` must be reciprocated by parent's `brand_refs[]` +- Hosting (static, CDN, brand-agent, AAO-hosted, self-hosted) is independent of the data model and stays an implementation choice + +Migration path defined: 3.x accepts both shapes with deprecation warnings; brand-protocol 2.0 (decoupled from AdCP major) cuts over. diff --git a/docs.json b/docs.json index 2ff138757b..b25373362e 100644 --- a/docs.json +++ b/docs.json @@ -502,6 +502,12 @@ "docs/brand-protocol/tasks/acquire_rights", "docs/brand-protocol/tasks/update_rights" ] + }, + { + "group": "Proposals", + "pages": [ + "docs/brand-protocol/proposals/distributed-brand-json-rfc" + ] } ] }, diff --git a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx new file mode 100644 index 0000000000..8d8e12cfb4 --- /dev/null +++ b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx @@ -0,0 +1,337 @@ +--- +title: "RFC — Distributed brand.json" +description: "Proposal to evolve brand.json from monolithic house portfolio to distributed per-brand canonical documents linked by mutual-assertion." +"og:title": "AdCP — Distributed brand.json RFC" +--- + + +**RFC — discussion in progress.** This is a proposal under discussion in [issue #3409](https://github.com/adcontextprotocol/adcp/issues/3409). Not yet a ratified part of the brand protocol. The current normative spec lives at [brand.json](/docs/brand-protocol/brand-json). + + +## Status + +| Field | Value | +| --- | --- | +| Author | bokelley | +| Status | Proposed | +| Tracking | [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) | +| Target | brand-protocol 1.1 (additive); breaking parts at 2.0 | +| Affects | `core/brand-manifest.json` schema, `docs/brand-protocol/brand-json.mdx` | + +## Summary + +Evolve brand.json from a single house-portfolio document containing inline child brand definitions into a **collection of canonical per-brand documents** linked by mutual-assertion pointers. + +In the current model, a house publishes one large `brand.json` with a `brands[]` array containing inline definitions for every owned brand. In the proposed model, each brand publishes its own canonical document at its own domain (or via `authoritative_location` indirection); the parent's document keeps a `brand_refs[]` array of pointers to those children, not inline copies. A child's document declares its parent via a `house` pointer. Trust between parent and child requires **mutual assertion** at each link. + +This makes brand.json work for the long tail of independent and small publishers, supports multi-level hierarchies (e.g. `streetkix.com → converse.com → nikeinc.com`), keeps hosting decisions independent of the data model (static, CDN, or a brand-agent — any of those works), and gives consumers an unambiguous trust contract. + +## Motivation + +### What's in the spec today + +The current portfolio variant has the parent's brand.json own everything inline: + +```json +{ + "house": { "domain": "nikeinc.com", "name": "Nike, Inc.", "architecture": "hybrid" }, + "brands": [ + { + "id": "nike", + "names": [{"en": "Nike"}], + "keller_type": "master", + "logos": [/* full inline logo data */], + "colors": {/* inline */}, + "tone": {/* inline */}, + "tagline": "Just Do It", + "visual_guidelines": {/* inline */}, + "properties": [{"type": "website", "identifier": "nike.com", "primary": true}] + }, + /* ...one giant block per owned brand... */ + ] +} +``` + +### The problems + +1. **Monolithic.** A holdco like Publicis with 100 subsidiaries publishes one file containing 100 inline brand definitions. Every update to any brand touches the whole document. Every consumer fetches the whole thing. Caching is all-or-nothing. + +2. **No leaf-side authority.** A brand that wants to publish its own brand.json (correct its parent's stale data, run independently of a parent registry, declare a new house relationship after an acquisition) can't — the spec's variant exclusivity gives the parent's portfolio sole authority for any brand it lists. + +3. **Doesn't model multi-level reality.** A 3rd-level brand like StreetKix (Nike → Converse → StreetKix) has no clean expression. Nike's portfolio listing every descendant flattens the structure; Converse can't manage StreetKix without forcing Nike to update. + +4. **Operational mismatch.** Holdcos with central brand teams want to manage subsidiary brand data centrally. Independent brands want self-publish. Sub-brands of a holdco often want to be managed by the parent's team but billed/owned at the brand level. The current model forces a single ops shape on everyone. + +5. **Trust is implicit.** A brand that appears in someone else's portfolio is "owned" by that house with no verification primitive. No way for a consumer to distinguish "Nike asserts Converse is theirs and Converse agrees" from "anyone could publish a brand.json claiming Converse is theirs." + +### What we want + +A data model that: + +- Supports multi-level hierarchies recursively without flattening +- Lets each brand own its own canonical data +- Lets the host of a brand.json (the parent's brand-agent, AAO, the brand itself, a CDN) be an independent operational decision +- Defines an unambiguous trust primitive — when can a consumer trust a parent/child claim? +- Preserves the current portfolio-variant publishers through a transition window + +## Proposal + +### Data model + +Three pieces. + +#### 1. Per-brand canonical documents + +Each brand publishes **one** canonical brand.json describing itself. Identity attributes (`name`, `logos`, `colors`, `fonts`, `tone`, `tagline`, `visual_guidelines`, `voice`, `avatar`) live ONLY here, never inlined elsewhere. + +```json +// converse.com — canonical +{ + "version": "1.0", + "name": "Converse", + "names": [{"en": "Converse"}], + "keller_type": "sub_brand", + "house": { "domain": "nikeinc.com" }, + "logos": [...], + "colors": {...}, + "tone": {...}, + "tagline": "Sneaker for the streets", + "brand_refs": [ + { "id": "streetkix", "domain": "streetkix.com" } + ] +} +``` + +#### 2. `house` pointer (immediate parent) + +A new optional field on brand-manifest. Declares the brand's **immediate** parent — one level up, not the ultimate root. Multi-level chains emerge from following pointers. Crawler walks recursively to find the master. + +```json +"house": { "domain": "nikeinc.com" } +``` + +For the master brand of a house (no parent), the field is omitted. + +#### 3. `brand_refs[]` (replacing inline `brands[]`) + +A new array of pointer objects on the parent's canonical document. Each pointer is intentionally minimal — ID and the canonical domain where the child's full document lives: + +```json +"brand_refs": [ + { "id": "nike", "domain": "nike.com" }, + { "id": "jordan", "domain": "jordan.com" }, + { "id": "converse", "domain": "converse.com" } +] +``` + +No preview fields. No inline content. The protocol contract is unambiguous: any consumer that wants any attribute about a brand fetches that brand's canonical document. There is exactly one source of truth per brand. + +The legacy `brands[]` field stays valid through the migration window (see [Migration](#migration)) but is deprecated. + +### Hosting is independent + +The data model says nothing about where bytes live. Implementations choose: + +| Pattern | How | +| --- | --- | +| Self-hosted at the brand's domain | `domain.com/.well-known/brand.json` is the canonical document | +| AAO-hosted | `domain.com/.well-known/brand.json` is a stub with `authoritative_location: "https://agenticadvertising.org/brands/domain.com/brand.json"` | +| Parent's brand-agent or static server | Same pattern — stub at the brand's own domain points at the parent's canonical URL | +| CDN-fronted | Either of the above with caching infrastructure in front | +| Mixed within a single family | Top-level uses one host; some sub-brands self-host; others use AAO | + +The crawler does not care. It follows the discovery contract: + +1. Fetch `domain.com/.well-known/brand.json` +2. If it has `authoritative_location`, fetch that URL +3. Parse the canonical document there + +This is the same indirection mechanism already in spec; the proposal doesn't change it. It just makes explicit that **any party** can be the host. The brand-agent service spec ([building a brand agent](/docs/brand-protocol/building-a-brand-agent)) is a separate, complementary concept — a brand-agent can serve brand.json content via `get_brand_identity`, but trust still flows from the static document's authenticity, not the agent's identity. + +### Inheritance via `house_attributes` + +For attributes that genuinely belong house-wide — privacy policy, compliance flags, corporate legal entity, jurisdictional data — the master (or any ancestor) may publish a `house_attributes` block that descendants inherit by default: + +```json +// nikeinc.com — master, declares house-wide attributes +{ + "name": "Nike, Inc.", + "house_attributes": { + "privacy_policy_url": "https://nikeinc.com/privacy", + "data_protection_roles": [...], + "compliance_policies": ["no_under_13_targeting"], + "tax_entity": "Nike, Inc." + }, + "brand_refs": [ + { "id": "nike", "domain": "nike.com" }, + { "id": "converse", "domain": "converse.com" } + ] +} +``` + +A descendant resolves its **effective house attributes** by walking up the chain: + +``` +streetkix.com (no house_attributes block) + → converse.com (no house_attributes block) + → nikeinc.com (defines house_attributes) + ↳ effective: { privacy_policy_url: ..., data_protection_roles: ..., ... } +``` + +A descendant may override specific inherited fields: + +```json +// streetkix.com — overrides one inherited compliance attribute +{ + "house": { "domain": "converse.com" }, + "house_attributes_overrides": { + "compliance_policies": ["no_under_13_targeting", "us_only"] + } +} +``` + +The spec defines which top-level fields are **brand-identity** (per-brand only, never inherited): `name`, `names`, `logos`, `colors`, `fonts`, `tone`, `voice`, `tagline`, `visual_guidelines`, `avatar`. + +Everything else is potentially inheritable; the master's `house_attributes` block is the canonical inheritance source. + +## Trust model + +The hard part. Five layers, increasing in strength. + +### 1. Document authenticity (baseline) + +A canonical brand.json document is authentic if and only if it is served via TLS by infrastructure the consumer can verify the brand controls. Two paths to authenticity: + +- **Direct**: served at `domain.com/.well-known/brand.json` over TLS valid for `domain.com`. Standard web-PKI. +- **Indirect via `authoritative_location`**: stub at `domain.com/.well-known/brand.json` (proves domain control) points at a separate URL where the canonical document is served. The pointer is the trust anchor; the consumer trusts the pointed-to URL because the brand's domain control endorsed it. + +This layer establishes "I trust this is the brand's own document." Nothing more. + +### 2. Self-claims (always trusted for self-attributes) + +A brand's own canonical document is authoritative for **its own identity attributes**: `name`, `logos`, `colors`, `voice`, etc. Domain control = self-identity authority. No cross-checking needed. + +### 3. One-sided relationship claims (metadata only — NOT trust) + +A child's document says `house: { domain: "converse.com" }`. Converse's canonical document does NOT include the child's domain in `brand_refs[]`. + +This is **supportive metadata**, not a trust edge. Surface it in UIs as "claimed but unverified." Do not extend inheritance trust through it. Auto-provisioning, member-feature inheritance, billable seat inclusion: **NO**. + +### 4. Mutual assertion (the trust edge) + +Child's document says `house: { domain: "converse.com" }`. Converse's canonical document `brand_refs[]` includes `{ id: ..., domain: ".com" }`. Both verifiable at fetch time. **This is the link's trust edge.** Auto-provisioning, member-feature inheritance, inherited `house_attributes`: YES. + +For multi-level chains (`streetkix → converse → nikeinc`), every link must be mutually-asserted. One non-mutual link breaks the chain at that point — trust extends only as far as the deepest mutually-asserted ancestor. + +### 5. Cryptographic signing / brand-agent endorsement (future, optional) + +Out of scope for v1. Future extensions: a parent's brand-agent could sign attestations about its hosted brands; verifiable credentials; etc. Not needed to ship the data model. + +### Conflict resolution + +| Scenario | Resolution | +| --- | --- | +| Child claims `house: A`, A's `brand_refs[]` does not include child | One-sided. Untrusted. UI: "claimed, unverified." | +| Child claims `house: A`, A's `brand_refs[]` includes child | Mutual. Trusted edge. | +| A's `brand_refs[]` includes child, but child's document has no `house` (or `house: null`) | One-sided in the other direction. A's claim is supportive metadata. Child is treated as having no parent. | +| Two parents (A and B) both have child in `brand_refs[]`; child's `house` is A | A wins (child's claim is canonical). B's claim is visible-but-unverified. | +| Two parents both list child; child has no `house` declaration | Neither is trusted. UI shows both as competing unverified claims. | +| Last-validated > 180 days | Edge ages out. Treat as one-sided regardless of prior state. | + +The 180-day TTL is already implemented in the AAO crawler (`server/src/db/org-filters.ts` recursive walk in `findPayingOrgForDomain` and `resolveEffectiveMembership`). The proposed spec formalizes it. + +## Migration + +The current portfolio variant has live publishers. We give them a window: + +### 3.x (transitional) + +- Schema accepts both `brands[]` (legacy, with inline content) and `brand_refs[]` (new, pointer-only). +- If both present, `brand_refs[]` wins. +- If only `brands[]` is present, the validator emits a deprecation warning when entries contain anything beyond `{id, domain}`. Consumers treat inline content as a fallback when the child's canonical document doesn't exist yet. +- New publishers MUST use `brand_refs[]`. Existing publishers SHOULD migrate. + +### 2.0 (cutover — likely brand-protocol 2.0, not the same as AdCP major) + +- `brands[]` is invalid. `brand_refs[]` only. +- All brand identity attributes live exclusively at the canonical document for the brand they describe. + +### Migration helpers + +- AAO publishes a one-shot CLI: given a legacy portfolio brand.json, generate per-child canonical documents at the right URLs and update the parent to use `brand_refs[]`. +- AAO's hosted brand.json service auto-migrates members on first edit after the spec lands. + +## AAO API ergonomic note (non-normative) + +The protocol's pointer-only `brand_refs[]` means consumers must follow each pointer to fetch a child's data. This is correct for the protocol — it gives an unambiguous source-of-truth contract. + +For consumers who want a one-shot ergonomic view, AAO's API may offer a server-side merge: + +```http +GET /api/brands/nikeinc.com/family +``` + +Returns a denormalized tree of the parent + all (mutually-asserted) descendants in one response, with each branch's authoritative data merged in. This is a convenience layer over the protocol; **it does not change the protocol**. The merge happens server-side; clients pay one fetch instead of N. + +Other consumers (registry crawlers, AdCP agents, validators) follow the pointer-only contract and resolve lazily. + +## Implementation notes + +### Schema deltas (`core/brand-manifest.json`) + +- Add optional `house: BrandRef` field +- Add optional `house_attributes: object` field (free-form for now; spec specific keys per inheritance use case) +- Add optional `house_attributes_overrides: object` field +- Add optional `brand_refs: BrandRef[]` field +- Mark existing `brands` field as deprecated +- New shared `BrandRef` type: `{ id: string, domain: string }` (or refactor existing `core/brand-ref.json` to align) + +### Crawler resolution algorithm + +``` +resolve(domain): + doc = fetch(domain) + if doc has authoritative_location: + doc = fetch(authoritative_location) + + result = doc.identity_attributes + if doc has house: + parent = resolve(doc.house.domain) + if parent.brand_refs contains domain: + # Mutual assertion — extend trust + result.effective_house_attributes = merge( + parent.effective_house_attributes, + doc.house_attributes_overrides + ) + result.parent = parent (mutually_asserted: true) + else: + result.parent_claim = parent (mutually_asserted: false) # surface as metadata + + return result +``` + +Bound by max-depth (5 hops, matching existing `resolveEffectiveMembership`). Cycle protection via visited-domain set. + +### Validator behavior + +- Reject documents with both inline `brands[]` containing per-brand identity attributes AND `brand_refs[]` (ambiguous; force publisher to migrate). +- Warn on `brands[]` with inline identity attributes. +- Warn when a `house` claim is not mutually-asserted by the named parent (advisory only — single-sided claims are allowed by spec, just not trusted for inheritance). + +## Open questions + +These need spec-owner / discussion input: + +1. **Field name: `brand_refs` vs alternatives.** Names exactly what it is, lines up with `brand-ref.json`. Other candidates: `members`, `subsidiaries`, `house_brands`, `portfolio`. Vote: `brand_refs`. +2. **Where do `house_attributes` keys get standardized?** Loose object now; spec individual keys (privacy_policy_url, data_protection_roles, ...) over time. Vote: start permissive, formalize per use case. +3. **Should the spec mandate mutual-assertion for trust, or leave it to consumers?** Mandating it means every implementation has the same trust model. Leaving it lets consumers be more or less strict. Vote: mandate as the canonical trust primitive; spec text says consumers MAY apply additional checks (signing, brand-agent endorsement) but MUST NOT trust one-sided claims as the trust edge. +4. **Migration timeline.** 3.x → 2.0 (brand-protocol major) on what cadence? Suggest at least 12 months between deprecation and removal. +5. **Brand-protocol vs AdCP version coupling.** brand-protocol versioning is currently linked to AdCP's. Is this a brand-protocol 1.1 (additive) bump now and brand-protocol 2.0 cutover (breaking) later? Or a single AdCP 4.0 spec change? Vote: brand-protocol 1.1 + 2.0, decoupled cadence. + +## References + +- [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) — tracking issue +- [brand.json](/docs/brand-protocol/brand-json) — current normative spec +- [Building a brand agent](/docs/brand-protocol/building-a-brand-agent) — the separate brand-agent MCP service spec +- [#3378](https://github.com/adcontextprotocol/adcp/pull/3378) — brand-hierarchy auto-link (the trust model implemented in AAO crawler today) +- [#3450](https://github.com/adcontextprotocol/adcp/pull/3450) — team-page hierarchy display From 3684e9898ab6fdff2888314d14d163915755faeb Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 29 Apr 2026 16:34:20 -0400 Subject: [PATCH 02/13] docs(brand-protocol): drop Proposals subgroup from docs.json RFC stays at docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx as a working document, but isn't surfaced in the published docs nav. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs.json b/docs.json index b25373362e..2ff138757b 100644 --- a/docs.json +++ b/docs.json @@ -502,12 +502,6 @@ "docs/brand-protocol/tasks/acquire_rights", "docs/brand-protocol/tasks/update_rights" ] - }, - { - "group": "Proposals", - "pages": [ - "docs/brand-protocol/proposals/distributed-brand-json-rfc" - ] } ] }, From a3d655ec67fafb6cb441de58cc6f370096671a56 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 1 May 2026 12:12:50 -0400 Subject: [PATCH 03/13] =?UTF-8?q?docs(brand-protocol):=20RFC=20v2=20?= =?UTF-8?q?=E2=80=94=20hybrid=20model,=20flat=20hierarchy,=20parent=5Fhous?= =?UTF-8?q?e=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Pawel's review on PR #3533: 1. Lead with the operational pain point — Converse can't update its own logo in AdCP without editing Nike, Inc.'s file. Reframed Motivation. 2. Hybrid model — brands[] (inline, parent-owned) and brand_refs[] (pointer, child-owned) both first-class. Pull-based migration: brands[] is no longer deprecated. Sub-brands without their own domain stay inline. 3. Flat hierarchy — only the house declares ownership via brand_refs[] or brands[]. A brand cannot have its own brand_refs[]. Trust collapses to a single hop; no recursive walking. 4. Renamed child-side pointer from `house` to `parent_house` to avoid collision with the existing `house` declaration object on the root. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../proposals/distributed-brand-json-rfc.mdx | 278 +++++++++--------- 1 file changed, 134 insertions(+), 144 deletions(-) diff --git a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx index 8d8e12cfb4..0d91cbc235 100644 --- a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx +++ b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx @@ -1,6 +1,6 @@ --- title: "RFC — Distributed brand.json" -description: "Proposal to evolve brand.json from monolithic house portfolio to distributed per-brand canonical documents linked by mutual-assertion." +description: "Proposal to evolve brand.json so brands can publish their own canonical documents alongside parent-owned inline definitions, with a flat house-to-brands trust model." "og:title": "AdCP — Distributed brand.json RFC" --- @@ -15,175 +15,157 @@ description: "Proposal to evolve brand.json from monolithic house portfolio to d | Author | bokelley | | Status | Proposed | | Tracking | [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) | -| Target | brand-protocol 1.1 (additive); breaking parts at 2.0 | +| Target | brand-protocol 1.1 (additive) | | Affects | `core/brand-manifest.json` schema, `docs/brand-protocol/brand-json.mdx` | ## Summary -Evolve brand.json from a single house-portfolio document containing inline child brand definitions into a **collection of canonical per-brand documents** linked by mutual-assertion pointers. +Evolve brand.json so brands can publish their own canonical documents alongside the existing inline-children shape. The house remains the single authority for who is in the family; children pick the publishing model that fits. -In the current model, a house publishes one large `brand.json` with a `brands[]` array containing inline definitions for every owned brand. In the proposed model, each brand publishes its own canonical document at its own domain (or via `authoritative_location` indirection); the parent's document keeps a `brand_refs[]` array of pointers to those children, not inline copies. A child's document declares its parent via a `house` pointer. Trust between parent and child requires **mutual assertion** at each link. +A house's brand.json keeps `brands[]` for inline children (parent-owned data, the current shape) **and** adds `brand_refs[]` for pointer children whose canonical document lives elsewhere (child-owned data). A child's canonical document declares its parent via `parent_house`. Trust between the house and a pointer child requires **mutual assertion** — both sides must reciprocate. -This makes brand.json work for the long tail of independent and small publishers, supports multi-level hierarchies (e.g. `streetkix.com → converse.com → nikeinc.com`), keeps hosting decisions independent of the data model (static, CDN, or a brand-agent — any of those works), and gives consumers an unambiguous trust contract. +The hierarchy is **flat**: only the house declares ownership. There is no recursive parent chain. ## Motivation -### What's in the spec today +### The pain point -The current portfolio variant has the parent's brand.json own everything inline: +Today brand identity for a holdco lives in **one** brand.json owned by the parent. Every change to any brand requires an edit to the parent's file. -```json -{ - "house": { "domain": "nikeinc.com", "name": "Nike, Inc.", "architecture": "hybrid" }, - "brands": [ - { - "id": "nike", - "names": [{"en": "Nike"}], - "keller_type": "master", - "logos": [/* full inline logo data */], - "colors": {/* inline */}, - "tone": {/* inline */}, - "tagline": "Just Do It", - "visual_guidelines": {/* inline */}, - "properties": [{"type": "website", "identifier": "nike.com", "primary": true}] - }, - /* ...one giant block per owned brand... */ - ] -} -``` - -### The problems - -1. **Monolithic.** A holdco like Publicis with 100 subsidiaries publishes one file containing 100 inline brand definitions. Every update to any brand touches the whole document. Every consumer fetches the whole thing. Caching is all-or-nothing. - -2. **No leaf-side authority.** A brand that wants to publish its own brand.json (correct its parent's stale data, run independently of a parent registry, declare a new house relationship after an acquisition) can't — the spec's variant exclusivity gives the parent's portfolio sole authority for any brand it lists. +If Converse wants to update its logo in AdCP, someone has to edit Nike, Inc.'s brand.json. If Jordan launches a new tagline, same file. If a holdco like Publicis runs 100 subsidiaries, all 100 brand teams converge on the same monolithic document. This is a structural mismatch: brand teams own their identity, but the protocol forces a single ops choke point at the corporate parent. -3. **Doesn't model multi-level reality.** A 3rd-level brand like StreetKix (Nike → Converse → StreetKix) has no clean expression. Nike's portfolio listing every descendant flattens the structure; Converse can't manage StreetKix without forcing Nike to update. +The same shape blocks independent brands from publishing at all — a brand listed in someone else's portfolio has no protocol-level path to assert its own canonical data, even when domain control proves it could. -4. **Operational mismatch.** Holdcos with central brand teams want to manage subsidiary brand data centrally. Independent brands want self-publish. Sub-brands of a holdco often want to be managed by the parent's team but billed/owned at the brand level. The current model forces a single ops shape on everyone. +### Secondary problems -5. **Trust is implicit.** A brand that appears in someone else's portfolio is "owned" by that house with no verification primitive. No way for a consumer to distinguish "Nike asserts Converse is theirs and Converse agrees" from "anyone could publish a brand.json claiming Converse is theirs." +1. **Caching is all-or-nothing.** A 100-brand monolith re-fetches in full on any change. +2. **No leaf-side authority.** A brand listed in a parent's portfolio can't override stale parent-published data, even when it controls its own domain. +3. **Trust is implicit.** A brand appearing in someone else's portfolio is "owned" by that house with no verification. No way to distinguish "Nike asserts Converse is theirs and Converse agrees" from "anyone could publish a brand.json claiming Converse is theirs." -### What we want +### Constraints we want to preserve -A data model that: - -- Supports multi-level hierarchies recursively without flattening -- Lets each brand own its own canonical data -- Lets the host of a brand.json (the parent's brand-agent, AAO, the brand itself, a CDN) be an independent operational decision -- Defines an unambiguous trust primitive — when can a consumer trust a parent/child claim? -- Preserves the current portfolio-variant publishers through a transition window +- **House remains the authority for ownership.** The protocol shouldn't let arbitrary brands claim themselves into someone's portfolio. Only the house decides who is in the family. +- **No forced migration.** Existing portfolio publishers should keep working. Brands that don't want or need self-publish should stay simple. +- **Hosting is independent of data model.** Static, CDN, brand-agent service, AAO-hosted, self-hosted — all should work. ## Proposal -### Data model +### Hybrid: inline children **and** pointer children + +A house's canonical brand.json may carry both: -Three pieces. +- **`brands[]`** — inline child definitions. **Parent owns the data.** Same shape as today. Best for sub-brands without their own domain (Nike SB, an internal product line) or sub-brands the holdco wants to manage centrally. +- **`brand_refs[]`** — pointer entries. **Child owns the data** at its own canonical document. Best for sub-brands with their own domain that want self-publish authority (Converse, Jordan). -#### 1. Per-brand canonical documents +A given child appears in **exactly one** of the two. A house can mix freely. -Each brand publishes **one** canonical brand.json describing itself. Identity attributes (`name`, `logos`, `colors`, `fonts`, `tone`, `tagline`, `visual_guidelines`, `voice`, `avatar`) live ONLY here, never inlined elsewhere. +```json +// nikeinc.com — house, mixes inline and pointer children +{ + "house": { "domain": "nikeinc.com", "name": "Nike, Inc.", "architecture": "hybrid" }, + "house_attributes": { + "privacy_policy_url": "https://agreementservice.svs.nike.com/...", + "compliance_policies": ["no_under_13_targeting"] + }, + "brands": [ + { + "id": "nike-sb", + "names": [{"en": "Nike SB"}], + "keller_type": "sub_brand", + "logos": [/* parent-managed inline */] + } + ], + "brand_refs": [ + { "brand_id": "converse", "domain": "converse.com" }, + { "brand_id": "jordan", "domain": "jordan.com" } + ] +} +``` ```json -// converse.com — canonical +// converse.com — pointer child, owns its own data { "version": "1.0", "name": "Converse", "names": [{"en": "Converse"}], "keller_type": "sub_brand", - "house": { "domain": "nikeinc.com" }, + "parent_house": { "domain": "nikeinc.com" }, "logos": [...], "colors": {...}, "tone": {...}, - "tagline": "Sneaker for the streets", - "brand_refs": [ - { "id": "streetkix", "domain": "streetkix.com" } - ] + "tagline": "Sneaker for the streets" } ``` -#### 2. `house` pointer (immediate parent) +### Flat hierarchy — only the house adds brands + +Only the **house** has `brand_refs[]` / `brands[]`. A brand cannot list its own children. The hierarchy is one level deep. -A new optional field on brand-manifest. Declares the brand's **immediate** parent — one level up, not the ultimate root. Multi-level chains emerge from following pointers. Crawler walks recursively to find the master. +A multi-tier real-world arrangement (e.g. "StreetKix is run by Converse's team but legally owned by Nike, Inc.") collapses for the purpose of the data model: StreetKix's `parent_house` points to nikeinc.com directly, and StreetKix appears in nikeinc.com's `brand_refs[]`. Operational delegation between Converse and StreetKix is internal to Nike's organization — not a protocol concept. + +This dramatically simplifies trust and inheritance: one hop, single ancestor, no recursive walks. + +### `parent_house` (renamed from `house`) on a child + +Children declare their parent via a new `parent_house` field. The existing `house` field on the root keeps its current meaning — the corporate-entity declaration object `{domain, name, architecture}`. Two distinct fields, no overload. ```json -"house": { "domain": "nikeinc.com" } +"parent_house": { "domain": "nikeinc.com" } ``` -For the master brand of a house (no parent), the field is omitted. +A pure house (no parent — the root) omits `parent_house`. A child omits `house` (it's not declaring a corporate entity, it's a brand within one). -#### 3. `brand_refs[]` (replacing inline `brands[]`) - -A new array of pointer objects on the parent's canonical document. Each pointer is intentionally minimal — ID and the canonical domain where the child's full document lives: +### `brand_refs[]` shape ```json "brand_refs": [ - { "id": "nike", "domain": "nike.com" }, - { "id": "jordan", "domain": "jordan.com" }, - { "id": "converse", "domain": "converse.com" } + { "brand_id": "converse", "domain": "converse.com" } ] ``` -No preview fields. No inline content. The protocol contract is unambiguous: any consumer that wants any attribute about a brand fetches that brand's canonical document. There is exactly one source of truth per brand. +Pointer-only — `brand_id` and the canonical domain. No inline content, no preview fields. Any consumer that wants any attribute about a pointer child fetches that child's canonical document. -The legacy `brands[]` field stays valid through the migration window (see [Migration](#migration)) but is deprecated. +The legacy `brands[]` field stays valid alongside it. ### Hosting is independent -The data model says nothing about where bytes live. Implementations choose: +The data model says nothing about where bytes live. For pointer children, the canonical document is served at the pointed-to domain via the existing discovery contract (`domain.com/.well-known/brand.json`, with optional `authoritative_location` indirection). The hosting party is an implementation choice: | Pattern | How | | --- | --- | -| Self-hosted at the brand's domain | `domain.com/.well-known/brand.json` is the canonical document | +| Brand self-hosts | `domain.com/.well-known/brand.json` is the canonical document | | AAO-hosted | `domain.com/.well-known/brand.json` is a stub with `authoritative_location: "https://agenticadvertising.org/brands/domain.com/brand.json"` | -| Parent's brand-agent or static server | Same pattern — stub at the brand's own domain points at the parent's canonical URL | -| CDN-fronted | Either of the above with caching infrastructure in front | -| Mixed within a single family | Top-level uses one host; some sub-brands self-host; others use AAO | - -The crawler does not care. It follows the discovery contract: +| Parent's brand-agent or static server | Stub at brand's domain points at the parent's canonical URL | +| CDN-fronted | Either above with caching infrastructure in front | +| Mixed within a single house | Some children inline (`brands[]`), some pointer-self-hosted, some pointer-AAO-hosted, etc. | -1. Fetch `domain.com/.well-known/brand.json` -2. If it has `authoritative_location`, fetch that URL -3. Parse the canonical document there - -This is the same indirection mechanism already in spec; the proposal doesn't change it. It just makes explicit that **any party** can be the host. The brand-agent service spec ([building a brand agent](/docs/brand-protocol/building-a-brand-agent)) is a separate, complementary concept — a brand-agent can serve brand.json content via `get_brand_identity`, but trust still flows from the static document's authenticity, not the agent's identity. +The crawler doesn't care about hosting. It follows the discovery contract. The brand-agent service spec ([building a brand agent](/docs/brand-protocol/building-a-brand-agent)) is a separate, complementary concept — a brand-agent can serve brand.json content, but trust still flows from the static document's authenticity. ### Inheritance via `house_attributes` -For attributes that genuinely belong house-wide — privacy policy, compliance flags, corporate legal entity, jurisdictional data — the master (or any ancestor) may publish a `house_attributes` block that descendants inherit by default: +For attributes that belong house-wide — privacy policy, compliance flags, corporate legal entity, jurisdictional data — the house may publish a `house_attributes` block that all its children inherit: ```json -// nikeinc.com — master, declares house-wide attributes +// nikeinc.com — house declares house-wide attributes { - "name": "Nike, Inc.", + "house": { "domain": "nikeinc.com", "name": "Nike, Inc." }, "house_attributes": { "privacy_policy_url": "https://nikeinc.com/privacy", "data_protection_roles": [...], "compliance_policies": ["no_under_13_targeting"], "tax_entity": "Nike, Inc." - }, - "brand_refs": [ - { "id": "nike", "domain": "nike.com" }, - { "id": "converse", "domain": "converse.com" } - ] + } } ``` -A descendant resolves its **effective house attributes** by walking up the chain: +A pointer child resolves its **effective house attributes** in one step: fetch its `parent_house.domain`'s canonical document, take its `house_attributes`. No multi-level walking. -``` -streetkix.com (no house_attributes block) - → converse.com (no house_attributes block) - → nikeinc.com (defines house_attributes) - ↳ effective: { privacy_policy_url: ..., data_protection_roles: ..., ... } -``` - -A descendant may override specific inherited fields: +A child may override specific inherited fields: ```json -// streetkix.com — overrides one inherited compliance attribute +// jordan.com — pointer child, overrides one inherited compliance attribute { - "house": { "domain": "converse.com" }, + "parent_house": { "domain": "nikeinc.com" }, "house_attributes_overrides": { "compliance_policies": ["no_under_13_targeting", "us_only"] } @@ -192,18 +174,18 @@ A descendant may override specific inherited fields: The spec defines which top-level fields are **brand-identity** (per-brand only, never inherited): `name`, `names`, `logos`, `colors`, `fonts`, `tone`, `voice`, `tagline`, `visual_guidelines`, `avatar`. -Everything else is potentially inheritable; the master's `house_attributes` block is the canonical inheritance source. +Everything else on the house is potentially inheritable through `house_attributes`. ## Trust model -The hard part. Five layers, increasing in strength. +Single-hop. Five layers, increasing in strength. ### 1. Document authenticity (baseline) -A canonical brand.json document is authentic if and only if it is served via TLS by infrastructure the consumer can verify the brand controls. Two paths to authenticity: +A canonical brand.json document is authentic if and only if it is served via TLS by infrastructure the consumer can verify the brand controls. Two paths: - **Direct**: served at `domain.com/.well-known/brand.json` over TLS valid for `domain.com`. Standard web-PKI. -- **Indirect via `authoritative_location`**: stub at `domain.com/.well-known/brand.json` (proves domain control) points at a separate URL where the canonical document is served. The pointer is the trust anchor; the consumer trusts the pointed-to URL because the brand's domain control endorsed it. +- **Indirect via `authoritative_location`**: stub at `domain.com/.well-known/brand.json` (proves domain control) points at a separate URL where the canonical document is served. This layer establishes "I trust this is the brand's own document." Nothing more. @@ -211,55 +193,62 @@ This layer establishes "I trust this is the brand's own document." Nothing more. A brand's own canonical document is authoritative for **its own identity attributes**: `name`, `logos`, `colors`, `voice`, etc. Domain control = self-identity authority. No cross-checking needed. +For inline children in `brands[]`, the parent's document authenticity covers them — the parent is the authority for the child's data, by definition. + ### 3. One-sided relationship claims (metadata only — NOT trust) -A child's document says `house: { domain: "converse.com" }`. Converse's canonical document does NOT include the child's domain in `brand_refs[]`. +A pointer child says `parent_house: { domain: "nikeinc.com" }`. nikeinc.com's canonical document does NOT include the child's domain in `brand_refs[]`. This is **supportive metadata**, not a trust edge. Surface it in UIs as "claimed but unverified." Do not extend inheritance trust through it. Auto-provisioning, member-feature inheritance, billable seat inclusion: **NO**. ### 4. Mutual assertion (the trust edge) -Child's document says `house: { domain: "converse.com" }`. Converse's canonical document `brand_refs[]` includes `{ id: ..., domain: ".com" }`. Both verifiable at fetch time. **This is the link's trust edge.** Auto-provisioning, member-feature inheritance, inherited `house_attributes`: YES. +Pointer child's document says `parent_house: { domain: "nikeinc.com" }`. nikeinc.com's `brand_refs[]` includes `{ brand_id: ..., domain: ".com" }`. Both verifiable in one fetch each. **This is the trust edge.** Auto-provisioning, member-feature inheritance, inherited `house_attributes`: YES. -For multi-level chains (`streetkix → converse → nikeinc`), every link must be mutually-asserted. One non-mutual link breaks the chain at that point — trust extends only as far as the deepest mutually-asserted ancestor. +For inline children in `brands[]`, mutual assertion is implicit — the house authored the entry; no separate child claim exists. Parent's TLS = the assertion. ### 5. Cryptographic signing / brand-agent endorsement (future, optional) -Out of scope for v1. Future extensions: a parent's brand-agent could sign attestations about its hosted brands; verifiable credentials; etc. Not needed to ship the data model. +Out of scope for v1. Future extensions: a house's brand-agent could sign attestations about its hosted brands; verifiable credentials; etc. Not needed to ship the data model. ### Conflict resolution | Scenario | Resolution | | --- | --- | -| Child claims `house: A`, A's `brand_refs[]` does not include child | One-sided. Untrusted. UI: "claimed, unverified." | -| Child claims `house: A`, A's `brand_refs[]` includes child | Mutual. Trusted edge. | -| A's `brand_refs[]` includes child, but child's document has no `house` (or `house: null`) | One-sided in the other direction. A's claim is supportive metadata. Child is treated as having no parent. | -| Two parents (A and B) both have child in `brand_refs[]`; child's `house` is A | A wins (child's claim is canonical). B's claim is visible-but-unverified. | -| Two parents both list child; child has no `house` declaration | Neither is trusted. UI shows both as competing unverified claims. | +| Pointer child claims `parent_house: A`, A's `brand_refs[]` does not include child | One-sided. Untrusted. UI: "claimed, unverified." | +| Pointer child claims `parent_house: A`, A's `brand_refs[]` includes child | Mutual. Trusted edge. | +| A's `brand_refs[]` includes child, child's document has no `parent_house` | One-sided in the other direction. A's claim is supportive metadata. Child is treated as having no parent. | +| Two houses (A and B) both list child in `brand_refs[]`; child's `parent_house` is A | A wins. B's claim is visible-but-unverified. | +| Two houses both list child; child has no `parent_house` declaration | Neither is trusted. UI shows both as competing unverified claims. | +| A child appears in both `brands[]` and `brand_refs[]` of the same house | Validation error. Publisher must choose one. | | Last-validated > 180 days | Edge ages out. Treat as one-sided regardless of prior state. | -The 180-day TTL is already implemented in the AAO crawler (`server/src/db/org-filters.ts` recursive walk in `findPayingOrgForDomain` and `resolveEffectiveMembership`). The proposed spec formalizes it. +The 180-day TTL is already implemented in the AAO crawler (`server/src/db/org-filters.ts`). The proposed spec formalizes it. ## Migration -The current portfolio variant has live publishers. We give them a window: +Pull-based, not push-based. Existing publishers don't have to do anything until a child wants to self-publish. + +### 3.x (additive) -### 3.x (transitional) +- `brand_refs[]` is a new optional field alongside `brands[]`. +- `parent_house` is a new optional field on a child's canonical document. +- A house may use either, both, or neither. Existing portfolio publishers continue to work unchanged. +- No deprecation of `brands[]`. Inline remains a first-class option. -- Schema accepts both `brands[]` (legacy, with inline content) and `brand_refs[]` (new, pointer-only). -- If both present, `brand_refs[]` wins. -- If only `brands[]` is present, the validator emits a deprecation warning when entries contain anything beyond `{id, domain}`. Consumers treat inline content as a fallback when the child's canonical document doesn't exist yet. -- New publishers MUST use `brand_refs[]`. Existing publishers SHOULD migrate. +### A child's path to self-publish -### 2.0 (cutover — likely brand-protocol 2.0, not the same as AdCP major) +1. Child stands up a canonical document at its own domain (or `authoritative_location` target). +2. Child's document declares `parent_house: { domain: }`. +3. House removes the child's entry from `brands[]` and adds `{ brand_id, domain }` to `brand_refs[]`. +4. Crawler picks up the mutual assertion on next refresh (≤ 180-day TTL). -- `brands[]` is invalid. `brand_refs[]` only. -- All brand identity attributes live exclusively at the canonical document for the brand they describe. +No coordination required at the spec/version level — both shapes are valid simultaneously. ### Migration helpers -- AAO publishes a one-shot CLI: given a legacy portfolio brand.json, generate per-child canonical documents at the right URLs and update the parent to use `brand_refs[]`. -- AAO's hosted brand.json service auto-migrates members on first edit after the spec lands. +- AAO publishes a one-shot CLI: given a legacy portfolio brand.json, generate a per-child canonical document at the right URL and rewrite the parent's `brands[]` entry as a `brand_refs[]` pointer. +- AAO's hosted brand.json service offers a "promote child to self-publish" workflow that does the migration automatically. ## AAO API ergonomic note (non-normative) @@ -271,7 +260,7 @@ For consumers who want a one-shot ergonomic view, AAO's API may offer a server-s GET /api/brands/nikeinc.com/family ``` -Returns a denormalized tree of the parent + all (mutually-asserted) descendants in one response, with each branch's authoritative data merged in. This is a convenience layer over the protocol; **it does not change the protocol**. The merge happens server-side; clients pay one fetch instead of N. +Returns a denormalized tree of the house + all (mutually-asserted) brand children in one response, with each pointer child's authoritative data merged in. Inline children appear as-is. This is a convenience layer over the protocol; **it does not change the protocol**. The merge happens server-side; clients pay one fetch instead of N. Other consumers (registry crawlers, AdCP agents, validators) follow the pointer-only contract and resolve lazily. @@ -279,14 +268,15 @@ Other consumers (registry crawlers, AdCP agents, validators) follow the pointer- ### Schema deltas (`core/brand-manifest.json`) -- Add optional `house: BrandRef` field -- Add optional `house_attributes: object` field (free-form for now; spec specific keys per inheritance use case) -- Add optional `house_attributes_overrides: object` field -- Add optional `brand_refs: BrandRef[]` field -- Mark existing `brands` field as deprecated -- New shared `BrandRef` type: `{ id: string, domain: string }` (or refactor existing `core/brand-ref.json` to align) +- Add optional `brand_refs: BrandRef[]` field on the house document +- Add optional `parent_house: { domain: string }` field on a brand document +- Add optional `house_attributes: object` field on the house document (free-form for now; spec specific keys per inheritance use case) +- Add optional `house_attributes_overrides: object` field on a brand document +- Validation: a `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house +- Validation: only the house document may have `brand_refs[]` / `brands[]`. A brand document with `parent_house` set MUST NOT have `brand_refs[]` of its own. +- New shared `BrandRef` type: `{ brand_id: string, domain: string }` (or refactor existing `core/brand-ref.json` to align) -### Crawler resolution algorithm +### Crawler resolution algorithm (single hop) ``` resolve(domain): @@ -295,38 +285,38 @@ resolve(domain): doc = fetch(authoritative_location) result = doc.identity_attributes - if doc has house: - parent = resolve(doc.house.domain) - if parent.brand_refs contains domain: + if doc has parent_house: + house = fetch(doc.parent_house.domain) + if house.brand_refs contains domain: # Mutual assertion — extend trust result.effective_house_attributes = merge( - parent.effective_house_attributes, + house.house_attributes, doc.house_attributes_overrides ) - result.parent = parent (mutually_asserted: true) + result.house = house (mutually_asserted: true) else: - result.parent_claim = parent (mutually_asserted: false) # surface as metadata + result.house_claim = house (mutually_asserted: false) # surface as metadata return result ``` -Bound by max-depth (5 hops, matching existing `resolveEffectiveMembership`). Cycle protection via visited-domain set. +Single hop. No recursion, no max-depth, no cycle protection needed. ### Validator behavior -- Reject documents with both inline `brands[]` containing per-brand identity attributes AND `brand_refs[]` (ambiguous; force publisher to migrate). -- Warn on `brands[]` with inline identity attributes. -- Warn when a `house` claim is not mutually-asserted by the named parent (advisory only — single-sided claims are allowed by spec, just not trusted for inheritance). +- Reject a brand document that has both `parent_house` set AND `brand_refs[]` (a brand can't also be a house). +- Reject a brand_id appearing in both `brands[]` and `brand_refs[]` of the same house. +- Warn when a `parent_house` claim is not mutually-asserted by the named house (advisory only — single-sided claims are allowed by spec, just not trusted for inheritance). ## Open questions These need spec-owner / discussion input: -1. **Field name: `brand_refs` vs alternatives.** Names exactly what it is, lines up with `brand-ref.json`. Other candidates: `members`, `subsidiaries`, `house_brands`, `portfolio`. Vote: `brand_refs`. -2. **Where do `house_attributes` keys get standardized?** Loose object now; spec individual keys (privacy_policy_url, data_protection_roles, ...) over time. Vote: start permissive, formalize per use case. -3. **Should the spec mandate mutual-assertion for trust, or leave it to consumers?** Mandating it means every implementation has the same trust model. Leaving it lets consumers be more or less strict. Vote: mandate as the canonical trust primitive; spec text says consumers MAY apply additional checks (signing, brand-agent endorsement) but MUST NOT trust one-sided claims as the trust edge. -4. **Migration timeline.** 3.x → 2.0 (brand-protocol major) on what cadence? Suggest at least 12 months between deprecation and removal. -5. **Brand-protocol vs AdCP version coupling.** brand-protocol versioning is currently linked to AdCP's. Is this a brand-protocol 1.1 (additive) bump now and brand-protocol 2.0 cutover (breaking) later? Or a single AdCP 4.0 spec change? Vote: brand-protocol 1.1 + 2.0, decoupled cadence. +1. **Field name: `parent_house` vs alternatives.** Reuses existing "house" terminology, makes direction unambiguous, avoids the field-collision Pawel flagged. Other candidates: `parent`, `house_ref`, `parent_brand_domain`. Vote: `parent_house`. +2. **`brand_refs` vs alternatives.** Names exactly what it is, lines up with `brand-ref.json`. Other candidates: `pointer_brands`, `linked_brands`, `external_brands`. Vote: `brand_refs`. +3. **Where do `house_attributes` keys get standardized?** Loose object now; spec individual keys (privacy_policy_url, data_protection_roles, ...) over time. Vote: start permissive, formalize per use case. +4. **Should the spec mandate mutual-assertion for trust, or leave it to consumers?** Mandating it means every implementation has the same trust model. Vote: mandate as the canonical trust primitive; spec text says consumers MAY apply additional checks (signing, brand-agent endorsement) but MUST NOT trust one-sided claims as the trust edge. +5. **Should we explicitly disallow recursion?** Current proposal: only houses declare ownership; brands cannot have `brand_refs[]`. Alternative: leave it open for a future v2. Vote: explicitly disallow at v1, revisit if a real use case emerges. ## References From 45ccf758452d7ab6190da8332edb30c91bf87212 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 1 May 2026 12:45:51 -0400 Subject: [PATCH 04/13] docs(brand-protocol): use template-syntax placeholder in RFC AAO-hosted example check:owned-links treated the example URL as a real link and failed CI when the path 404'd. Wrap the brand-domain segment as \${domain} so the linter recognizes it as a placeholder (it skips URLs containing \${). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx index 0d91cbc235..4b1bec6d52 100644 --- a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx +++ b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx @@ -134,7 +134,7 @@ The data model says nothing about where bytes live. For pointer children, the ca | Pattern | How | | --- | --- | | Brand self-hosts | `domain.com/.well-known/brand.json` is the canonical document | -| AAO-hosted | `domain.com/.well-known/brand.json` is a stub with `authoritative_location: "https://agenticadvertising.org/brands/domain.com/brand.json"` | +| AAO-hosted | `domain.com/.well-known/brand.json` is a stub with `authoritative_location: "https://agenticadvertising.org/brands/${domain}/brand.json"` | | Parent's brand-agent or static server | Stub at brand's domain points at the parent's canonical URL | | CDN-fronted | Either above with caching infrastructure in front | | Mixed within a single house | Some children inline (`brands[]`), some pointer-self-hosted, some pointer-AAO-hosted, etc. | From b415ee868293c7206709880287dda11a514675a6 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 19:51:21 -0400 Subject: [PATCH 05/13] =?UTF-8?q?docs(brand-protocol):=20RFC=20v3=20?= =?UTF-8?q?=E2=80=94=20drop=20house=5Fattributes;=20rename=20to=20house=5F?= =?UTF-8?q?domain;=20add=20managed=5Fby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the second round of expert review on PR #3533. Substantive changes: - Drop house_attributes / house_attributes_overrides / house_attributes_locked entirely. Inheritance/override semantics turned out to be muddy: if a brand could weaken a house policy, it wasn't really a house policy. House-level fields (data_subject_contestation, trademarks, authorized_operators) are already on the house schema; consumers walk house_domain to read them. Brand-level additions are just brand-level fields. - Rename child-side pointer from parent_house ({domain}) to house_domain (string). Reuses the existing #/definitions/domain pattern. Drops the proposed core/house-ref.json file. Matches the existing House Redirect's string-domain convention. - Add managed_by (string, optional) on brand_refs[] entries. House-declared delegation for grouping/discovery. Non-trust-bearing. Captures WPP/Publicis reality without reintroducing recursive trust. - Make house_domain optional on the Brand Canonical Document so standalone brands (Patagonia, Liquid Death) have a valid shape. Acquisition adds house_domain later; no migration required. - Add an explicit M&A section: existing redirect variants (House Redirect, Authoritative Location Redirect) handle reorganizations; resolution follows redirects through house_domain. - Add a clear field-resolution table: where each consumer-side question is answered. No inheritance/override, just per-brand vs per-house ownership. Trust model still single-hop, mutual-assertion via house_domain ↔ brand_refs[]. Migration still pull-based. brands[] still first-class (not deprecated). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../proposals/distributed-brand-json-rfc.mdx | 232 ++++++++++-------- 1 file changed, 135 insertions(+), 97 deletions(-) diff --git a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx index 4b1bec6d52..042d89ebcc 100644 --- a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx +++ b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx @@ -1,11 +1,11 @@ --- title: "RFC — Distributed brand.json" -description: "Proposal to evolve brand.json so brands can publish their own canonical documents alongside parent-owned inline definitions, with a flat house-to-brands trust model." +description: "Proposal to evolve brand.json so brands can publish their own canonical documents alongside parent-owned inline definitions, with a flat house-to-brands trust model and house-declared management delegation." "og:title": "AdCP — Distributed brand.json RFC" --- -**RFC — discussion in progress.** This is a proposal under discussion in [issue #3409](https://github.com/adcontextprotocol/adcp/issues/3409). Not yet a ratified part of the brand protocol. The current normative spec lives at [brand.json](/docs/brand-protocol/brand-json). +**RFC — discussion in progress.** This is a proposal under discussion in [issue #3409](https://github.com/adcontextprotocol/adcp/issues/3409) and PR [#3533](https://github.com/adcontextprotocol/adcp/pull/3533). Schema implementation cut for review at PR [#3764](https://github.com/adcontextprotocol/adcp/pull/3764). Not yet ratified. The current normative spec lives at [brand.json](/docs/brand-protocol/brand-json). ## Status @@ -16,15 +16,17 @@ description: "Proposal to evolve brand.json so brands can publish their own cano | Status | Proposed | | Tracking | [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) | | Target | brand-protocol 1.1 (additive) | -| Affects | `core/brand-manifest.json` schema, `docs/brand-protocol/brand-json.mdx` | +| Affects | `static/schemas/source/brand.json`, `docs/brand-protocol/brand-json.mdx` | ## Summary Evolve brand.json so brands can publish their own canonical documents alongside the existing inline-children shape. The house remains the single authority for who is in the family; children pick the publishing model that fits. -A house's brand.json keeps `brands[]` for inline children (parent-owned data, the current shape) **and** adds `brand_refs[]` for pointer children whose canonical document lives elsewhere (child-owned data). A child's canonical document declares its parent via `parent_house`. Trust between the house and a pointer child requires **mutual assertion** — both sides must reciprocate. +A house's brand.json keeps `brands[]` for inline children (parent-owned data, the current shape) **and** adds `brand_refs[]` for pointer children whose canonical document lives elsewhere (child-owned data). A child's canonical document declares its house via `house_domain`. Trust between the house and a pointer child requires **mutual assertion** — both sides must reciprocate. -The hierarchy is **flat**: only the house declares ownership. There is no recursive parent chain. +The hierarchy is **flat**: only the house declares ownership. There is no recursive parent chain. Operational delegation (e.g., a holdco letting an agency manage a brand) is expressed via `managed_by` on the house's `brand_refs[]` entry — house-declared, non-trust-bearing, for grouping and discovery only. + +Acquisitions and reorganizations are handled by the existing redirect variants (House Redirect, Authoritative Location Redirect) — no new primitive needed. ## Motivation @@ -32,7 +34,7 @@ The hierarchy is **flat**: only the house declares ownership. There is no recurs Today brand identity for a holdco lives in **one** brand.json owned by the parent. Every change to any brand requires an edit to the parent's file. -If Converse wants to update its logo in AdCP, someone has to edit Nike, Inc.'s brand.json. If Jordan launches a new tagline, same file. If a holdco like Publicis runs 100 subsidiaries, all 100 brand teams converge on the same monolithic document. This is a structural mismatch: brand teams own their identity, but the protocol forces a single ops choke point at the corporate parent. +If Converse wants to update its logo in AdCP, someone has to edit Nike, Inc.'s brand.json. If Jordan launches a new tagline, same file. If a holdco runs 100 subsidiary brands, all 100 brand teams converge on the same monolithic document. This is a structural mismatch: brand teams own their identity, but the protocol forces a single ops choke point at the corporate parent. The same shape blocks independent brands from publishing at all — a brand listed in someone else's portfolio has no protocol-level path to assert its own canonical data, even when domain control proves it could. @@ -40,13 +42,15 @@ The same shape blocks independent brands from publishing at all — a brand list 1. **Caching is all-or-nothing.** A 100-brand monolith re-fetches in full on any change. 2. **No leaf-side authority.** A brand listed in a parent's portfolio can't override stale parent-published data, even when it controls its own domain. -3. **Trust is implicit.** A brand appearing in someone else's portfolio is "owned" by that house with no verification. No way to distinguish "Nike asserts Converse is theirs and Converse agrees" from "anyone could publish a brand.json claiming Converse is theirs." +3. **Trust is implicit.** A brand appearing in someone else's portfolio is "owned" by that house with no verification primitive. No way to distinguish "Nike asserts Converse is theirs and Converse agrees" from "anyone could publish a brand.json claiming Converse is theirs." +4. **No expression for delegated management.** Holdcos like WPP delegate brand management to agency networks (BBH, Ogilvy). The protocol has no place for "WPP owns this brand, BBH manages it day-to-day." ### Constraints we want to preserve - **House remains the authority for ownership.** The protocol shouldn't let arbitrary brands claim themselves into someone's portfolio. Only the house decides who is in the family. - **No forced migration.** Existing portfolio publishers should keep working. Brands that don't want or need self-publish should stay simple. - **Hosting is independent of data model.** Static, CDN, brand-agent service, AAO-hosted, self-hosted — all should work. +- **No new primitives unless necessary.** The schema already has redirects, references, and a four-variant top-level shape. Reuse what's there. ## Proposal @@ -63,21 +67,17 @@ A given child appears in **exactly one** of the two. A house can mix freely. // nikeinc.com — house, mixes inline and pointer children { "house": { "domain": "nikeinc.com", "name": "Nike, Inc.", "architecture": "hybrid" }, - "house_attributes": { - "privacy_policy_url": "https://agreementservice.svs.nike.com/...", - "compliance_policies": ["no_under_13_targeting"] - }, "brands": [ { - "id": "nike-sb", - "names": [{"en": "Nike SB"}], + "id": "nike_sb", + "names": [{"en_US": "Nike SB"}], "keller_type": "sub_brand", "logos": [/* parent-managed inline */] } ], "brand_refs": [ - { "brand_id": "converse", "domain": "converse.com" }, - { "brand_id": "jordan", "domain": "jordan.com" } + { "domain": "converse.com", "brand_id": "converse" }, + { "domain": "jordan.com", "brand_id": "jordan" } ] } ``` @@ -86,10 +86,11 @@ A given child appears in **exactly one** of the two. A house can mix freely. // converse.com — pointer child, owns its own data { "version": "1.0", + "id": "converse", "name": "Converse", - "names": [{"en": "Converse"}], + "names": [{"en_US": "Converse"}], "keller_type": "sub_brand", - "parent_house": { "domain": "nikeinc.com" }, + "house_domain": "nikeinc.com", "logos": [...], "colors": {...}, "tone": {...}, @@ -101,31 +102,69 @@ A given child appears in **exactly one** of the two. A house can mix freely. Only the **house** has `brand_refs[]` / `brands[]`. A brand cannot list its own children. The hierarchy is one level deep. -A multi-tier real-world arrangement (e.g. "StreetKix is run by Converse's team but legally owned by Nike, Inc.") collapses for the purpose of the data model: StreetKix's `parent_house` points to nikeinc.com directly, and StreetKix appears in nikeinc.com's `brand_refs[]`. Operational delegation between Converse and StreetKix is internal to Nike's organization — not a protocol concept. +A multi-tier real-world arrangement (e.g. "StreetKix is run by Converse's team but legally owned by Nike, Inc.") collapses for the data model: StreetKix's `house_domain` is `nikeinc.com` directly, and StreetKix appears in nikeinc.com's `brand_refs[]`. Operational delegation between Converse and StreetKix is expressed via `managed_by` on Nike's `brand_refs[]` entry, not as a separate hierarchical layer (see next section). -This dramatically simplifies trust and inheritance: one hop, single ancestor, no recursive walks. +This dramatically simplifies trust: one hop, single authority, no recursive walks. -### `parent_house` (renamed from `house`) on a child +### `house_domain` on a child (string) -Children declare their parent via a new `parent_house` field. The existing `house` field on the root keeps its current meaning — the corporate-entity declaration object `{domain, name, architecture}`. Two distinct fields, no overload. +Children declare their parent house via a `house_domain` field — a plain string, the domain of the house's brand.json. Reuses the same domain pattern as the existing House Redirect variant (which already uses `house: ""` as a string). ```json -"parent_house": { "domain": "nikeinc.com" } +"house_domain": "nikeinc.com" ``` -A pure house (no parent — the root) omits `parent_house`. A child omits `house` (it's not declaring a corporate entity, it's a brand within one). +A standalone brand (no house — Patagonia, Liquid Death) omits `house_domain`. The brand canonical document is still valid; it just declares no parent relationship. If the brand is later acquired, it adds `house_domain` and the new house adds the brand to `brand_refs[]`. No new variant needed. ### `brand_refs[]` shape ```json "brand_refs": [ - { "brand_id": "converse", "domain": "converse.com" } + { + "domain": "converse.com", + "brand_id": "converse", + "managed_by": "ogilvy.com" + } ] ``` -Pointer-only — `brand_id` and the canonical domain. No inline content, no preview fields. Any consumer that wants any attribute about a pointer child fetches that child's canonical document. +| Field | Required | Meaning | +| --- | --- | --- | +| `domain` | yes | Where the child's canonical brand.json lives | +| `brand_id` | optional | Stable identifier for this brand within the house's portfolio | +| `managed_by` | optional | Domain of the entity that operationally manages this brand. **House-declared, non-trust-bearing.** UIs and discovery tools group by `managed_by`; trust still flows child → house only. | + +### Delegation via `managed_by` -The legacy `brands[]` field stays valid alongside it. +Real holdcos delegate brand management to agency networks. WPP plc owns brands but Ogilvy or BBH actually runs them day-to-day. + +`managed_by` on a `brand_refs[]` entry captures this **as a unilateral declaration by the owning house**. The leaf brand doesn't need to know about the manager. The manager doesn't need to publish anything to confirm. UIs render BBH Sport under BBH for agency views; trust validation walks BBH Sport → WPP only. + +```json +// wpp.com +{ + "house": { "domain": "wpp.com", "name": "WPP plc" }, + "brand_refs": [ + { "domain": "bbh-sport.com", "brand_id": "bbh_sport", + "managed_by": "bbh.com" }, + { "domain": "ogilvy-toyota.com", "brand_id": "ogilvy_toyota", + "managed_by": "ogilvy.com" }, + { "domain": "wpp-direct.com", "brand_id": "wpp_direct" } + ] +} +``` + +```json +// bbh-sport.com — leaf doesn't reference BBH, just WPP +{ + "id": "bbh_sport", + "names": [{"en_US": "BBH Sport"}], + "keller_type": "endorsed", + "house_domain": "wpp.com" +} +``` + +If WPP changes the manager (BBH → directly managed → Ogilvy), only WPP's brand_refs[] entry updates. The leaf doesn't churn. If BBH disagrees with the management claim, that's a legal/business matter, not a protocol concern. ### Hosting is independent @@ -141,40 +180,21 @@ The data model says nothing about where bytes live. For pointer children, the ca The crawler doesn't care about hosting. It follows the discovery contract. The brand-agent service spec ([building a brand agent](/docs/brand-protocol/building-a-brand-agent)) is a separate, complementary concept — a brand-agent can serve brand.json content, but trust still flows from the static document's authenticity. -### Inheritance via `house_attributes` - -For attributes that belong house-wide — privacy policy, compliance flags, corporate legal entity, jurisdictional data — the house may publish a `house_attributes` block that all its children inherit: +### Resolution: where does a consumer read each field? -```json -// nikeinc.com — house declares house-wide attributes -{ - "house": { "domain": "nikeinc.com", "name": "Nike, Inc." }, - "house_attributes": { - "privacy_policy_url": "https://nikeinc.com/privacy", - "data_protection_roles": [...], - "compliance_policies": ["no_under_13_targeting"], - "tax_entity": "Nike, Inc." - } -} -``` +The spec already separates per-brand fields from house-level fields. The new shape doesn't introduce inheritance/override semantics. Each consumer-side question has a single answer: -A pointer child resolves its **effective house attributes** in one step: fetch its `parent_house.domain`'s canonical document, take its `house_attributes`. No multi-level walking. - -A child may override specific inherited fields: - -```json -// jordan.com — pointer child, overrides one inherited compliance attribute -{ - "parent_house": { "domain": "nikeinc.com" }, - "house_attributes_overrides": { - "compliance_policies": ["no_under_13_targeting", "us_only"] - } -} -``` +| Question | Resolution | +| --- | --- | +| Brand identity (logos, colors, tone, tagline, voice) | Read from brand's own canonical document. For inline children, read from the parent's `brands[]` entry. | +| Brand contact, properties, industries | Read from brand's own canonical document. | +| `data_subject_contestation` | Brand-level if present; otherwise walk `house_domain` → `house.data_subject_contestation`. (Existing resolution rule, unchanged.) | +| Trademarks, authorized_operators, corporate contact | Read from the house's brand.json. | +| Mutual-assertion verification | Fetch both brand and house, compare `house_domain` ↔ `brand_refs[]`. | -The spec defines which top-level fields are **brand-identity** (per-brand only, never inherited): `name`, `names`, `logos`, `colors`, `fonts`, `tone`, `voice`, `tagline`, `visual_guidelines`, `avatar`. +There is no `house_attributes` block, no `house_attributes_overrides`, no `house_attributes_locked`. Houses publish their corporate-level fields in the existing house schema; brands publish their brand-level fields in their own canonical document; consumers walk `house_domain` to read corporate fields when needed. -Everything else on the house is potentially inheritable through `house_attributes`. +If a brand wants stricter compliance than its house (e.g., extra audience exclusions), it publishes those constraints at the brand level. House and brand constraints both apply; consumers respect the union. ## Trust model @@ -197,13 +217,13 @@ For inline children in `brands[]`, the parent's document authenticity covers the ### 3. One-sided relationship claims (metadata only — NOT trust) -A pointer child says `parent_house: { domain: "nikeinc.com" }`. nikeinc.com's canonical document does NOT include the child's domain in `brand_refs[]`. +A pointer child says `house_domain: "nikeinc.com"`. nikeinc.com's canonical document does NOT include the child's domain in `brand_refs[]`. -This is **supportive metadata**, not a trust edge. Surface it in UIs as "claimed but unverified." Do not extend inheritance trust through it. Auto-provisioning, member-feature inheritance, billable seat inclusion: **NO**. +This is **supportive metadata**, not a trust edge. Surface it in UIs as "claimed but unverified." Do not extend governance trust through it. Auto-provisioning, member-feature inheritance, billable seat inclusion: **NO**. ### 4. Mutual assertion (the trust edge) -Pointer child's document says `parent_house: { domain: "nikeinc.com" }`. nikeinc.com's `brand_refs[]` includes `{ brand_id: ..., domain: ".com" }`. Both verifiable in one fetch each. **This is the trust edge.** Auto-provisioning, member-feature inheritance, inherited `house_attributes`: YES. +Pointer child's document says `house_domain: "nikeinc.com"`. nikeinc.com's `brand_refs[]` includes `{ domain: ".com", brand_id: ... }`. Both verifiable in two fetches. **This is the trust edge.** Auto-provisioning, member-feature inheritance, governance fallback: YES. For inline children in `brands[]`, mutual assertion is implicit — the house authored the entry; no separate child claim exists. Parent's TLS = the assertion. @@ -215,16 +235,39 @@ Out of scope for v1. Future extensions: a house's brand-agent could sign attesta | Scenario | Resolution | | --- | --- | -| Pointer child claims `parent_house: A`, A's `brand_refs[]` does not include child | One-sided. Untrusted. UI: "claimed, unverified." | -| Pointer child claims `parent_house: A`, A's `brand_refs[]` includes child | Mutual. Trusted edge. | -| A's `brand_refs[]` includes child, child's document has no `parent_house` | One-sided in the other direction. A's claim is supportive metadata. Child is treated as having no parent. | -| Two houses (A and B) both list child in `brand_refs[]`; child's `parent_house` is A | A wins. B's claim is visible-but-unverified. | -| Two houses both list child; child has no `parent_house` declaration | Neither is trusted. UI shows both as competing unverified claims. | +| Pointer child claims `house_domain: A`, A's `brand_refs[]` does not include child | One-sided. Untrusted. UI: "claimed, unverified." | +| Pointer child claims `house_domain: A`, A's `brand_refs[]` includes child | Mutual. Trusted edge. | +| A's `brand_refs[]` includes child, child's document has no `house_domain` | One-sided in the other direction. A's claim is supportive metadata. Child is treated as having no house. | +| Two houses (A and B) both list child in `brand_refs[]`; child's `house_domain` is A | A wins. B's claim is visible-but-unverified. | +| Two houses both list child; child has no `house_domain` declaration | Neither is trusted. UI shows both as competing unverified claims. | | A child appears in both `brands[]` and `brand_refs[]` of the same house | Validation error. Publisher must choose one. | | Last-validated > 180 days | Edge ages out. Treat as one-sided regardless of prior state. | The 180-day TTL is already implemented in the AAO crawler (`server/src/db/org-filters.ts`). The proposed spec formalizes it. +`managed_by` is **not part of the trust model**. It's a unilateral declaration by the owning house, used for grouping and discovery. A misuse ("WPP says BBH manages 100 brands but BBH never agreed") doesn't compromise trust because `managed_by` carries no governance weight. + +## Acquisitions and reorganizations + +Existing brand.json variants handle M&A natively. No new primitive needed. + +**Pre-deal:** + +- `dentsu.com/.well-known/brand.json` is a House Portfolio. +- Dentsu's brands' canonical docs say `house_domain: "dentsu.com"`. + +**Deal closes:** + +- Dentsu's `brand.json` is replaced with a House Redirect → `{ "house": "wpp.com" }` (or an Authoritative Location Redirect to WPP's hosted file). +- WPP's brand.json adds the acquired brands to `brand_refs[]` with `managed_by: "dentsu.com"` (ops continuity). + +**Post-deal:** + +- Leaves still pointing at `house_domain: "dentsu.com"` resolve through the redirect to WPP's portfolio. Mutual-assertion holds via the redirect chain. +- Leaves don't have to update urgently. Over time they migrate to `house_domain: "wpp.com"` for clarity, but it's not a trust requirement. + +The existing redirect machinery does the work. The spec just needs to call out, in the resolution algorithm, that **`house_domain` resolution follows the same discovery contract as any brand.json fetch — including redirect variants.** + ## Migration Pull-based, not push-based. Existing publishers don't have to do anything until a child wants to self-publish. @@ -232,15 +275,16 @@ Pull-based, not push-based. Existing publishers don't have to do anything until ### 3.x (additive) - `brand_refs[]` is a new optional field alongside `brands[]`. -- `parent_house` is a new optional field on a child's canonical document. -- A house may use either, both, or neither. Existing portfolio publishers continue to work unchanged. +- `house_domain` is a new optional field on a brand canonical document. +- `managed_by` is a new optional field on `brand_refs[]` entries. +- A house may use any combination. Existing portfolio publishers continue to work unchanged. - No deprecation of `brands[]`. Inline remains a first-class option. ### A child's path to self-publish 1. Child stands up a canonical document at its own domain (or `authoritative_location` target). -2. Child's document declares `parent_house: { domain: }`. -3. House removes the child's entry from `brands[]` and adds `{ brand_id, domain }` to `brand_refs[]`. +2. Child's document declares `house_domain: ""`. +3. House removes the child's entry from `brands[]` and adds `{ domain, brand_id, managed_by? }` to `brand_refs[]`. 4. Crawler picks up the mutual assertion on next refresh (≤ 180-day TTL). No coordination required at the spec/version level — both shapes are valid simultaneously. @@ -260,43 +304,35 @@ For consumers who want a one-shot ergonomic view, AAO's API may offer a server-s GET /api/brands/nikeinc.com/family ``` -Returns a denormalized tree of the house + all (mutually-asserted) brand children in one response, with each pointer child's authoritative data merged in. Inline children appear as-is. This is a convenience layer over the protocol; **it does not change the protocol**. The merge happens server-side; clients pay one fetch instead of N. +Returns a denormalized tree of the house + all (mutually-asserted) brand children in one response, with each pointer child's authoritative data merged in. Inline children appear as-is. This is a convenience layer over the protocol; **it does not change the protocol**. Other consumers (registry crawlers, AdCP agents, validators) follow the pointer-only contract and resolve lazily. ## Implementation notes -### Schema deltas (`core/brand-manifest.json`) +### Schema deltas (`static/schemas/source/brand.json`) + +- House Portfolio variant (existing) gains optional `brand_refs[]` field. Each entry is `{ domain, brand_id?, managed_by? }`. Required field on `required`: widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` / `brand_refs[]`. +- New top-level variant: **Brand Canonical Document**. Composes the existing brand definition via `allOf` plus optional `house_domain` (string), `$schema`, `version`, `last_updated`. Excludes top-level `house`, `brands`, `brand_refs`, `authorized_operators` to disambiguate from House Portfolio. +- No new schema files. `house_domain` and `managed_by` reuse the existing `domain` definition (string with the standard pattern). +- No `house_attributes` / `house_attributes_overrides` / `house_attributes_locked` blocks. Houses publish corporate-level fields in the existing house schema (`data_subject_contestation`, `trademarks`, `contact`, `authorized_operators`). + +### Cross-array invariant (validator + lint) -- Add optional `brand_refs: BrandRef[]` field on the house document -- Add optional `parent_house: { domain: string }` field on a brand document -- Add optional `house_attributes: object` field on the house document (free-form for now; spec specific keys per inheritance use case) -- Add optional `house_attributes_overrides: object` field on a brand document -- Validation: a `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house -- Validation: only the house document may have `brand_refs[]` / `brands[]`. A brand document with `parent_house` set MUST NOT have `brand_refs[]` of its own. -- New shared `BrandRef` type: `{ brand_id: string, domain: string }` (or refactor existing `core/brand-ref.json` to align) +A `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. JSON Schema cannot easily express this; the spec mandates it and validators/lint enforce it. ### Crawler resolution algorithm (single hop) ``` resolve(domain): - doc = fetch(domain) - if doc has authoritative_location: - doc = fetch(authoritative_location) - + doc = fetch(domain).follow_redirects() # follows authoritative_location and House Redirect result = doc.identity_attributes - if doc has parent_house: - house = fetch(doc.parent_house.domain) + if doc has house_domain: + house = fetch(doc.house_domain).follow_redirects() if house.brand_refs contains domain: - # Mutual assertion — extend trust - result.effective_house_attributes = merge( - house.house_attributes, - doc.house_attributes_overrides - ) result.house = house (mutually_asserted: true) else: result.house_claim = house (mutually_asserted: false) # surface as metadata - return result ``` @@ -304,24 +340,26 @@ Single hop. No recursion, no max-depth, no cycle protection needed. ### Validator behavior -- Reject a brand document that has both `parent_house` set AND `brand_refs[]` (a brand can't also be a house). +- Reject a brand canonical document that has top-level `house`, `brands`, `brand_refs`, or `authorized_operators` — those are house-only fields. - Reject a brand_id appearing in both `brands[]` and `brand_refs[]` of the same house. -- Warn when a `parent_house` claim is not mutually-asserted by the named house (advisory only — single-sided claims are allowed by spec, just not trusted for inheritance). +- Warn when a `house_domain` claim is not mutually-asserted by the named house (advisory only — single-sided claims are allowed by spec, just not trusted). ## Open questions These need spec-owner / discussion input: -1. **Field name: `parent_house` vs alternatives.** Reuses existing "house" terminology, makes direction unambiguous, avoids the field-collision Pawel flagged. Other candidates: `parent`, `house_ref`, `parent_brand_domain`. Vote: `parent_house`. -2. **`brand_refs` vs alternatives.** Names exactly what it is, lines up with `brand-ref.json`. Other candidates: `pointer_brands`, `linked_brands`, `external_brands`. Vote: `brand_refs`. -3. **Where do `house_attributes` keys get standardized?** Loose object now; spec individual keys (privacy_policy_url, data_protection_roles, ...) over time. Vote: start permissive, formalize per use case. -4. **Should the spec mandate mutual-assertion for trust, or leave it to consumers?** Mandating it means every implementation has the same trust model. Vote: mandate as the canonical trust primitive; spec text says consumers MAY apply additional checks (signing, brand-agent endorsement) but MUST NOT trust one-sided claims as the trust edge. -5. **Should we explicitly disallow recursion?** Current proposal: only houses declare ownership; brands cannot have `brand_refs[]`. Alternative: leave it open for a future v2. Vote: explicitly disallow at v1, revisit if a real use case emerges. +1. **Should `house_domain` on a brand canonical document be required or optional?** Optional in this proposal — supports standalone brands (Patagonia) without requiring a degenerate "house of one." Vote: keep optional. +2. **Where do the inline `brand_refs[]` entry fields belong long-term?** Currently inline in brand.json. If reused elsewhere, refactor into a shared `core/` schema. Vote: inline for v1, refactor only if a second consumer emerges. +3. **Should the spec mandate mutual-assertion for trust, or leave it to consumers?** Mandating it means every implementation has the same trust model. Vote: mandate as the canonical trust primitive; spec text says consumers MAY apply additional checks (signing, brand-agent endorsement) but MUST NOT trust one-sided claims as the trust edge. +4. **Migration timeline.** Both shapes coexist indefinitely; no forced cutover. If a deprecation is ever appropriate, that's a future RFC. +5. **`search_brands` trust state surfacing.** The crawler computes `mutually_asserted: true|false` per brand. PR [#3486](https://github.com/adcontextprotocol/adcp/pull/3486) (search_brands discovery verb) doesn't currently surface this in response stubs. Follow-up: extend `SearchBrandResult` to carry the trust signal so DSPs can act on it. Out of scope for this RFC but explicitly tracked. ## References - [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) — tracking issue +- [#3533](https://github.com/adcontextprotocol/adcp/pull/3533) — this RFC PR +- [#3764](https://github.com/adcontextprotocol/adcp/pull/3764) — schema implementation cut - [brand.json](/docs/brand-protocol/brand-json) — current normative spec - [Building a brand agent](/docs/brand-protocol/building-a-brand-agent) — the separate brand-agent MCP service spec - [#3378](https://github.com/adcontextprotocol/adcp/pull/3378) — brand-hierarchy auto-link (the trust model implemented in AAO crawler today) -- [#3450](https://github.com/adcontextprotocol/adcp/pull/3450) — team-page hierarchy display +- [#3486](https://github.com/adcontextprotocol/adcp/pull/3486) — search_brands discovery verb (cross-cutting follow-up for trust-state surfacing) From 6a3afc160b5ff43c0feaaead3d0d3a3030591827 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 1 May 2026 12:55:18 -0400 Subject: [PATCH 06/13] feat(brand-protocol): schema cut for distributed brand.json RFC (#3533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concrete schema additions for review against RFC #3533. Additive only — existing publishers unchanged. brand.json schema: - New variant: Brand Canonical Document. Self-published per-brand doc with `parent_house: BrandRef` pointer + optional house_attributes_overrides. Composes the existing `brand` definition via allOf so identity fields match the inline `brands[]` shape. - House Portfolio variant gains `brand_refs[]` (pointer brands, child-owned data) and `house_attributes` (house-wide inheritable attributes). Required changed from ["house","brands"] to ["house"] with anyOf at-least-one of brands[]/brand_refs[]. - Two new examples illustrating mixed inline+pointer house and a self- published Converse canonical document. docs/brand-protocol/brand-json.mdx: added a "Proposed (RFC)" callout pointing at the RFC and PR #3533. Existing four variants documented as-is. Cross-array invariant — a brand_id MUST NOT appear in both brands[] and brand_refs[] of the same house — is documented in field descriptions. JSON Schema can't express it; lint/validator follow-up needed if RFC ratifies. Status: review-only. Not normative until RFC ratifies. Marked DRAFT. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/distributed-brand-json-impl.md | 19 +++++ docs/brand-protocol/brand-json.mdx | 4 + static/schemas/source/brand.json | 93 ++++++++++++++++++++++- 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 .changeset/distributed-brand-json-impl.md diff --git a/.changeset/distributed-brand-json-impl.md b/.changeset/distributed-brand-json-impl.md new file mode 100644 index 0000000000..8c8e754e6f --- /dev/null +++ b/.changeset/distributed-brand-json-impl.md @@ -0,0 +1,19 @@ +--- +"adcontextprotocol": minor +--- + +Schema implementation cut for the distributed brand.json RFC ([#3533](https://github.com/adcontextprotocol/adcp/pull/3533)). Additive — existing publishers unchanged. + +**`brand.json` schema additions:** + +- New top-level variant: **Brand Canonical Document** — a self-published per-brand document carrying the brand's identity attributes plus `parent_house: BrandRef` (pointer to the corporate house) and optional `house_attributes_overrides`. +- **House Portfolio** variant gains: + - `brand_refs: BrandRef[]` — pointer brands whose canonical documents live elsewhere (child-owned data). Mutual-assertion trust required. + - `house_attributes` — house-wide attributes inherited by all brands (privacy policy, compliance flags, corporate legal entity). +- House Portfolio `required` widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` or `brand_refs[]`. + +**Cross-array invariant** (validator + lint, not JSON Schema expressible): a `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. + +**Trust model summary** (full text in the RFC): a child brand canonical document declares `parent_house: { domain: }`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Inline children (`brands[]`) are covered by the parent's document authenticity directly. + +**Status:** for review of the concrete shape only. Not normative until the RFC ratifies. The `brand-json.mdx` reference page carries a "Proposed" callout pointing at #3533. diff --git a/docs/brand-protocol/brand-json.mdx b/docs/brand-protocol/brand-json.mdx index b9eaf9f492..349619bc04 100644 --- a/docs/brand-protocol/brand-json.mdx +++ b/docs/brand-protocol/brand-json.mdx @@ -10,6 +10,10 @@ The `brand.json` file provides a standardized way for brands to claim their iden `brand.json` is the canonical source of brand identity data. The brand object defined here (logos, colors, tone, tagline) is the single brand definition used across AdCP. Tasks reference brands by domain and brand_id — the system resolves full identity from `brand.json` or the [registry](/docs/registry/index). + +**Proposed (RFC, not yet ratified):** A fifth variant — Brand Canonical Document — and new optional fields on House Portfolio (`brand_refs[]`, `house_attributes`) and on a brand canonical document (`parent_house`, `house_attributes_overrides`) are under discussion. They let a child brand publish its own canonical document while the house declares ownership via mutual assertion. The schema in this branch accepts these fields additively for review purposes; the existing four variants are unchanged for current publishers. See the [distributed brand.json RFC](https://github.com/adcontextprotocol/adcp/blob/main/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx) and [#3533](https://github.com/adcontextprotocol/adcp/pull/3533). + + ## File location Brands host the `brand.json` file at: diff --git a/static/schemas/source/brand.json b/static/schemas/source/brand.json index a93add52ce..bb63601d9d 100644 --- a/static/schemas/source/brand.json +++ b/static/schemas/source/brand.json @@ -1377,17 +1377,28 @@ { "type": "object", "title": "House Portfolio", - "description": "Full house/brand portfolio with hierarchy, creative assets, and properties", + "description": "Full house/brand portfolio with hierarchy, creative assets, and properties. May carry inline brands (parent-owned, brands[]) and/or pointer brands (child-owned canonical documents, brand_refs[]). At least one of brands[] or brand_refs[] is required. A brand_id MUST NOT appear in both. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", "properties": { "$schema": { "type": "string" }, "version": { "type": "string" }, "house": { "$ref": "#/definitions/house" }, "brands": { "type": "array", - "description": "Brands owned by this house", + "description": "Inline brands owned by this house (parent-owned data). Use for sub-brands without their own canonical document — typically those without a dedicated domain or that the holdco wants to manage centrally. A brand_id MUST NOT appear in both brands[] and brand_refs[].", "items": { "$ref": "#/definitions/brand" }, "minItems": 1 }, + "brand_refs": { + "type": "array", + "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's parent_house must reciprocate this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "items": { "$ref": "/schemas/core/brand-ref.json" }, + "minItems": 1 + }, + "house_attributes": { + "type": "object", + "description": "House-wide attributes inherited by all brands in the house (privacy policy, compliance flags, corporate legal entity, jurisdictional data). Children may override individual keys via house_attributes_overrides on their canonical document. Free-form by design; the spec will formalize specific keys per use case. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "additionalProperties": true + }, "contact": { "$ref": "#/definitions/contact" }, "authorized_operators": { "type": "array", @@ -1409,8 +1420,39 @@ }, "last_updated": { "type": "string", "format": "date-time" } }, - "required": ["house", "brands"], + "required": ["house"], + "anyOf": [ + { "required": ["brands"] }, + { "required": ["brand_refs"] } + ], "additionalProperties": false + }, + { + "type": "object", + "title": "Brand Canonical Document", + "description": "Self-published brand document where the brand owns its own identity attributes. Declares its parent house via parent_house; for trust, the named house's brand_refs[] must reciprocate (mutual assertion). Hosted at the brand's own /.well-known/brand.json (or via authoritative_location indirection). See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "allOf": [ + { + "type": "object", + "properties": { + "$schema": { "type": "string" }, + "version": { "type": "string" }, + "parent_house": { + "$ref": "/schemas/core/brand-ref.json", + "description": "Pointer to the corporate house this brand belongs to. The named house's brand_refs[] MUST reciprocate for mutual-assertion trust. Single-hop only — a brand cannot itself declare brand_refs[]." + }, + "house_attributes_overrides": { + "type": "object", + "description": "Per-brand overrides for inherited house_attributes. Only specified keys are overridden; others inherit from the parent house's house_attributes block.", + "additionalProperties": true + }, + "last_updated": { "type": "string", "format": "date-time" } + }, + "required": ["parent_house"], + "not": { "required": ["brand_refs"] } + }, + { "$ref": "#/definitions/brand" } + ] } ], "examples": [ @@ -1791,6 +1833,51 @@ } ], "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/brand.json", + "version": "1.0", + "house": { + "domain": "nikeinc.com", + "name": "Nike, Inc.", + "architecture": "hybrid" + }, + "house_attributes": { + "privacy_policy_url": "https://agreementservice.svs.nike.com/rest/agreement?agreementType=privacyPolicy", + "compliance_policies": ["no_under_13_targeting"], + "tax_entity": "Nike, Inc." + }, + "brands": [ + { + "id": "nike_sb", + "names": [{"en_US": "Nike SB"}], + "keller_type": "sub_brand", + "logos": [ + {"url": "https://nike.com/sb/logo.svg", "variant": "primary"} + ] + } + ], + "brand_refs": [ + { "domain": "converse.com", "brand_id": "converse" }, + { "domain": "jordan.com", "brand_id": "jordan" } + ], + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/brand.json", + "version": "1.0", + "id": "converse", + "names": [{"en_US": "Converse"}], + "keller_type": "sub_brand", + "parent_house": { "domain": "nikeinc.com", "brand_id": "converse" }, + "logos": [ + {"url": "https://converse.com/logo.svg", "variant": "primary"} + ], + "tagline": "Sneaker for the streets", + "house_attributes_overrides": { + "compliance_policies": ["no_under_13_targeting", "us_only"] + }, + "last_updated": "2026-01-15T10:00:00Z" } ] } From 2698ead01bb2c42402947ac5a23417092058118c Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 15:58:30 -0400 Subject: [PATCH 07/13] fix(brand-protocol): tighten Brand Canonical Document variant + fix parent_house example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-reviewer + protocol-expert findings on PR #3764: - Brand Canonical Document variant lacked oneOf disambiguation. brand definition has additionalProperties: true and the inner allOf member had no constraint, so a malformed Portfolio with a stray parent_house could silently re-type as a Canonical Document. Replaced the narrow not: {required: ["brand_refs"]} with not: {anyOf: [...]} blocking all house-only top-level keys (house, brands, brand_refs, house_attributes, authorized_operators). - Fixed example: parent_house: { domain: "nikeinc.com", brand_id: "converse" } read as "I'm pointing at converse inside nikeinc.com" — but Converse owns this document; the brand_id was the self, not the parent. parent_house is a pointer UP, only domain is meaningful for a typical house pointer. Updated both the schema example and the docs section. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/brand-protocol/brand-json.mdx | 103 +++++++++++++++++++++++++++++ static/schemas/source/brand.json | 12 +++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/docs/brand-protocol/brand-json.mdx b/docs/brand-protocol/brand-json.mdx index 349619bc04..d03820befd 100644 --- a/docs/brand-protocol/brand-json.mdx +++ b/docs/brand-protocol/brand-json.mdx @@ -150,6 +150,109 @@ Contains the full brand hierarchy with all brands and properties: } ``` +## Distributed extensions (Proposed — RFC #3533) + + +The fields and variant in this section are **under RFC review** ([#3533](https://github.com/adcontextprotocol/adcp/pull/3533), schema cut [#3764](https://github.com/adcontextprotocol/adcp/pull/3764)) and not yet normative. Existing publishers using the four variants above are unaffected. See the [proposal document](https://github.com/adcontextprotocol/adcp/blob/main/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx) for the full motivation, trust model, and migration plan. + + +The proposal lets a child brand publish its **own** canonical document while the house remains the authority for who is in the family. The data model stays one level deep — only houses declare ownership; a child brand cannot itself declare children. + +### 5. Brand Canonical Document (proposed) + +Self-published per-brand document where the brand owns its own identity attributes. Hosted at the brand's own `/.well-known/brand.json` (or via [authoritative location redirect](#1-authoritative-location-redirect)). Declares its parent house via `parent_house`; trust requires the named house to reciprocate. + +```json +{ + "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", + "version": "1.0", + "id": "converse", + "names": [{"en_US": "Converse"}], + "keller_type": "sub_brand", + "parent_house": { "domain": "nikeinc.com" }, + "logos": [ + {"url": "https://converse.com/logo.svg", "variant": "primary"} + ], + "tagline": "Sneaker for the streets", + "house_attributes_overrides": { + "compliance_policies": ["no_under_13_targeting", "us_only"] + } +} +``` + +Use this when a brand has its own domain and wants self-publish authority for its identity (logos, colors, tone, taglines). The brand carries the same identity fields as an inline entry in `brands[]`. + +### House Portfolio additions (proposed) + +A House Portfolio document gains two optional fields: + +| Field | Type | Description | +|---|---|---| +| `brand_refs` | array of [BrandRef](#brand-references) | Pointer brands. Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. **Mutual-assertion trust:** the pointed-to document's `parent_house.domain` must equal this house's domain. | +| `house_attributes` | object | House-wide attributes inherited by all brands in the house (privacy policy, compliance flags, corporate legal entity, jurisdictional data). Free-form; specific keys formalize per use case. | + +A house may use `brands[]`, `brand_refs[]`, or both. **Constraint:** a `brand_id` MUST NOT appear in both arrays. + +```json +{ + "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", + "version": "1.0", + "house": { + "domain": "nikeinc.com", + "name": "Nike, Inc.", + "architecture": "hybrid" + }, + "house_attributes": { + "privacy_policy_url": "https://agreementservice.svs.nike.com/...", + "compliance_policies": ["no_under_13_targeting"], + "tax_entity": "Nike, Inc." + }, + "brands": [ + { + "id": "nike_sb", + "names": [{"en_US": "Nike SB"}], + "keller_type": "sub_brand" + } + ], + "brand_refs": [ + { "domain": "converse.com", "brand_id": "converse" }, + { "domain": "jordan.com", "brand_id": "jordan" } + ] +} +``` + +### Brand Canonical Document additions (proposed) + +A self-published brand document gains: + +| Field | Type | Description | +|---|---|---| +| `parent_house` | [BrandRef](#brand-references) | Pointer to the corporate house this brand belongs to. The named house's `brand_refs[]` MUST reciprocate for mutual-assertion trust. Single-hop — a brand cannot itself have `brand_refs[]`. | +| `house_attributes_overrides` | object | Per-brand overrides for inherited `house_attributes`. Only specified keys are overridden; others inherit from the parent house. | + +### Mutual-assertion trust model + +Trust between a house and a pointer child requires **both sides** to reciprocate: + +| Edge | Both sides match? | Trust | +|---|---|---| +| Inline child in `brands[]` | n/a — house owns the data | Trusted (parent's TLS = the assertion) | +| Pointer child: `parent_house: A`, A's `brand_refs[]` includes child | Yes | **Trusted edge.** Inheritance of `house_attributes`, governance propagation, member-feature inheritance: yes. | +| Pointer child claims `parent_house: A`, A's `brand_refs[]` does not include child | No | One-sided. Surface as "claimed, unverified." Don't extend trust. | +| A's `brand_refs[]` includes child, child has no `parent_house` | No | One-sided in the other direction. Treat child as having no parent. | + +Validation is a runtime check (the crawler fetches both documents and compares); the JSON Schema accepts the fields independently. + +### Resolution + +For a brand at `domain.com`: + +1. Fetch `domain.com/.well-known/brand.json`. If it has `authoritative_location`, fetch that URL. +2. The result is either an inline brand entry (the document is a House Portfolio containing this brand in `brands[]`) or a Brand Canonical Document (the document IS the brand). +3. If it's a Brand Canonical Document with `parent_house`, fetch the house's `brand.json` to verify reciprocation and resolve `house_attributes` (merged with `house_attributes_overrides`). + +Single-hop. No recursive walking. + ## House definition The house object represents the corporate entity: diff --git a/static/schemas/source/brand.json b/static/schemas/source/brand.json index bb63601d9d..98fe53f50e 100644 --- a/static/schemas/source/brand.json +++ b/static/schemas/source/brand.json @@ -1449,7 +1449,15 @@ "last_updated": { "type": "string", "format": "date-time" } }, "required": ["parent_house"], - "not": { "required": ["brand_refs"] } + "not": { + "anyOf": [ + { "required": ["house"] }, + { "required": ["brands"] }, + { "required": ["brand_refs"] }, + { "required": ["house_attributes"] }, + { "required": ["authorized_operators"] } + ] + } }, { "$ref": "#/definitions/brand" } ] @@ -1869,7 +1877,7 @@ "id": "converse", "names": [{"en_US": "Converse"}], "keller_type": "sub_brand", - "parent_house": { "domain": "nikeinc.com", "brand_id": "converse" }, + "parent_house": { "domain": "nikeinc.com" }, "logos": [ {"url": "https://converse.com/logo.svg", "variant": "primary"} ], From 64c59c588fa5af7fe950977d07f1b78ddd8b0484 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 19:56:01 -0400 Subject: [PATCH 08/13] =?UTF-8?q?feat(brand-protocol):=20align=20impl=20wi?= =?UTF-8?q?th=20RFC=20v3=20=E2=80=94=20house=5Fdomain=20string=20+=20manag?= =?UTF-8?q?ed=5Fby=20+=20drop=20house=5Fattributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracks RFC #3533 v3. Replaces the v2 cut with the simplified model decided during expert review. Schema deltas (relative to previous impl cut): - Replace parent_house ($ref core/brand-ref.json) with house_domain (string, reuses #/definitions/domain). Strings match the existing House Redirect convention and drop the planned core/house-ref.json file entirely. - Make house_domain optional on Brand Canonical Document so standalone brands (Patagonia, Liquid Death) have a valid shape without spinning up a degenerate "house of one." If a standalone brand is later acquired, it adds house_domain and the new house adds it to brand_refs[]. No new variant needed. - Drop house_attributes / house_attributes_overrides entirely. Inheritance/ override semantics turned out muddy — if a brand could weaken a house policy, it wasn't really a house policy. House-level fields stay where they already are (data_subject_contestation, trademarks, authorized_operators on the house schema). Brand-level constraints are additive, not overrides. - Replace the brand_refs[] $ref to core/brand-ref.json with an inline {domain, brand_id?, managed_by?} shape. brand-ref.json's existing governance-override fields (industries, data_subject_contestation) don't belong on a house-side declaration; they're consumer-side overrides. - Add managed_by (string, optional) on brand_refs[] entries — house-declared delegation for grouping/discovery, non-trust-bearing. Captures WPP/Publicis reality (BBH manages this brand for WPP) without reintroducing recursive trust. Examples updated: - Nike Inc. mixed-shape (inline Nike SB + pointer Converse, Jordan) - WPP with managed_by (BBH Sport managed_by bbh.com, etc.) - Converse self-published with house_domain - Patagonia standalone (no house_domain) docs/brand-protocol/brand-json.mdx: rewritten Distributed Extensions section to match the new shape. Adds the four worked examples. Adds field-resolution table showing where each consumer-side question is answered (no inheritance, no overrides). Adds explicit M&A section showing existing redirect variants handle reorganizations. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/distributed-brand-json-impl.md | 11 +-- docs/brand-protocol/brand-json.mdx | 107 +++++++++++++++++----- static/schemas/source/brand.json | 79 ++++++++++------ 3 files changed, 141 insertions(+), 56 deletions(-) diff --git a/.changeset/distributed-brand-json-impl.md b/.changeset/distributed-brand-json-impl.md index 8c8e754e6f..0e69e1f2da 100644 --- a/.changeset/distributed-brand-json-impl.md +++ b/.changeset/distributed-brand-json-impl.md @@ -6,14 +6,13 @@ Schema implementation cut for the distributed brand.json RFC ([#3533](https://gi **`brand.json` schema additions:** -- New top-level variant: **Brand Canonical Document** — a self-published per-brand document carrying the brand's identity attributes plus `parent_house: BrandRef` (pointer to the corporate house) and optional `house_attributes_overrides`. -- **House Portfolio** variant gains: - - `brand_refs: BrandRef[]` — pointer brands whose canonical documents live elsewhere (child-owned data). Mutual-assertion trust required. - - `house_attributes` — house-wide attributes inherited by all brands (privacy policy, compliance flags, corporate legal entity). +- New top-level variant: **Brand Canonical Document** — a self-published per-brand document carrying the brand's identity attributes plus optional `house_domain` (string, the domain of the brand's parent house). Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. Excludes top-level house-only fields (`house`, `brands`, `brand_refs`, `authorized_operators`) to disambiguate from House Portfolio. +- **House Portfolio** variant gains `brand_refs[]` — pointer brands whose canonical documents live elsewhere (child-owned data). Each entry: `{ domain, brand_id?, managed_by? }`. `managed_by` (optional) is house-declared, non-trust-bearing — for grouping and discovery, used by holdcos to express agency-network delegation. - House Portfolio `required` widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` or `brand_refs[]`. +- All new fields reuse existing schema patterns (`#/definitions/domain`, `#/definitions/brand_id`); no new `core/*.json` files added. **Cross-array invariant** (validator + lint, not JSON Schema expressible): a `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. -**Trust model summary** (full text in the RFC): a child brand canonical document declares `parent_house: { domain: }`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Inline children (`brands[]`) are covered by the parent's document authenticity directly. +**Trust model summary** (full text in the RFC): a child brand canonical document declares `house_domain: ""`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Inline children (`brands[]`) are covered by the parent's document authenticity directly. `managed_by` carries no trust weight. -**Status:** for review of the concrete shape only. Not normative until the RFC ratifies. The `brand-json.mdx` reference page carries a "Proposed" callout pointing at #3533. +**Status:** for review of the concrete shape only. Not normative until the RFC ratifies. The `brand-json.mdx` reference page carries a "Proposed" callout pointing at #3533 and worked examples (Nike mixed hybrid, WPP with delegation, Converse self-published, Patagonia standalone). diff --git a/docs/brand-protocol/brand-json.mdx b/docs/brand-protocol/brand-json.mdx index d03820befd..e7a39a0c8b 100644 --- a/docs/brand-protocol/brand-json.mdx +++ b/docs/brand-protocol/brand-json.mdx @@ -11,7 +11,7 @@ The `brand.json` file provides a standardized way for brands to claim their iden -**Proposed (RFC, not yet ratified):** A fifth variant — Brand Canonical Document — and new optional fields on House Portfolio (`brand_refs[]`, `house_attributes`) and on a brand canonical document (`parent_house`, `house_attributes_overrides`) are under discussion. They let a child brand publish its own canonical document while the house declares ownership via mutual assertion. The schema in this branch accepts these fields additively for review purposes; the existing four variants are unchanged for current publishers. See the [distributed brand.json RFC](https://github.com/adcontextprotocol/adcp/blob/main/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx) and [#3533](https://github.com/adcontextprotocol/adcp/pull/3533). +**Proposed (RFC, not yet ratified):** A fifth variant — Brand Canonical Document — and new optional fields on House Portfolio (`brand_refs[]`) and on a brand canonical document (`house_domain`) are under discussion. They let a child brand publish its own canonical document while the house declares ownership via mutual assertion. House delegation (e.g., a holdco letting an agency manage a brand) is expressed via an optional `managed_by` field on the house's `brand_refs[]` entry. The schema in this branch accepts these fields additively for review purposes; the existing four variants are unchanged for current publishers. See the [distributed brand.json RFC](https://github.com/adcontextprotocol/adcp/blob/main/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx) and [#3533](https://github.com/adcontextprotocol/adcp/pull/3533). ## File location @@ -160,7 +160,9 @@ The proposal lets a child brand publish its **own** canonical document while the ### 5. Brand Canonical Document (proposed) -Self-published per-brand document where the brand owns its own identity attributes. Hosted at the brand's own `/.well-known/brand.json` (or via [authoritative location redirect](#1-authoritative-location-redirect)). Declares its parent house via `parent_house`; trust requires the named house to reciprocate. +Self-published per-brand document where the brand owns its own identity attributes. Hosted at the brand's own `/.well-known/brand.json` (or via [authoritative location redirect](#1-authoritative-location-redirect)). Optionally declares its house via `house_domain`; for trust, the named house's `brand_refs[]` must reciprocate. Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. + +**With a house — Converse under Nike:** ```json { @@ -169,14 +171,27 @@ Self-published per-brand document where the brand owns its own identity attribut "id": "converse", "names": [{"en_US": "Converse"}], "keller_type": "sub_brand", - "parent_house": { "domain": "nikeinc.com" }, + "house_domain": "nikeinc.com", "logos": [ {"url": "https://converse.com/logo.svg", "variant": "primary"} ], - "tagline": "Sneaker for the streets", - "house_attributes_overrides": { - "compliance_policies": ["no_under_13_targeting", "us_only"] - } + "tagline": "Sneaker for the streets" +} +``` + +**Standalone — Patagonia:** + +```json +{ + "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", + "version": "1.0", + "id": "patagonia", + "names": [{"en_US": "Patagonia"}], + "keller_type": "independent", + "logos": [ + {"url": "https://patagonia.com/logo.svg", "variant": "primary"} + ], + "tagline": "We're in business to save our home planet." } ``` @@ -184,15 +199,24 @@ Use this when a brand has its own domain and wants self-publish authority for it ### House Portfolio additions (proposed) -A House Portfolio document gains two optional fields: +A House Portfolio document gains one optional field: | Field | Type | Description | |---|---|---| -| `brand_refs` | array of [BrandRef](#brand-references) | Pointer brands. Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. **Mutual-assertion trust:** the pointed-to document's `parent_house.domain` must equal this house's domain. | -| `house_attributes` | object | House-wide attributes inherited by all brands in the house (privacy policy, compliance flags, corporate legal entity, jurisdictional data). Free-form; specific keys formalize per use case. | +| `brand_refs[]` | array of objects | Pointer brands owned by this house. Each entry: `{ domain, brand_id?, managed_by? }`. Mutual-assertion trust: the pointed-to document's `house_domain` must equal this house's domain. | A house may use `brands[]`, `brand_refs[]`, or both. **Constraint:** a `brand_id` MUST NOT appear in both arrays. +The `brand_refs[]` entry shape: + +| Field | Required | Meaning | +|---|---|---| +| `domain` | yes | Where the child's canonical brand.json lives | +| `brand_id` | no | Stable identifier for this brand within the house's portfolio | +| `managed_by` | no | Domain of the entity that operationally manages this brand. **House-declared, non-trust-bearing.** UIs and discovery tools group by `managed_by`; the leaf doesn't have to know about the manager. | + +**Example — mixed hybrid (Nike):** + ```json { "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", @@ -202,11 +226,6 @@ A house may use `brands[]`, `brand_refs[]`, or both. **Constraint:** a `brand_id "name": "Nike, Inc.", "architecture": "hybrid" }, - "house_attributes": { - "privacy_policy_url": "https://agreementservice.svs.nike.com/...", - "compliance_policies": ["no_under_13_targeting"], - "tax_entity": "Nike, Inc." - }, "brands": [ { "id": "nike_sb", @@ -221,14 +240,33 @@ A house may use `brands[]`, `brand_refs[]`, or both. **Constraint:** a `brand_id } ``` +**Example — house with delegation (WPP):** + +```json +{ + "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", + "version": "1.0", + "house": { + "domain": "wpp.com", + "name": "WPP plc" + }, + "brand_refs": [ + { "domain": "bbh-sport.com", "brand_id": "bbh_sport", "managed_by": "bbh.com" }, + { "domain": "ogilvy-toyota.com", "brand_id": "ogilvy_toyota", "managed_by": "ogilvy.com" }, + { "domain": "wpp-direct.com", "brand_id": "wpp_direct" } + ] +} +``` + +`managed_by` is unilaterally declared by the owning house. The leaf at `bbh-sport.com` says only `house_domain: "wpp.com"` — it doesn't reference BBH at all. UIs render BBH Sport under BBH for agency views; trust validation walks BBH Sport → WPP only. WPP can change the manager without updating any leaf. + ### Brand Canonical Document additions (proposed) A self-published brand document gains: | Field | Type | Description | |---|---|---| -| `parent_house` | [BrandRef](#brand-references) | Pointer to the corporate house this brand belongs to. The named house's `brand_refs[]` MUST reciprocate for mutual-assertion trust. Single-hop — a brand cannot itself have `brand_refs[]`. | -| `house_attributes_overrides` | object | Per-brand overrides for inherited `house_attributes`. Only specified keys are overridden; others inherit from the parent house. | +| `house_domain` | string (domain) | Optional pointer to the corporate house this brand belongs to. The named house's `brand_refs[]` MUST reciprocate for mutual-assertion trust. Single-hop — a brand cannot itself have `brand_refs[]`. Omit for standalone brands. | ### Mutual-assertion trust model @@ -237,22 +275,45 @@ Trust between a house and a pointer child requires **both sides** to reciprocate | Edge | Both sides match? | Trust | |---|---|---| | Inline child in `brands[]` | n/a — house owns the data | Trusted (parent's TLS = the assertion) | -| Pointer child: `parent_house: A`, A's `brand_refs[]` includes child | Yes | **Trusted edge.** Inheritance of `house_attributes`, governance propagation, member-feature inheritance: yes. | -| Pointer child claims `parent_house: A`, A's `brand_refs[]` does not include child | No | One-sided. Surface as "claimed, unverified." Don't extend trust. | -| A's `brand_refs[]` includes child, child has no `parent_house` | No | One-sided in the other direction. Treat child as having no parent. | +| Pointer child: `house_domain: A`, A's `brand_refs[]` includes child | Yes | **Trusted edge.** Governance propagation, member-feature inheritance, billable inclusion: yes. | +| Pointer child claims `house_domain: A`, A's `brand_refs[]` does not include child | No | One-sided. Surface as "claimed, unverified." Don't extend trust. | +| A's `brand_refs[]` includes child, child has no `house_domain` | No | One-sided in the other direction. Treat child as standalone. | +| Standalone brand (no `house_domain`) | n/a | Trusted as itself. No house relationship. | Validation is a runtime check (the crawler fetches both documents and compares); the JSON Schema accepts the fields independently. -### Resolution +### Field resolution + +There is no inheritance/override block. Each consumer-side question has a single answer: + +| Question | Resolution | +|---|---| +| Brand identity (logos, colors, tone, tagline, voice) | Read from brand's own canonical document. For inline children, read from the parent's `brands[]` entry. | +| Brand contact, properties, industries | Read from brand's own canonical document. | +| `data_subject_contestation` | Brand-level if present; otherwise walk `house_domain` → `house.data_subject_contestation`. (Existing resolution rule, unchanged.) | +| Trademarks, authorized_operators, corporate contact | Read from the house's brand.json. | +| Mutual-assertion verification | Fetch both brand and house, compare `house_domain` ↔ `brand_refs[]`. | + +If a brand wants stricter constraints than its house (e.g., extra audience exclusions), it publishes those constraints at the brand level. House and brand constraints both apply; consumers respect the union. + +### Resolution algorithm For a brand at `domain.com`: -1. Fetch `domain.com/.well-known/brand.json`. If it has `authoritative_location`, fetch that URL. +1. Fetch `domain.com/.well-known/brand.json`. If it's a redirect variant ([authoritative_location](#1-authoritative-location-redirect) or [House Redirect](#2-house-redirect)), follow it. 2. The result is either an inline brand entry (the document is a House Portfolio containing this brand in `brands[]`) or a Brand Canonical Document (the document IS the brand). -3. If it's a Brand Canonical Document with `parent_house`, fetch the house's `brand.json` to verify reciprocation and resolve `house_attributes` (merged with `house_attributes_overrides`). +3. If it's a Brand Canonical Document with `house_domain`, fetch the house's `brand.json` (following redirects) to verify reciprocation. Read corporate-level fields (e.g., `data_subject_contestation`) from the house if not present on the brand. Single-hop. No recursive walking. +### Acquisitions and reorganizations + +Existing redirect variants handle M&A natively: + +1. Pre-deal: `dentsu.com/.well-known/brand.json` is a House Portfolio. Dentsu brands' canonical docs say `house_domain: "dentsu.com"`. +2. Deal closes: Dentsu's `brand.json` is replaced with a [House Redirect](#2-house-redirect) → `{ "house": "wpp.com" }`. WPP adds the acquired brands to `brand_refs[]` with `managed_by: "dentsu.com"` for ops continuity. +3. Post-deal: leaves still pointing at `house_domain: "dentsu.com"` resolve through the redirect to WPP. Mutual-assertion holds via the redirect chain. No urgent leaf migration needed. + ## House definition The house object represents the corporate entity: diff --git a/static/schemas/source/brand.json b/static/schemas/source/brand.json index 98fe53f50e..549142a4be 100644 --- a/static/schemas/source/brand.json +++ b/static/schemas/source/brand.json @@ -1390,15 +1390,29 @@ }, "brand_refs": { "type": "array", - "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's parent_house must reciprocate this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", - "items": { "$ref": "/schemas/core/brand-ref.json" }, + "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "items": { + "type": "object", + "description": "Reference to a pointer brand owned by this house. The child publishes its own canonical brand.json at the named domain.", + "properties": { + "domain": { + "$ref": "#/definitions/domain", + "description": "Domain where the child's canonical brand.json lives" + }, + "brand_id": { + "$ref": "#/definitions/brand_id", + "description": "Stable brand identifier within the house portfolio" + }, + "managed_by": { + "$ref": "#/definitions/domain", + "description": "Optional domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared, non-trust-bearing — used for grouping and discovery, not validation. The child does not need to reciprocate this claim." + } + }, + "required": ["domain"], + "additionalProperties": false + }, "minItems": 1 }, - "house_attributes": { - "type": "object", - "description": "House-wide attributes inherited by all brands in the house (privacy policy, compliance flags, corporate legal entity, jurisdictional data). Children may override individual keys via house_attributes_overrides on their canonical document. Free-form by design; the spec will formalize specific keys per use case. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", - "additionalProperties": true - }, "contact": { "$ref": "#/definitions/contact" }, "authorized_operators": { "type": "array", @@ -1430,31 +1444,24 @@ { "type": "object", "title": "Brand Canonical Document", - "description": "Self-published brand document where the brand owns its own identity attributes. Declares its parent house via parent_house; for trust, the named house's brand_refs[] must reciprocate (mutual assertion). Hosted at the brand's own /.well-known/brand.json (or via authoritative_location indirection). See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "description": "Self-published brand document where the brand owns its own identity attributes. Optionally declares its house via house_domain; for trust, the named house's brand_refs[] must reciprocate (mutual assertion). Standalone brands (no parent house) omit house_domain. Hosted at the brand's own /.well-known/brand.json (or via authoritative_location indirection). See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", "allOf": [ { "type": "object", "properties": { "$schema": { "type": "string" }, "version": { "type": "string" }, - "parent_house": { - "$ref": "/schemas/core/brand-ref.json", - "description": "Pointer to the corporate house this brand belongs to. The named house's brand_refs[] MUST reciprocate for mutual-assertion trust. Single-hop only — a brand cannot itself declare brand_refs[]." - }, - "house_attributes_overrides": { - "type": "object", - "description": "Per-brand overrides for inherited house_attributes. Only specified keys are overridden; others inherit from the parent house's house_attributes block.", - "additionalProperties": true + "house_domain": { + "$ref": "#/definitions/domain", + "description": "Optional pointer to the corporate house this brand belongs to. The named house's brand_refs[] MUST reciprocate for mutual-assertion trust. Single-hop only — a brand cannot itself declare brand_refs[]. Omit for standalone brands (no house)." }, "last_updated": { "type": "string", "format": "date-time" } }, - "required": ["parent_house"], "not": { "anyOf": [ { "required": ["house"] }, { "required": ["brands"] }, { "required": ["brand_refs"] }, - { "required": ["house_attributes"] }, { "required": ["authorized_operators"] } ] } @@ -1850,11 +1857,6 @@ "name": "Nike, Inc.", "architecture": "hybrid" }, - "house_attributes": { - "privacy_policy_url": "https://agreementservice.svs.nike.com/rest/agreement?agreementType=privacyPolicy", - "compliance_policies": ["no_under_13_targeting"], - "tax_entity": "Nike, Inc." - }, "brands": [ { "id": "nike_sb", @@ -1871,20 +1873,43 @@ ], "last_updated": "2026-01-15T10:00:00Z" }, + { + "$schema": "/schemas/brand.json", + "version": "1.0", + "house": { + "domain": "wpp.com", + "name": "WPP plc" + }, + "brand_refs": [ + { "domain": "bbh-sport.com", "brand_id": "bbh_sport", "managed_by": "bbh.com" }, + { "domain": "ogilvy-toyota.com", "brand_id": "ogilvy_toyota", "managed_by": "ogilvy.com" }, + { "domain": "wpp-direct.com", "brand_id": "wpp_direct" } + ], + "last_updated": "2026-01-15T10:00:00Z" + }, { "$schema": "/schemas/brand.json", "version": "1.0", "id": "converse", "names": [{"en_US": "Converse"}], "keller_type": "sub_brand", - "parent_house": { "domain": "nikeinc.com" }, + "house_domain": "nikeinc.com", "logos": [ {"url": "https://converse.com/logo.svg", "variant": "primary"} ], "tagline": "Sneaker for the streets", - "house_attributes_overrides": { - "compliance_policies": ["no_under_13_targeting", "us_only"] - }, + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/brand.json", + "version": "1.0", + "id": "patagonia", + "names": [{"en_US": "Patagonia"}], + "keller_type": "independent", + "logos": [ + {"url": "https://patagonia.com/logo.svg", "variant": "primary"} + ], + "tagline": "We're in business to save our home planet.", "last_updated": "2026-01-15T10:00:00Z" } ] From d34127be7117c684f3bc168392eb9f94db639769 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 20:02:25 -0400 Subject: [PATCH 09/13] =?UTF-8?q?docs(brand-protocol):=20RFC=20v3=20tighte?= =?UTF-8?q?ning=20=E2=80=94=20normative=20managed=5Fby,=20conformance,=20s?= =?UTF-8?q?trictest-of=20compliance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses round-2 expert review on RFC v3: - managed_by gets normative language: MUST NOT be used for trust/auth decisions; verifiers MUST ignore for authorization. UIs SHOULD render as unilateral house claim, not aggregate cross-house. - New "Compliance fields: strictest-of resolution" section. Identity fields (logos, voice, tone) stay brand-wins. Compliance/governance fields (data_subject_contestation, compliance_policies, regulated-category flags) resolve as union/strictest of house-level and brand-level — brand cannot weaken house assertions, only add stricter constraints. - New "Standalone brands" subsection: absence of house_domain ⇒ standalone, regardless of one-sided third-party brand_refs[] claims. - New "Conformance" section formalizing brand_id cross-array uniqueness, within-array uniqueness, mutual-assertion as canonical trust primitive, managed_by non-trust, standalone trumps third-party claim, strictest-of rule, 180-day TTL. - New "Prior art" section citing IAB Tech Lab ads.txt/sellers.json reciprocal publication model — same trust shape, deployed industry pattern. No structural changes to the data model. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../proposals/distributed-brand-json-rfc.mdx | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx index 042d89ebcc..5e32cc0212 100644 --- a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx +++ b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx @@ -166,6 +166,8 @@ Real holdcos delegate brand management to agency networks. WPP plc owns brands b If WPP changes the manager (BBH → directly managed → Ogilvy), only WPP's brand_refs[] entry updates. The leaf doesn't churn. If BBH disagrees with the management claim, that's a legal/business matter, not a protocol concern. +**Normative:** `managed_by` MUST NOT be used for trust or authorization decisions. Verifiers MUST ignore it when evaluating mutual assertion, governance propagation, billable inclusion, or operator authorization. UIs SHOULD render it as a unilateral house claim (e.g., "WPP says BBH manages this"), and consumers SHOULD NOT aggregate cross-house ("BBH's portfolio") without independent confirmation from the named manager. + ### Hosting is independent The data model says nothing about where bytes live. For pointer children, the canonical document is served at the pointed-to domain via the existing discovery contract (`domain.com/.well-known/brand.json`, with optional `authoritative_location` indirection). The hosting party is an implementation choice: @@ -194,7 +196,19 @@ The spec already separates per-brand fields from house-level fields. The new sha There is no `house_attributes` block, no `house_attributes_overrides`, no `house_attributes_locked`. Houses publish their corporate-level fields in the existing house schema; brands publish their brand-level fields in their own canonical document; consumers walk `house_domain` to read corporate fields when needed. -If a brand wants stricter compliance than its house (e.g., extra audience exclusions), it publishes those constraints at the brand level. House and brand constraints both apply; consumers respect the union. +### Compliance fields: strictest-of resolution + +For **identity** fields (`name`, `names`, `logos`, `colors`, `fonts`, `tone`, `voice`, `tagline`, `visual_guidelines`, `avatar`), the brand-level value is authoritative; the house value is not consulted. + +For **compliance and governance** fields, the resolved value is the **strictest of** the house-level and brand-level values. A brand cannot weaken a house's governance assertions; it can only add stricter constraints. This applies to: + +- `data_subject_contestation` — both contacts SHOULD be presented to data subjects (consumers may use either; brand-level does NOT replace house-level) +- `compliance_policies`, audience exclusions, regulated-category flags — resolved as union (more restrictions wins) +- Future regulated-category fields as the spec formalizes them + +This is the load-bearing reason holdcos publish corporate-level governance: brand teams should not be able to soften compliance by self-publishing. Identity fields are brand-wins because that's what brand teams own; governance fields are strictest-of because that's how legal/compliance regimes actually work. + +The schema does not encode this rule; it's a resolution-layer semantic in the spec text. Validators and crawlers implement it; publishers may rely on it. ## Trust model @@ -231,6 +245,10 @@ For inline children in `brands[]`, mutual assertion is implicit — the house au Out of scope for v1. Future extensions: a house's brand-agent could sign attestations about its hosted brands; verifiable credentials; etc. Not needed to ship the data model. +### Standalone brands + +A brand canonical document MAY omit `house_domain`. Such a document is a **standalone brand** — no house relationship, no mutual-assertion edge to evaluate. Even if some other house's `brand_refs[]` lists this brand, the absence of `house_domain` on the brand's own document is dispositive: the brand is treated as standalone, the third-party claim is one-sided supportive metadata only, and no governance/inheritance trust is extended. + ### Conflict resolution | Scenario | Resolution | @@ -344,6 +362,18 @@ Single hop. No recursion, no max-depth, no cycle protection needed. - Reject a brand_id appearing in both `brands[]` and `brand_refs[]` of the same house. - Warn when a `house_domain` claim is not mutually-asserted by the named house (advisory only — single-sided claims are allowed by spec, just not trusted). +## Conformance + +These invariants MUST be enforced by validators and crawlers; JSON Schema cannot express them directly: + +- **`brand_id` cross-array uniqueness.** A given `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. Publisher must choose one. +- **`brand_id` within-array uniqueness.** A given `brand_id` MUST be unique within `brands[]` and unique within `brand_refs[]` of the same house. +- **Mutual-assertion as the trust primitive.** Consumers MUST NOT extend governance trust (auto-provisioning, member-feature inheritance, billable seat inclusion, inherited compliance fields) through one-sided claims. Mutual assertion (child's `house_domain` matches a `brand_refs[]` entry on the named house) is the canonical trust edge. +- **`managed_by` not a trust signal.** Consumers MUST NOT use `managed_by` for trust or authorization decisions. UIs SHOULD render it as a unilateral house claim. +- **Standalone trumps third-party claim.** A brand canonical document with no `house_domain` is standalone, regardless of any third-party house's `brand_refs[]` claim about it. +- **Compliance fields strictest-of.** For governance fields (`data_subject_contestation`, `compliance_policies`, audience exclusions, regulated-category flags), the resolved value is the union/strictest of house-level and brand-level. Brand-level publishers MUST NOT rely on weakening house-level assertions. +- **180-day TTL.** Mutual-assertion edges that have not been re-validated within 180 days SHOULD be treated as one-sided regardless of last-known state. + ## Open questions These need spec-owner / discussion input: @@ -354,6 +384,12 @@ These need spec-owner / discussion input: 4. **Migration timeline.** Both shapes coexist indefinitely; no forced cutover. If a deprecation is ever appropriate, that's a future RFC. 5. **`search_brands` trust state surfacing.** The crawler computes `mutually_asserted: true|false` per brand. PR [#3486](https://github.com/adcontextprotocol/adcp/pull/3486) (search_brands discovery verb) doesn't currently surface this in response stubs. Follow-up: extend `SearchBrandResult` to carry the trust signal so DSPs can act on it. Out of scope for this RFC but explicitly tracked. +## Prior art + +The mutual-assertion trust primitive proposed here mirrors the IAB Tech Lab's `ads.txt` / `sellers.json` reciprocal-publication model: a buyer is trusted as a seller's reseller iff both sides publish the relationship at well-known URLs. Same trust shape, same non-cryptographic "who claims what about whom" verification, same fallback to one-sided / unverified for partial publication. It's a deployed, durable industry pattern. + +Within AdCP, PR [#3468](https://github.com/adcontextprotocol/adcp/pull/3468) (provenance verifier contract — seller-publishes / buyer-represents / seller-confirms) uses the same family of construction for a different field family. + ## References - [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) — tracking issue @@ -363,3 +399,4 @@ These need spec-owner / discussion input: - [Building a brand agent](/docs/brand-protocol/building-a-brand-agent) — the separate brand-agent MCP service spec - [#3378](https://github.com/adcontextprotocol/adcp/pull/3378) — brand-hierarchy auto-link (the trust model implemented in AAO crawler today) - [#3486](https://github.com/adcontextprotocol/adcp/pull/3486) — search_brands discovery verb (cross-cutting follow-up for trust-state surfacing) +- [IAB Tech Lab ads.txt / sellers.json](https://iabtechlab.com/ads-txt/) — prior art for mutual-assertion trust at well-known URLs From 41ece56200cbfe3c1cb0e9e7ed2df47e2d6a2686 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 20:02:58 -0400 Subject: [PATCH 10/13] =?UTF-8?q?feat(brand-protocol):=20schema=20review?= =?UTF-8?q?=20fixes=20=E2=80=94=20extract=20brand=5Fref=20definition=20+?= =?UTF-8?q?=20tighten=20not=20deny-list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-reviewer round-2 nits on PR #3764: - Extract brand_refs[] item shape from inline to #/definitions/brand_ref (named type for SDK codegen / generator output). Description includes the managed_by non-trust normative language directly on the field. - Tighten Brand Canonical Document not.anyOf to also block House Redirect's region and note keys. Disambiguation against House Redirect was already clean via id+names requirement, but the deny-list now matches all four other variants' top-level fields explicitly. No example or behavior changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- static/schemas/source/brand.json | 47 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/static/schemas/source/brand.json b/static/schemas/source/brand.json index 549142a4be..1f3fb18783 100644 --- a/static/schemas/source/brand.json +++ b/static/schemas/source/brand.json @@ -14,6 +14,26 @@ "description": "Brand identifier within the house portfolio. Lowercase alphanumeric with underscores. House chooses this ID.", "pattern": "^[a-z0-9_]+$" }, + "brand_ref": { + "type": "object", + "description": "Reference to a pointer brand owned by this house. The child publishes its own canonical brand.json at the named domain. House-side declaration; mutual-assertion trust requires the child's house_domain to reciprocate. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "properties": { + "domain": { + "$ref": "#/definitions/domain", + "description": "Domain where the child's canonical brand.json lives" + }, + "brand_id": { + "$ref": "#/definitions/brand_id", + "description": "Stable brand identifier within the house portfolio" + }, + "managed_by": { + "$ref": "#/definitions/domain", + "description": "Optional domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared, non-trust-bearing — used for grouping and discovery only. Verifiers MUST NOT use this field for trust or authorization decisions. The child does not need to reciprocate this claim." + } + }, + "required": ["domain"], + "additionalProperties": false + }, "localized_name": { "type": "object", "description": "A localized name with BCP 47 locale code key (e.g., 'en_US', 'fr_CA', 'zh_CN') and name value. Bare language codes ('en') are accepted as wildcards for backwards compatibility.", @@ -1390,27 +1410,8 @@ }, "brand_refs": { "type": "array", - "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", - "items": { - "type": "object", - "description": "Reference to a pointer brand owned by this house. The child publishes its own canonical brand.json at the named domain.", - "properties": { - "domain": { - "$ref": "#/definitions/domain", - "description": "Domain where the child's canonical brand.json lives" - }, - "brand_id": { - "$ref": "#/definitions/brand_id", - "description": "Stable brand identifier within the house portfolio" - }, - "managed_by": { - "$ref": "#/definitions/domain", - "description": "Optional domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared, non-trust-bearing — used for grouping and discovery, not validation. The child does not need to reciprocate this claim." - } - }, - "required": ["domain"], - "additionalProperties": false - }, + "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]; brand_id values MUST be unique within brand_refs[]. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "items": { "$ref": "#/definitions/brand_ref" }, "minItems": 1 }, "contact": { "$ref": "#/definitions/contact" }, @@ -1462,7 +1463,9 @@ { "required": ["house"] }, { "required": ["brands"] }, { "required": ["brand_refs"] }, - { "required": ["authorized_operators"] } + { "required": ["authorized_operators"] }, + { "required": ["region"] }, + { "required": ["note"] } ] } }, From 6bd02ac109166abf517536046c3d4db72dd19b78 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 13 May 2026 19:33:26 -0400 Subject: [PATCH 11/13] feat(brand-protocol): ratify distributed brand.json + fold RFC + typed brand-level trademarks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates #3409 / #3533 (RFC) / #3764 (schema cut) / #3910 (fold) / #3909 (typed trademarks) into one normative PR. **Fold (#3910):** RFC content (Motivation, Conformance, strictest-of compliance resolution, Prior art) absorbed into docs/brand-protocol/brand-json.mdx as the normative spec. Proposals subdir and the standalone RFC file deleted. Variant list now reads as five variants from the top; Brand Canonical Document slots in as #5 alongside the other four. **Typed trademarks (#3909):** New #/definitions/trademark extracts the inline house-portfolio shape ({registry, number, mark}) with optional status, license_type, countries. brand definition gains trademarks: Trademark[]. House Portfolio's inline trademarks[] migrated to $ref. House-level + brand- level resolution is union (both lists are valid claims). Existing publishers are unaffected — all additions are optional, the existing four variants are unchanged, and the inline trademark shape continues to validate against the extracted definition. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/distributed-brand-json-impl.md | 17 +- .changeset/distributed-brand-json-rfc.md | 16 - docs/brand-protocol/brand-json.mdx | 245 ++++++----- .../proposals/distributed-brand-json-rfc.mdx | 402 ------------------ static/schemas/source/brand.json | 60 ++- 5 files changed, 203 insertions(+), 537 deletions(-) delete mode 100644 .changeset/distributed-brand-json-rfc.md delete mode 100644 docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx diff --git a/.changeset/distributed-brand-json-impl.md b/.changeset/distributed-brand-json-impl.md index 0e69e1f2da..3de5a53044 100644 --- a/.changeset/distributed-brand-json-impl.md +++ b/.changeset/distributed-brand-json-impl.md @@ -2,17 +2,16 @@ "adcontextprotocol": minor --- -Schema implementation cut for the distributed brand.json RFC ([#3533](https://github.com/adcontextprotocol/adcp/pull/3533)). Additive — existing publishers unchanged. +`brand.json` gains a fifth variant and distributed publishing model. Additive — existing publishers unchanged. -**`brand.json` schema additions:** +**New variant — Brand Canonical Document.** A self-published per-brand document carrying the brand's identity attributes plus optional `house_domain` (string, the domain of the brand's parent house). Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. Excludes top-level house-only fields (`house`, `brands`, `brand_refs`, `authorized_operators`) to disambiguate from House Portfolio. -- New top-level variant: **Brand Canonical Document** — a self-published per-brand document carrying the brand's identity attributes plus optional `house_domain` (string, the domain of the brand's parent house). Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. Excludes top-level house-only fields (`house`, `brands`, `brand_refs`, `authorized_operators`) to disambiguate from House Portfolio. -- **House Portfolio** variant gains `brand_refs[]` — pointer brands whose canonical documents live elsewhere (child-owned data). Each entry: `{ domain, brand_id?, managed_by? }`. `managed_by` (optional) is house-declared, non-trust-bearing — for grouping and discovery, used by holdcos to express agency-network delegation. -- House Portfolio `required` widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` or `brand_refs[]`. -- All new fields reuse existing schema patterns (`#/definitions/domain`, `#/definitions/brand_id`); no new `core/*.json` files added. +**House Portfolio additions.** Gains `brand_refs[]` — pointer brands whose canonical documents live elsewhere (child-owned data). Each entry: `{ domain, brand_id?, managed_by? }`. `managed_by` (optional) is house-declared, non-trust-bearing — for grouping and discovery, used by holdcos to express agency-network delegation. Required widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` or `brand_refs[]`. -**Cross-array invariant** (validator + lint, not JSON Schema expressible): a `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. +**Typed brand-level trademarks.** New `#/definitions/trademark` extracts the inline house-portfolio shape (`{registry, number, mark}`) as a named definition with optional `status`, `license_type`, `countries`. The existing `brand` definition now accepts typed `trademarks: Trademark[]`, enabling both inline `brands[]` entries and self-publishing Brand Canonical Documents to carry their brand-specific marks. House-level `trademarks[]` remains for corporate-level marks; resolution is union. + +**Trust model.** A child Brand Canonical Document declares `house_domain: ""`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Inline children (`brands[]`) are covered by the parent's document authenticity directly. `managed_by` carries no trust weight. Standalone (no `house_domain`) trumps any third-party portfolio claim. Compliance fields resolve strictest-of house and brand. -**Trust model summary** (full text in the RFC): a child brand canonical document declares `house_domain: ""`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Inline children (`brands[]`) are covered by the parent's document authenticity directly. `managed_by` carries no trust weight. +**Cross-array invariant** (validator + lint, not JSON Schema expressible): a `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. -**Status:** for review of the concrete shape only. Not normative until the RFC ratifies. The `brand-json.mdx` reference page carries a "Proposed" callout pointing at #3533 and worked examples (Nike mixed hybrid, WPP with delegation, Converse self-published, Patagonia standalone). +`brand-json.mdx` is the normative spec — Motivation, the five variants, the trust model, the resolution algorithm, Conformance, and prior art (ads.txt / sellers.json) all live there. diff --git a/.changeset/distributed-brand-json-rfc.md b/.changeset/distributed-brand-json-rfc.md deleted file mode 100644 index d6b520e6d0..0000000000 --- a/.changeset/distributed-brand-json-rfc.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Draft RFC for distributed brand.json — propose evolving from monolithic house portfolio (one big document containing inline child brand definitions) to a collection of canonical per-brand documents linked by mutual-assertion pointers. - -The RFC lives at `docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx` (linked from the brand protocol nav under "Proposals"). Tracking discussion is in [#3409](https://github.com/adcontextprotocol/adcp/issues/3409). Not yet normative — needs spec-owner sign-off before any code or schema changes land. - -Key proposed changes (subject to discussion): -- Each brand publishes one canonical brand.json owning its own attributes -- New `house` pointer field for declaring an immediate parent (multi-level chains via recursion) -- New `brand_refs[]` field replacing inline `brands[]` content (pointer-only `{id, domain}`) -- New `house_attributes` block for inheritable house-wide metadata (privacy, compliance, corporate entity) -- Mutual-assertion as the canonical trust primitive — child's `house` must be reciprocated by parent's `brand_refs[]` -- Hosting (static, CDN, brand-agent, AAO-hosted, self-hosted) is independent of the data model and stays an implementation choice - -Migration path defined: 3.x accepts both shapes with deprecation warnings; brand-protocol 2.0 (decoupled from AdCP major) cuts over. diff --git a/docs/brand-protocol/brand-json.mdx b/docs/brand-protocol/brand-json.mdx index e7a39a0c8b..2b57cb3240 100644 --- a/docs/brand-protocol/brand-json.mdx +++ b/docs/brand-protocol/brand-json.mdx @@ -4,15 +4,17 @@ description: "brand.json specification for AdCP. File format, variants (portfoli "og:title": "AdCP — brand.json Specification" --- -The `brand.json` file provides a standardized way for brands to claim their identity and establish discoverable brand information. It supports four mutually exclusive variants to accommodate different use cases. +The `brand.json` file provides a standardized way for brands to claim their identity and establish discoverable brand information. It supports five variants to accommodate different publishing models. `brand.json` is the canonical source of brand identity data. The brand object defined here (logos, colors, tone, tagline) is the single brand definition used across AdCP. Tasks reference brands by domain and brand_id — the system resolves full identity from `brand.json` or the [registry](/docs/registry/index). - -**Proposed (RFC, not yet ratified):** A fifth variant — Brand Canonical Document — and new optional fields on House Portfolio (`brand_refs[]`) and on a brand canonical document (`house_domain`) are under discussion. They let a child brand publish its own canonical document while the house declares ownership via mutual assertion. House delegation (e.g., a holdco letting an agency manage a brand) is expressed via an optional `managed_by` field on the house's `brand_refs[]` entry. The schema in this branch accepts these fields additively for review purposes; the existing four variants are unchanged for current publishers. See the [distributed brand.json RFC](https://github.com/adcontextprotocol/adcp/blob/main/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx) and [#3533](https://github.com/adcontextprotocol/adcp/pull/3533). - +## Motivation + +Brand identity for a holdco could live in **one** brand.json owned by the parent — every child change requires editing the parent's file. If Converse wants to update its logo, someone edits Nike, Inc.'s file. If a holdco runs 100 subsidiary brands, all 100 teams converge on the same monolithic document. Brand teams own their identity, but a monolithic shape forces a single ops choke point at the corporate parent. It also blocks independent brands from publishing at all — a brand listed in someone else's portfolio has no protocol-level path to assert its own canonical data, even when domain control proves it could. + +The spec resolves this by letting a child brand publish its **own** canonical document while the house remains the authority for who is in the family. Inline `brands[]` stays a first-class option (parent owns the data); `brand_refs[]` adds pointer children whose canonical document lives elsewhere (child owns the data). A house mixes freely. The hierarchy is one level deep — only a house declares ownership; a brand cannot itself have children. ## File location @@ -26,7 +28,17 @@ Following [RFC 8615](https://datatracker.ietf.org/doc/html/rfc8615) well-known U ## Variants -The brand.json file supports four mutually exclusive variants: +The brand.json file supports five variants: + +| # | Variant | Use when | +|---|---|---| +| 1 | Authoritative Location Redirect | The canonical document is hosted at another URL | +| 2 | House Redirect | The brand domain rolls up to a larger house | +| 3 | Brand Agent | An MCP agent provides authoritative brand identity | +| 4 | House Portfolio | A house publishes its brands (inline, by pointer, or both) | +| 5 | Brand Canonical Document | A brand self-publishes its own identity (with optional `house_domain` pointer up) | + +Variants 1–3 are mutually exclusive with each other and with variants 4–5. Variants 4 and 5 are distinct shapes — a document is either a portfolio or a brand canonical document, never both — but they're related: a house's portfolio can reference brand canonical documents via `brand_refs[]`. ### 1. Authoritative Location Redirect @@ -126,7 +138,14 @@ When a brand has an agent, the agent is the authoritative source for brand ident ### 4. House Portfolio -Contains the full brand hierarchy with all brands and properties: +The house publishes its brands. A house may carry inline child definitions, pointer references to brands that self-publish, or both: + +- **`brands[]`** — inline child definitions. **Parent owns the data.** Best for sub-brands without their own domain (Nike SB, an internal product line) or sub-brands the holdco wants to manage centrally. +- **`brand_refs[]`** — pointer entries. **Child owns the data** at its own canonical document (variant 5). Best for sub-brands with their own domain that want self-publish authority (Converse, Jordan). + +A given child appears in **exactly one** of the two arrays. At least one of `brands[]` or `brand_refs[]` must be present. + +**Simple house (inline only):** ```json { @@ -150,72 +169,7 @@ Contains the full brand hierarchy with all brands and properties: } ``` -## Distributed extensions (Proposed — RFC #3533) - - -The fields and variant in this section are **under RFC review** ([#3533](https://github.com/adcontextprotocol/adcp/pull/3533), schema cut [#3764](https://github.com/adcontextprotocol/adcp/pull/3764)) and not yet normative. Existing publishers using the four variants above are unaffected. See the [proposal document](https://github.com/adcontextprotocol/adcp/blob/main/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx) for the full motivation, trust model, and migration plan. - - -The proposal lets a child brand publish its **own** canonical document while the house remains the authority for who is in the family. The data model stays one level deep — only houses declare ownership; a child brand cannot itself declare children. - -### 5. Brand Canonical Document (proposed) - -Self-published per-brand document where the brand owns its own identity attributes. Hosted at the brand's own `/.well-known/brand.json` (or via [authoritative location redirect](#1-authoritative-location-redirect)). Optionally declares its house via `house_domain`; for trust, the named house's `brand_refs[]` must reciprocate. Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. - -**With a house — Converse under Nike:** - -```json -{ - "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", - "version": "1.0", - "id": "converse", - "names": [{"en_US": "Converse"}], - "keller_type": "sub_brand", - "house_domain": "nikeinc.com", - "logos": [ - {"url": "https://converse.com/logo.svg", "variant": "primary"} - ], - "tagline": "Sneaker for the streets" -} -``` - -**Standalone — Patagonia:** - -```json -{ - "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", - "version": "1.0", - "id": "patagonia", - "names": [{"en_US": "Patagonia"}], - "keller_type": "independent", - "logos": [ - {"url": "https://patagonia.com/logo.svg", "variant": "primary"} - ], - "tagline": "We're in business to save our home planet." -} -``` - -Use this when a brand has its own domain and wants self-publish authority for its identity (logos, colors, tone, taglines). The brand carries the same identity fields as an inline entry in `brands[]`. - -### House Portfolio additions (proposed) - -A House Portfolio document gains one optional field: - -| Field | Type | Description | -|---|---|---| -| `brand_refs[]` | array of objects | Pointer brands owned by this house. Each entry: `{ domain, brand_id?, managed_by? }`. Mutual-assertion trust: the pointed-to document's `house_domain` must equal this house's domain. | - -A house may use `brands[]`, `brand_refs[]`, or both. **Constraint:** a `brand_id` MUST NOT appear in both arrays. - -The `brand_refs[]` entry shape: - -| Field | Required | Meaning | -|---|---|---| -| `domain` | yes | Where the child's canonical brand.json lives | -| `brand_id` | no | Stable identifier for this brand within the house's portfolio | -| `managed_by` | no | Domain of the entity that operationally manages this brand. **House-declared, non-trust-bearing.** UIs and discovery tools group by `managed_by`; the leaf doesn't have to know about the manager. | - -**Example — mixed hybrid (Nike):** +**Mixed hybrid (inline + pointer):** ```json { @@ -240,7 +194,17 @@ The `brand_refs[]` entry shape: } ``` -**Example — house with delegation (WPP):** +The `brand_refs[]` entry shape: + +| Field | Required | Meaning | +|---|---|---| +| `domain` | yes | Where the child's canonical brand.json lives | +| `brand_id` | no | Stable identifier for this brand within the house's portfolio | +| `managed_by` | no | Domain of the entity that operationally manages this brand. **House-declared, non-trust-bearing.** UIs and discovery tools group by `managed_by`; the leaf doesn't have to know about the manager. | + +**House with delegation (WPP):** + +Holdcos delegate brand management to agency networks. WPP plc owns brands but Ogilvy or BBH actually runs them day-to-day. `managed_by` on a `brand_refs[]` entry captures this as a unilateral declaration by the owning house. The leaf brand doesn't reference the manager. UIs render BBH Sport under BBH for agency views; trust validation walks BBH Sport → WPP only. ```json { @@ -258,17 +222,58 @@ The `brand_refs[]` entry shape: } ``` -`managed_by` is unilaterally declared by the owning house. The leaf at `bbh-sport.com` says only `house_domain: "wpp.com"` — it doesn't reference BBH at all. UIs render BBH Sport under BBH for agency views; trust validation walks BBH Sport → WPP only. WPP can change the manager without updating any leaf. +`managed_by` is **not part of the trust model.** Consumers MUST NOT use it for trust or authorization decisions. See [Conformance](#conformance) below. -### Brand Canonical Document additions (proposed) +### 5. Brand Canonical Document -A self-published brand document gains: +Self-published per-brand document where the brand owns its own identity attributes. Hosted at the brand's own `/.well-known/brand.json` (or via [authoritative location redirect](#1-authoritative-location-redirect)). Optionally declares its house via `house_domain`; for trust, the named house's `brand_refs[]` must reciprocate. Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. -| Field | Type | Description | -|---|---|---| -| `house_domain` | string (domain) | Optional pointer to the corporate house this brand belongs to. The named house's `brand_refs[]` MUST reciprocate for mutual-assertion trust. Single-hop — a brand cannot itself have `brand_refs[]`. Omit for standalone brands. | +**With a house — Converse under Nike:** + +```json +{ + "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", + "version": "1.0", + "id": "converse", + "names": [{"en_US": "Converse"}], + "keller_type": "sub_brand", + "house_domain": "nikeinc.com", + "logos": [ + {"url": "https://converse.com/logo.svg", "variant": "primary"} + ], + "tagline": "Sneaker for the streets" +} +``` -### Mutual-assertion trust model +**Standalone — Patagonia:** + +```json +{ + "$schema": "https://adcontextprotocol.org/schemas/v3/brand.json", + "version": "1.0", + "id": "patagonia", + "names": [{"en_US": "Patagonia"}], + "keller_type": "independent", + "logos": [ + {"url": "https://patagonia.com/logo.svg", "variant": "primary"} + ], + "tagline": "We're in business to save our home planet." +} +``` + +Use this when a brand has its own domain and wants self-publish authority for its identity (logos, colors, tone, taglines). The brand carries the same identity fields as an inline entry in `brands[]`. + +Top-level fields: + +| Field | Type | Required | Description | +|---|---|---|---| +| `id` | string | Yes | Brand identifier (lowercase alphanumeric with underscores) | +| `names` | array | Yes | Localized names (see [Brand definition](#brand-definition)) | +| `house_domain` | string (domain) | No | Pointer to the corporate house this brand belongs to. The named house's `brand_refs[]` MUST reciprocate for mutual-assertion trust. Single-hop — a brand cannot itself have `brand_refs[]`. Omit for standalone brands. | + +All other brand-identity fields (`logos`, `colors`, `tone`, `tagline`, `visual_guidelines`, etc. — see [Brand definition](#brand-definition)) apply. + +## Mutual-assertion trust model Trust between a house and a pointer child requires **both sides** to reciprocate: @@ -290,21 +295,20 @@ There is no inheritance/override block. Each consumer-side question has a single |---|---| | Brand identity (logos, colors, tone, tagline, voice) | Read from brand's own canonical document. For inline children, read from the parent's `brands[]` entry. | | Brand contact, properties, industries | Read from brand's own canonical document. | -| `data_subject_contestation` | Brand-level if present; otherwise walk `house_domain` → `house.data_subject_contestation`. (Existing resolution rule, unchanged.) | | Trademarks, authorized_operators, corporate contact | Read from the house's brand.json. | +| `data_subject_contestation`, compliance policies, regulated-category flags | **Strictest-of** house-level and brand-level. See below. | | Mutual-assertion verification | Fetch both brand and house, compare `house_domain` ↔ `brand_refs[]`. | -If a brand wants stricter constraints than its house (e.g., extra audience exclusions), it publishes those constraints at the brand level. House and brand constraints both apply; consumers respect the union. +**Identity fields** (`name`, `names`, `logos`, `colors`, `fonts`, `tone`, `voice`, `tagline`, `visual_guidelines`, `avatar`): brand-level value is authoritative; the house value is not consulted. Brand teams own brand identity. -### Resolution algorithm +**Compliance and governance fields**: the resolved value is the **strictest of** the house-level and brand-level values. A brand cannot weaken a house's governance assertions; it can only add stricter constraints. Applies to: -For a brand at `domain.com`: +- `data_subject_contestation` — both contacts SHOULD be presented to data subjects (consumers may use either; brand-level does NOT replace house-level) +- `compliance_policies`, audience exclusions, regulated-category flags — resolved as union (more restrictions wins) -1. Fetch `domain.com/.well-known/brand.json`. If it's a redirect variant ([authoritative_location](#1-authoritative-location-redirect) or [House Redirect](#2-house-redirect)), follow it. -2. The result is either an inline brand entry (the document is a House Portfolio containing this brand in `brands[]`) or a Brand Canonical Document (the document IS the brand). -3. If it's a Brand Canonical Document with `house_domain`, fetch the house's `brand.json` (following redirects) to verify reciprocation. Read corporate-level fields (e.g., `data_subject_contestation`) from the house if not present on the brand. +This is the load-bearing reason holdcos publish corporate-level governance: brand teams should not be able to soften compliance by self-publishing. Identity is brand-wins because that's what brand teams own; governance is strictest-of because that's how legal/compliance regimes actually work. The schema does not encode this rule; it's a resolution-layer semantic. -Single-hop. No recursive walking. +See [Resolution algorithm](#resolution-algorithm) below for the full crawler procedure. ### Acquisitions and reorganizations @@ -344,6 +348,7 @@ Each brand in the `brands` array: | `tone` | object | No | Brand voice and messaging guidelines (`voice`, `attributes`, `dos`, `donts`) | | `tagline` | string | No | Brand tagline or slogan | | `visual_guidelines` | object | No | Structured visual rules for generative creative systems | +| `trademarks` | array | No | Registered trademarks owned or licensed by this brand (`registry`, `number`, `mark`, optional `status`, `license_type`, `countries`). See [Trademarks](#trademarks). | ### Names Array @@ -867,6 +872,36 @@ Visual prohibitions and guardrails — the visual equivalent of `tone.donts`. Th } ``` +## Trademarks + +Registered trademarks may appear at the **house level** (corporate marks — e.g., NIKE owned by Nike, Inc.) or at the **brand level** (brand-specific marks — e.g., CONVERSE owned by Converse). Both arrays are valid claims; resolution between them is **union**. + +```json +{ + "trademarks": [ + { + "registry": "USPTO", + "number": "12345678", + "mark": "CONVERSE", + "status": "active", + "license_type": "owned", + "countries": ["US"] + } + ] +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `registry` | string | Yes | Trademark registry (e.g., `USPTO`, `EUIPO`, `JPO`, `CNIPA`) | +| `number` | string | Yes | Registration number as issued by the registry | +| `mark` | string | Yes | The registered mark as published | +| `status` | enum | No | `active`, `pending`, `abandoned`, `cancelled`, `expired`. Omit for active marks if status tracking is not maintained. | +| `license_type` | enum | No | `owned` (default), `licensed_in`, `licensed_out` | +| `countries` | array | No | ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit. | + +Holdcos with cross-jurisdiction conflicts (USPTO `CONVERSE` vs EUIPO `CONVERSE` under different owners) should publish each registration as a separate entry and use `countries` to scope where the mark applies. + ## Property definition Properties are digital touchpoints associated with brands: @@ -909,16 +944,16 @@ This is the AdCP equivalent of `sellers.json` — the operator's public declarat To resolve a domain to a canonical brand: -1. Fetch `https://{domain}/.well-known/brand.json` +1. Fetch `https://{domain}/.well-known/brand.json`. 2. Check variant: - - **authoritative_location**: Fetch from that URL, continue from step 2 - - **house** (string): Fetch from house domain, continue from step 2 - - **brand_agent**: Return agent URL (agent provides brand info) - - **house** (object) + **brands**: Search for domain in properties -3. For house portfolio, find the brand whose properties contain the query domain -4. Return canonical brand information + - **authoritative_location**: fetch from that URL, continue from step 2. + - **house** (string): fetch from house domain, continue from step 2. + - **brand_agent**: return agent URL — the agent is authoritative. + - **House Portfolio** (`house` object + `brands[]` and/or `brand_refs[]`): for an inline child, find the brand whose `properties[]` or `id` matches the query; for a pointer child, follow `brand_refs[].domain` and resolve recursively (single hop). + - **Brand Canonical Document** (`id` + `names` at top level): the document is the brand. If `house_domain` is present, fetch the house's `brand.json` to verify reciprocation in its `brand_refs[]` (mutual assertion). Read corporate-level fields (e.g., `data_subject_contestation`) from the house if not present on the brand. +3. Return canonical brand information. -Maximum redirect depth: 3 hops. +Maximum redirect depth: 3 hops. The brand → house lookup is single-hop (no recursive parent walks). See [Mutual-assertion trust model](#mutual-assertion-trust-model) for trust semantics. ## Complete examples @@ -1077,6 +1112,24 @@ Recommended cache TTLs: - Redirect files: 24 hours - Failed lookups: 1 hour +## Conformance + +These invariants MUST be enforced by validators and crawlers; JSON Schema cannot express them directly: + +- **`brand_id` cross-array uniqueness.** A given `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. Publisher must choose one. +- **`brand_id` within-array uniqueness.** A given `brand_id` MUST be unique within `brands[]` and unique within `brand_refs[]` of the same house. +- **Mutual-assertion as the trust primitive.** Consumers MUST NOT extend governance trust (auto-provisioning, member-feature inheritance, billable seat inclusion, inherited compliance fields) through one-sided claims. Mutual assertion (child's `house_domain` matches a `brand_refs[]` entry on the named house) is the canonical trust edge. +- **`managed_by` is not a trust signal.** Consumers MUST NOT use `managed_by` for trust or authorization decisions. UIs SHOULD render it as a unilateral house claim ("WPP says BBH manages this") and SHOULD NOT aggregate cross-house ("BBH's portfolio") without independent confirmation from the named manager. +- **Standalone trumps third-party claim.** A Brand Canonical Document with no `house_domain` is standalone, regardless of any third-party house's `brand_refs[]` claim about it. +- **Compliance fields strictest-of.** For governance fields (`data_subject_contestation`, `compliance_policies`, audience exclusions, regulated-category flags), the resolved value is the union/strictest of house-level and brand-level. Brand-level publishers MUST NOT rely on weakening house-level assertions. +- **180-day TTL.** Mutual-assertion edges that have not been re-validated within 180 days SHOULD be treated as one-sided regardless of last-known state. + +## Prior art + +The mutual-assertion trust primitive mirrors the IAB Tech Lab's [`ads.txt`](https://iabtechlab.com/ads-txt/) and [`sellers.json`](https://iabtechlab.com/sellers-json/) reciprocal-publication model: a buyer is trusted as a seller's reseller iff both sides publish the relationship at well-known URLs. Same trust shape, same non-cryptographic "who claims what about whom" verification, same fallback to one-sided / unverified for partial publication. A deployed, durable industry pattern. + +Within AdCP, the [provenance verifier contract](https://github.com/adcontextprotocol/adcp/pull/3468) (seller-publishes / buyer-represents / seller-confirms) uses the same family of construction for a different field family. + ## Best practices 1. **Start simple**: Begin with minimal brand.json and add complexity as needed diff --git a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx b/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx deleted file mode 100644 index 5e32cc0212..0000000000 --- a/docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx +++ /dev/null @@ -1,402 +0,0 @@ ---- -title: "RFC — Distributed brand.json" -description: "Proposal to evolve brand.json so brands can publish their own canonical documents alongside parent-owned inline definitions, with a flat house-to-brands trust model and house-declared management delegation." -"og:title": "AdCP — Distributed brand.json RFC" ---- - - -**RFC — discussion in progress.** This is a proposal under discussion in [issue #3409](https://github.com/adcontextprotocol/adcp/issues/3409) and PR [#3533](https://github.com/adcontextprotocol/adcp/pull/3533). Schema implementation cut for review at PR [#3764](https://github.com/adcontextprotocol/adcp/pull/3764). Not yet ratified. The current normative spec lives at [brand.json](/docs/brand-protocol/brand-json). - - -## Status - -| Field | Value | -| --- | --- | -| Author | bokelley | -| Status | Proposed | -| Tracking | [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) | -| Target | brand-protocol 1.1 (additive) | -| Affects | `static/schemas/source/brand.json`, `docs/brand-protocol/brand-json.mdx` | - -## Summary - -Evolve brand.json so brands can publish their own canonical documents alongside the existing inline-children shape. The house remains the single authority for who is in the family; children pick the publishing model that fits. - -A house's brand.json keeps `brands[]` for inline children (parent-owned data, the current shape) **and** adds `brand_refs[]` for pointer children whose canonical document lives elsewhere (child-owned data). A child's canonical document declares its house via `house_domain`. Trust between the house and a pointer child requires **mutual assertion** — both sides must reciprocate. - -The hierarchy is **flat**: only the house declares ownership. There is no recursive parent chain. Operational delegation (e.g., a holdco letting an agency manage a brand) is expressed via `managed_by` on the house's `brand_refs[]` entry — house-declared, non-trust-bearing, for grouping and discovery only. - -Acquisitions and reorganizations are handled by the existing redirect variants (House Redirect, Authoritative Location Redirect) — no new primitive needed. - -## Motivation - -### The pain point - -Today brand identity for a holdco lives in **one** brand.json owned by the parent. Every change to any brand requires an edit to the parent's file. - -If Converse wants to update its logo in AdCP, someone has to edit Nike, Inc.'s brand.json. If Jordan launches a new tagline, same file. If a holdco runs 100 subsidiary brands, all 100 brand teams converge on the same monolithic document. This is a structural mismatch: brand teams own their identity, but the protocol forces a single ops choke point at the corporate parent. - -The same shape blocks independent brands from publishing at all — a brand listed in someone else's portfolio has no protocol-level path to assert its own canonical data, even when domain control proves it could. - -### Secondary problems - -1. **Caching is all-or-nothing.** A 100-brand monolith re-fetches in full on any change. -2. **No leaf-side authority.** A brand listed in a parent's portfolio can't override stale parent-published data, even when it controls its own domain. -3. **Trust is implicit.** A brand appearing in someone else's portfolio is "owned" by that house with no verification primitive. No way to distinguish "Nike asserts Converse is theirs and Converse agrees" from "anyone could publish a brand.json claiming Converse is theirs." -4. **No expression for delegated management.** Holdcos like WPP delegate brand management to agency networks (BBH, Ogilvy). The protocol has no place for "WPP owns this brand, BBH manages it day-to-day." - -### Constraints we want to preserve - -- **House remains the authority for ownership.** The protocol shouldn't let arbitrary brands claim themselves into someone's portfolio. Only the house decides who is in the family. -- **No forced migration.** Existing portfolio publishers should keep working. Brands that don't want or need self-publish should stay simple. -- **Hosting is independent of data model.** Static, CDN, brand-agent service, AAO-hosted, self-hosted — all should work. -- **No new primitives unless necessary.** The schema already has redirects, references, and a four-variant top-level shape. Reuse what's there. - -## Proposal - -### Hybrid: inline children **and** pointer children - -A house's canonical brand.json may carry both: - -- **`brands[]`** — inline child definitions. **Parent owns the data.** Same shape as today. Best for sub-brands without their own domain (Nike SB, an internal product line) or sub-brands the holdco wants to manage centrally. -- **`brand_refs[]`** — pointer entries. **Child owns the data** at its own canonical document. Best for sub-brands with their own domain that want self-publish authority (Converse, Jordan). - -A given child appears in **exactly one** of the two. A house can mix freely. - -```json -// nikeinc.com — house, mixes inline and pointer children -{ - "house": { "domain": "nikeinc.com", "name": "Nike, Inc.", "architecture": "hybrid" }, - "brands": [ - { - "id": "nike_sb", - "names": [{"en_US": "Nike SB"}], - "keller_type": "sub_brand", - "logos": [/* parent-managed inline */] - } - ], - "brand_refs": [ - { "domain": "converse.com", "brand_id": "converse" }, - { "domain": "jordan.com", "brand_id": "jordan" } - ] -} -``` - -```json -// converse.com — pointer child, owns its own data -{ - "version": "1.0", - "id": "converse", - "name": "Converse", - "names": [{"en_US": "Converse"}], - "keller_type": "sub_brand", - "house_domain": "nikeinc.com", - "logos": [...], - "colors": {...}, - "tone": {...}, - "tagline": "Sneaker for the streets" -} -``` - -### Flat hierarchy — only the house adds brands - -Only the **house** has `brand_refs[]` / `brands[]`. A brand cannot list its own children. The hierarchy is one level deep. - -A multi-tier real-world arrangement (e.g. "StreetKix is run by Converse's team but legally owned by Nike, Inc.") collapses for the data model: StreetKix's `house_domain` is `nikeinc.com` directly, and StreetKix appears in nikeinc.com's `brand_refs[]`. Operational delegation between Converse and StreetKix is expressed via `managed_by` on Nike's `brand_refs[]` entry, not as a separate hierarchical layer (see next section). - -This dramatically simplifies trust: one hop, single authority, no recursive walks. - -### `house_domain` on a child (string) - -Children declare their parent house via a `house_domain` field — a plain string, the domain of the house's brand.json. Reuses the same domain pattern as the existing House Redirect variant (which already uses `house: ""` as a string). - -```json -"house_domain": "nikeinc.com" -``` - -A standalone brand (no house — Patagonia, Liquid Death) omits `house_domain`. The brand canonical document is still valid; it just declares no parent relationship. If the brand is later acquired, it adds `house_domain` and the new house adds the brand to `brand_refs[]`. No new variant needed. - -### `brand_refs[]` shape - -```json -"brand_refs": [ - { - "domain": "converse.com", - "brand_id": "converse", - "managed_by": "ogilvy.com" - } -] -``` - -| Field | Required | Meaning | -| --- | --- | --- | -| `domain` | yes | Where the child's canonical brand.json lives | -| `brand_id` | optional | Stable identifier for this brand within the house's portfolio | -| `managed_by` | optional | Domain of the entity that operationally manages this brand. **House-declared, non-trust-bearing.** UIs and discovery tools group by `managed_by`; trust still flows child → house only. | - -### Delegation via `managed_by` - -Real holdcos delegate brand management to agency networks. WPP plc owns brands but Ogilvy or BBH actually runs them day-to-day. - -`managed_by` on a `brand_refs[]` entry captures this **as a unilateral declaration by the owning house**. The leaf brand doesn't need to know about the manager. The manager doesn't need to publish anything to confirm. UIs render BBH Sport under BBH for agency views; trust validation walks BBH Sport → WPP only. - -```json -// wpp.com -{ - "house": { "domain": "wpp.com", "name": "WPP plc" }, - "brand_refs": [ - { "domain": "bbh-sport.com", "brand_id": "bbh_sport", - "managed_by": "bbh.com" }, - { "domain": "ogilvy-toyota.com", "brand_id": "ogilvy_toyota", - "managed_by": "ogilvy.com" }, - { "domain": "wpp-direct.com", "brand_id": "wpp_direct" } - ] -} -``` - -```json -// bbh-sport.com — leaf doesn't reference BBH, just WPP -{ - "id": "bbh_sport", - "names": [{"en_US": "BBH Sport"}], - "keller_type": "endorsed", - "house_domain": "wpp.com" -} -``` - -If WPP changes the manager (BBH → directly managed → Ogilvy), only WPP's brand_refs[] entry updates. The leaf doesn't churn. If BBH disagrees with the management claim, that's a legal/business matter, not a protocol concern. - -**Normative:** `managed_by` MUST NOT be used for trust or authorization decisions. Verifiers MUST ignore it when evaluating mutual assertion, governance propagation, billable inclusion, or operator authorization. UIs SHOULD render it as a unilateral house claim (e.g., "WPP says BBH manages this"), and consumers SHOULD NOT aggregate cross-house ("BBH's portfolio") without independent confirmation from the named manager. - -### Hosting is independent - -The data model says nothing about where bytes live. For pointer children, the canonical document is served at the pointed-to domain via the existing discovery contract (`domain.com/.well-known/brand.json`, with optional `authoritative_location` indirection). The hosting party is an implementation choice: - -| Pattern | How | -| --- | --- | -| Brand self-hosts | `domain.com/.well-known/brand.json` is the canonical document | -| AAO-hosted | `domain.com/.well-known/brand.json` is a stub with `authoritative_location: "https://agenticadvertising.org/brands/${domain}/brand.json"` | -| Parent's brand-agent or static server | Stub at brand's domain points at the parent's canonical URL | -| CDN-fronted | Either above with caching infrastructure in front | -| Mixed within a single house | Some children inline (`brands[]`), some pointer-self-hosted, some pointer-AAO-hosted, etc. | - -The crawler doesn't care about hosting. It follows the discovery contract. The brand-agent service spec ([building a brand agent](/docs/brand-protocol/building-a-brand-agent)) is a separate, complementary concept — a brand-agent can serve brand.json content, but trust still flows from the static document's authenticity. - -### Resolution: where does a consumer read each field? - -The spec already separates per-brand fields from house-level fields. The new shape doesn't introduce inheritance/override semantics. Each consumer-side question has a single answer: - -| Question | Resolution | -| --- | --- | -| Brand identity (logos, colors, tone, tagline, voice) | Read from brand's own canonical document. For inline children, read from the parent's `brands[]` entry. | -| Brand contact, properties, industries | Read from brand's own canonical document. | -| `data_subject_contestation` | Brand-level if present; otherwise walk `house_domain` → `house.data_subject_contestation`. (Existing resolution rule, unchanged.) | -| Trademarks, authorized_operators, corporate contact | Read from the house's brand.json. | -| Mutual-assertion verification | Fetch both brand and house, compare `house_domain` ↔ `brand_refs[]`. | - -There is no `house_attributes` block, no `house_attributes_overrides`, no `house_attributes_locked`. Houses publish their corporate-level fields in the existing house schema; brands publish their brand-level fields in their own canonical document; consumers walk `house_domain` to read corporate fields when needed. - -### Compliance fields: strictest-of resolution - -For **identity** fields (`name`, `names`, `logos`, `colors`, `fonts`, `tone`, `voice`, `tagline`, `visual_guidelines`, `avatar`), the brand-level value is authoritative; the house value is not consulted. - -For **compliance and governance** fields, the resolved value is the **strictest of** the house-level and brand-level values. A brand cannot weaken a house's governance assertions; it can only add stricter constraints. This applies to: - -- `data_subject_contestation` — both contacts SHOULD be presented to data subjects (consumers may use either; brand-level does NOT replace house-level) -- `compliance_policies`, audience exclusions, regulated-category flags — resolved as union (more restrictions wins) -- Future regulated-category fields as the spec formalizes them - -This is the load-bearing reason holdcos publish corporate-level governance: brand teams should not be able to soften compliance by self-publishing. Identity fields are brand-wins because that's what brand teams own; governance fields are strictest-of because that's how legal/compliance regimes actually work. - -The schema does not encode this rule; it's a resolution-layer semantic in the spec text. Validators and crawlers implement it; publishers may rely on it. - -## Trust model - -Single-hop. Five layers, increasing in strength. - -### 1. Document authenticity (baseline) - -A canonical brand.json document is authentic if and only if it is served via TLS by infrastructure the consumer can verify the brand controls. Two paths: - -- **Direct**: served at `domain.com/.well-known/brand.json` over TLS valid for `domain.com`. Standard web-PKI. -- **Indirect via `authoritative_location`**: stub at `domain.com/.well-known/brand.json` (proves domain control) points at a separate URL where the canonical document is served. - -This layer establishes "I trust this is the brand's own document." Nothing more. - -### 2. Self-claims (always trusted for self-attributes) - -A brand's own canonical document is authoritative for **its own identity attributes**: `name`, `logos`, `colors`, `voice`, etc. Domain control = self-identity authority. No cross-checking needed. - -For inline children in `brands[]`, the parent's document authenticity covers them — the parent is the authority for the child's data, by definition. - -### 3. One-sided relationship claims (metadata only — NOT trust) - -A pointer child says `house_domain: "nikeinc.com"`. nikeinc.com's canonical document does NOT include the child's domain in `brand_refs[]`. - -This is **supportive metadata**, not a trust edge. Surface it in UIs as "claimed but unverified." Do not extend governance trust through it. Auto-provisioning, member-feature inheritance, billable seat inclusion: **NO**. - -### 4. Mutual assertion (the trust edge) - -Pointer child's document says `house_domain: "nikeinc.com"`. nikeinc.com's `brand_refs[]` includes `{ domain: ".com", brand_id: ... }`. Both verifiable in two fetches. **This is the trust edge.** Auto-provisioning, member-feature inheritance, governance fallback: YES. - -For inline children in `brands[]`, mutual assertion is implicit — the house authored the entry; no separate child claim exists. Parent's TLS = the assertion. - -### 5. Cryptographic signing / brand-agent endorsement (future, optional) - -Out of scope for v1. Future extensions: a house's brand-agent could sign attestations about its hosted brands; verifiable credentials; etc. Not needed to ship the data model. - -### Standalone brands - -A brand canonical document MAY omit `house_domain`. Such a document is a **standalone brand** — no house relationship, no mutual-assertion edge to evaluate. Even if some other house's `brand_refs[]` lists this brand, the absence of `house_domain` on the brand's own document is dispositive: the brand is treated as standalone, the third-party claim is one-sided supportive metadata only, and no governance/inheritance trust is extended. - -### Conflict resolution - -| Scenario | Resolution | -| --- | --- | -| Pointer child claims `house_domain: A`, A's `brand_refs[]` does not include child | One-sided. Untrusted. UI: "claimed, unverified." | -| Pointer child claims `house_domain: A`, A's `brand_refs[]` includes child | Mutual. Trusted edge. | -| A's `brand_refs[]` includes child, child's document has no `house_domain` | One-sided in the other direction. A's claim is supportive metadata. Child is treated as having no house. | -| Two houses (A and B) both list child in `brand_refs[]`; child's `house_domain` is A | A wins. B's claim is visible-but-unverified. | -| Two houses both list child; child has no `house_domain` declaration | Neither is trusted. UI shows both as competing unverified claims. | -| A child appears in both `brands[]` and `brand_refs[]` of the same house | Validation error. Publisher must choose one. | -| Last-validated > 180 days | Edge ages out. Treat as one-sided regardless of prior state. | - -The 180-day TTL is already implemented in the AAO crawler (`server/src/db/org-filters.ts`). The proposed spec formalizes it. - -`managed_by` is **not part of the trust model**. It's a unilateral declaration by the owning house, used for grouping and discovery. A misuse ("WPP says BBH manages 100 brands but BBH never agreed") doesn't compromise trust because `managed_by` carries no governance weight. - -## Acquisitions and reorganizations - -Existing brand.json variants handle M&A natively. No new primitive needed. - -**Pre-deal:** - -- `dentsu.com/.well-known/brand.json` is a House Portfolio. -- Dentsu's brands' canonical docs say `house_domain: "dentsu.com"`. - -**Deal closes:** - -- Dentsu's `brand.json` is replaced with a House Redirect → `{ "house": "wpp.com" }` (or an Authoritative Location Redirect to WPP's hosted file). -- WPP's brand.json adds the acquired brands to `brand_refs[]` with `managed_by: "dentsu.com"` (ops continuity). - -**Post-deal:** - -- Leaves still pointing at `house_domain: "dentsu.com"` resolve through the redirect to WPP's portfolio. Mutual-assertion holds via the redirect chain. -- Leaves don't have to update urgently. Over time they migrate to `house_domain: "wpp.com"` for clarity, but it's not a trust requirement. - -The existing redirect machinery does the work. The spec just needs to call out, in the resolution algorithm, that **`house_domain` resolution follows the same discovery contract as any brand.json fetch — including redirect variants.** - -## Migration - -Pull-based, not push-based. Existing publishers don't have to do anything until a child wants to self-publish. - -### 3.x (additive) - -- `brand_refs[]` is a new optional field alongside `brands[]`. -- `house_domain` is a new optional field on a brand canonical document. -- `managed_by` is a new optional field on `brand_refs[]` entries. -- A house may use any combination. Existing portfolio publishers continue to work unchanged. -- No deprecation of `brands[]`. Inline remains a first-class option. - -### A child's path to self-publish - -1. Child stands up a canonical document at its own domain (or `authoritative_location` target). -2. Child's document declares `house_domain: ""`. -3. House removes the child's entry from `brands[]` and adds `{ domain, brand_id, managed_by? }` to `brand_refs[]`. -4. Crawler picks up the mutual assertion on next refresh (≤ 180-day TTL). - -No coordination required at the spec/version level — both shapes are valid simultaneously. - -### Migration helpers - -- AAO publishes a one-shot CLI: given a legacy portfolio brand.json, generate a per-child canonical document at the right URL and rewrite the parent's `brands[]` entry as a `brand_refs[]` pointer. -- AAO's hosted brand.json service offers a "promote child to self-publish" workflow that does the migration automatically. - -## AAO API ergonomic note (non-normative) - -The protocol's pointer-only `brand_refs[]` means consumers must follow each pointer to fetch a child's data. This is correct for the protocol — it gives an unambiguous source-of-truth contract. - -For consumers who want a one-shot ergonomic view, AAO's API may offer a server-side merge: - -```http -GET /api/brands/nikeinc.com/family -``` - -Returns a denormalized tree of the house + all (mutually-asserted) brand children in one response, with each pointer child's authoritative data merged in. Inline children appear as-is. This is a convenience layer over the protocol; **it does not change the protocol**. - -Other consumers (registry crawlers, AdCP agents, validators) follow the pointer-only contract and resolve lazily. - -## Implementation notes - -### Schema deltas (`static/schemas/source/brand.json`) - -- House Portfolio variant (existing) gains optional `brand_refs[]` field. Each entry is `{ domain, brand_id?, managed_by? }`. Required field on `required`: widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` / `brand_refs[]`. -- New top-level variant: **Brand Canonical Document**. Composes the existing brand definition via `allOf` plus optional `house_domain` (string), `$schema`, `version`, `last_updated`. Excludes top-level `house`, `brands`, `brand_refs`, `authorized_operators` to disambiguate from House Portfolio. -- No new schema files. `house_domain` and `managed_by` reuse the existing `domain` definition (string with the standard pattern). -- No `house_attributes` / `house_attributes_overrides` / `house_attributes_locked` blocks. Houses publish corporate-level fields in the existing house schema (`data_subject_contestation`, `trademarks`, `contact`, `authorized_operators`). - -### Cross-array invariant (validator + lint) - -A `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. JSON Schema cannot easily express this; the spec mandates it and validators/lint enforce it. - -### Crawler resolution algorithm (single hop) - -``` -resolve(domain): - doc = fetch(domain).follow_redirects() # follows authoritative_location and House Redirect - result = doc.identity_attributes - if doc has house_domain: - house = fetch(doc.house_domain).follow_redirects() - if house.brand_refs contains domain: - result.house = house (mutually_asserted: true) - else: - result.house_claim = house (mutually_asserted: false) # surface as metadata - return result -``` - -Single hop. No recursion, no max-depth, no cycle protection needed. - -### Validator behavior - -- Reject a brand canonical document that has top-level `house`, `brands`, `brand_refs`, or `authorized_operators` — those are house-only fields. -- Reject a brand_id appearing in both `brands[]` and `brand_refs[]` of the same house. -- Warn when a `house_domain` claim is not mutually-asserted by the named house (advisory only — single-sided claims are allowed by spec, just not trusted). - -## Conformance - -These invariants MUST be enforced by validators and crawlers; JSON Schema cannot express them directly: - -- **`brand_id` cross-array uniqueness.** A given `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. Publisher must choose one. -- **`brand_id` within-array uniqueness.** A given `brand_id` MUST be unique within `brands[]` and unique within `brand_refs[]` of the same house. -- **Mutual-assertion as the trust primitive.** Consumers MUST NOT extend governance trust (auto-provisioning, member-feature inheritance, billable seat inclusion, inherited compliance fields) through one-sided claims. Mutual assertion (child's `house_domain` matches a `brand_refs[]` entry on the named house) is the canonical trust edge. -- **`managed_by` not a trust signal.** Consumers MUST NOT use `managed_by` for trust or authorization decisions. UIs SHOULD render it as a unilateral house claim. -- **Standalone trumps third-party claim.** A brand canonical document with no `house_domain` is standalone, regardless of any third-party house's `brand_refs[]` claim about it. -- **Compliance fields strictest-of.** For governance fields (`data_subject_contestation`, `compliance_policies`, audience exclusions, regulated-category flags), the resolved value is the union/strictest of house-level and brand-level. Brand-level publishers MUST NOT rely on weakening house-level assertions. -- **180-day TTL.** Mutual-assertion edges that have not been re-validated within 180 days SHOULD be treated as one-sided regardless of last-known state. - -## Open questions - -These need spec-owner / discussion input: - -1. **Should `house_domain` on a brand canonical document be required or optional?** Optional in this proposal — supports standalone brands (Patagonia) without requiring a degenerate "house of one." Vote: keep optional. -2. **Where do the inline `brand_refs[]` entry fields belong long-term?** Currently inline in brand.json. If reused elsewhere, refactor into a shared `core/` schema. Vote: inline for v1, refactor only if a second consumer emerges. -3. **Should the spec mandate mutual-assertion for trust, or leave it to consumers?** Mandating it means every implementation has the same trust model. Vote: mandate as the canonical trust primitive; spec text says consumers MAY apply additional checks (signing, brand-agent endorsement) but MUST NOT trust one-sided claims as the trust edge. -4. **Migration timeline.** Both shapes coexist indefinitely; no forced cutover. If a deprecation is ever appropriate, that's a future RFC. -5. **`search_brands` trust state surfacing.** The crawler computes `mutually_asserted: true|false` per brand. PR [#3486](https://github.com/adcontextprotocol/adcp/pull/3486) (search_brands discovery verb) doesn't currently surface this in response stubs. Follow-up: extend `SearchBrandResult` to carry the trust signal so DSPs can act on it. Out of scope for this RFC but explicitly tracked. - -## Prior art - -The mutual-assertion trust primitive proposed here mirrors the IAB Tech Lab's `ads.txt` / `sellers.json` reciprocal-publication model: a buyer is trusted as a seller's reseller iff both sides publish the relationship at well-known URLs. Same trust shape, same non-cryptographic "who claims what about whom" verification, same fallback to one-sided / unverified for partial publication. It's a deployed, durable industry pattern. - -Within AdCP, PR [#3468](https://github.com/adcontextprotocol/adcp/pull/3468) (provenance verifier contract — seller-publishes / buyer-represents / seller-confirms) uses the same family of construction for a different field family. - -## References - -- [#3409](https://github.com/adcontextprotocol/adcp/issues/3409) — tracking issue -- [#3533](https://github.com/adcontextprotocol/adcp/pull/3533) — this RFC PR -- [#3764](https://github.com/adcontextprotocol/adcp/pull/3764) — schema implementation cut -- [brand.json](/docs/brand-protocol/brand-json) — current normative spec -- [Building a brand agent](/docs/brand-protocol/building-a-brand-agent) — the separate brand-agent MCP service spec -- [#3378](https://github.com/adcontextprotocol/adcp/pull/3378) — brand-hierarchy auto-link (the trust model implemented in AAO crawler today) -- [#3486](https://github.com/adcontextprotocol/adcp/pull/3486) — search_brands discovery verb (cross-cutting follow-up for trust-state surfacing) -- [IAB Tech Lab ads.txt / sellers.json](https://iabtechlab.com/ads-txt/) — prior art for mutual-assertion trust at well-known URLs diff --git a/static/schemas/source/brand.json b/static/schemas/source/brand.json index 1f3fb18783..032f18f52d 100644 --- a/static/schemas/source/brand.json +++ b/static/schemas/source/brand.json @@ -14,9 +14,44 @@ "description": "Brand identifier within the house portfolio. Lowercase alphanumeric with underscores. House chooses this ID.", "pattern": "^[a-z0-9_]+$" }, + "trademark": { + "type": "object", + "description": "A registered trademark. May appear at house level (corporate marks, e.g., 'NIKE' owned by Nike, Inc.) or at brand level (brand-specific marks, e.g., 'CONVERSE' owned by Converse). Resolution between house- and brand-level trademarks is union — both lists are valid claims about marks the publisher controls.", + "properties": { + "registry": { + "type": "string", + "description": "Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')" + }, + "number": { + "type": "string", + "description": "Registration number as issued by the registry" + }, + "mark": { + "type": "string", + "description": "The registered mark as published" + }, + "status": { + "type": "string", + "enum": ["active", "pending", "abandoned", "cancelled", "expired"], + "description": "Registration status. Omit for active marks if status tracking is not maintained." + }, + "license_type": { + "type": "string", + "enum": ["owned", "licensed_in", "licensed_out"], + "description": "Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted." + }, + "countries": { + "type": "array", + "items": { "type": "string", "minLength": 2, "maxLength": 2 }, + "description": "ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit." + } + }, + "required": ["registry", "number", "mark"], + "additionalProperties": true + }, "brand_ref": { "type": "object", - "description": "Reference to a pointer brand owned by this house. The child publishes its own canonical brand.json at the named domain. House-side declaration; mutual-assertion trust requires the child's house_domain to reciprocate. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "description": "Reference to a pointer brand owned by this house. The child publishes its own canonical brand.json at the named domain. House-side declaration; mutual-assertion trust requires the child's house_domain to reciprocate. See docs/brand-protocol/brand-json.mdx", "properties": { "domain": { "$ref": "#/definitions/domain", @@ -448,6 +483,11 @@ }, "description": "Legal disclaimers for creatives" }, + "trademarks": { + "type": "array", + "items": { "$ref": "#/definitions/trademark" }, + "description": "Brand-level registered trademarks. Use for marks the brand owns or controls (e.g., a sub-brand's own marks distinct from the corporate parent). House-level trademarks live on the house object; resolution between the two is union — both lists are valid claims." + }, "voice_synthesis": { "type": "object", "description": "TTS voice synthesis configuration for AI-generated audio", @@ -1397,7 +1437,7 @@ { "type": "object", "title": "House Portfolio", - "description": "Full house/brand portfolio with hierarchy, creative assets, and properties. May carry inline brands (parent-owned, brands[]) and/or pointer brands (child-owned canonical documents, brand_refs[]). At least one of brands[] or brand_refs[] is required. A brand_id MUST NOT appear in both. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "description": "Full house/brand portfolio with hierarchy, creative assets, and properties. May carry inline brands (parent-owned, brands[]) and/or pointer brands (child-owned canonical documents, brand_refs[]). At least one of brands[] or brand_refs[] is required. A brand_id MUST NOT appear in both. See docs/brand-protocol/brand-json.mdx", "properties": { "$schema": { "type": "string" }, "version": { "type": "string" }, @@ -1410,7 +1450,7 @@ }, "brand_refs": { "type": "array", - "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]; brand_id values MUST be unique within brand_refs[]. See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]; brand_id values MUST be unique within brand_refs[]. See docs/brand-protocol/brand-json.mdx", "items": { "$ref": "#/definitions/brand_ref" }, "minItems": 1 }, @@ -1422,16 +1462,8 @@ }, "trademarks": { "type": "array", - "items": { - "type": "object", - "properties": { - "registry": { "type": "string" }, - "number": { "type": "string" }, - "mark": { "type": "string" } - }, - "required": ["registry", "number", "mark"], - "additionalProperties": true - } + "items": { "$ref": "#/definitions/trademark" }, + "description": "House-level (corporate) registered trademarks. Brand-level marks live on individual brand entries; resolution is union." }, "last_updated": { "type": "string", "format": "date-time" } }, @@ -1445,7 +1477,7 @@ { "type": "object", "title": "Brand Canonical Document", - "description": "Self-published brand document where the brand owns its own identity attributes. Optionally declares its house via house_domain; for trust, the named house's brand_refs[] must reciprocate (mutual assertion). Standalone brands (no parent house) omit house_domain. Hosted at the brand's own /.well-known/brand.json (or via authoritative_location indirection). See RFC: docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx", + "description": "Self-published brand document where the brand owns its own identity attributes. Optionally declares its house via house_domain; for trust, the named house's brand_refs[] must reciprocate (mutual assertion). Standalone brands (no parent house) omit house_domain. Hosted at the brand's own /.well-known/brand.json (or via authoritative_location indirection). See docs/brand-protocol/brand-json.mdx", "allOf": [ { "type": "object", From 07ef98ff3a6d76b9c4e26843d833b6552c249b7f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 04:20:58 -0400 Subject: [PATCH 12/13] =?UTF-8?q?feat(brand-protocol):=20expert-review=20f?= =?UTF-8?q?ixes=20=E2=80=94=20third=20trust=20tier,=20schema=20tightening,?= =?UTF-8?q?=20conformance=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds protocol/product/docs expert review on PR #4505. No design rollbacks; all changes tightening or expanding. Schema - Rename #/definitions/brand_ref → portfolio_entry (disambiguate from buyer-side core/brand-ref.json which identifies brands in media plans). Make brand_id required on portfolio_entry so cross-array invariant is enforceable. Add optional effective_at (ISO 8601) so consumers age edges from a publisher-anchored date. - Brand Canonical Document not.anyOf deny-list adds authoritative_location, redirect_reason, redirect_effective_at — closes ambiguous-match holes against redirect variants. - Trademark gains optional licensor_domain (when license_type=licensed_in) and nice_classes (Nice Classification for cross-industry disambiguation — Delta-airline vs Delta-faucet). Trust model - Three-tier trust table: brand-identity (TLS-only) and brand-relationships (mutual-assertion-gated) resolve separately. A leaf-only edge keeps identity trust; only relationships block. - Self-healing notification SHOULD: consumers SHOULD email the house's contact.email when they encounter a leaf-only edge, so the parent team can complete the reciprocal entry. Rate-limited per {leaf, house}. - managed_by reframed as a directory field (aggregation across houses is the intended use); MUST NOT trust kept, SHOULD NOT aggregate dropped — the latter was fiction. Conformance - New invariants: house_domain MUST NOT appear in brands[]; brand_refs[] unique by domain (not just brand_id); mutual-assertion verification MUST follow House Redirects on the house side; strictest-of compliance expanded to include policy_categories and brand-level disclaimers[]; edge-aging language reframed around effective_at rather than a fixed 180-day SHOULD. - Resolution algorithm: "resolve recursively (single hop)" → "resolve once" with explicit clause that the followed document MUST be a Brand Canonical Document. Docs / structure - Variant 4/5 cross-reference trust model forward instead of asserting it inline — variant 5 reads cleanly when first encountered. - Variant 5 field table fully enumerates top-level fields and explicitly lists prohibited fields. - New "Adopting brand_refs[] for an existing portfolio" subsection documenting the migration path and AAO registry behaviour. - New "Out of scope" subsection: JVs with two parents, PE-opacity rollups, jurisdictional governance divergence — explicitly outside brand.json's scope (brand identity ≠ corporate legal structure). - Prior art expanded with app-ads.txt and WebFinger / host-meta (RFC 7033, 6415) as IETF analogues for well-known + JSON resource discovery. - Frontmatter description and managed_by prose updated. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/distributed-brand-json-impl.md | 23 ++-- docs/brand-protocol/brand-json.mdx | 122 ++++++++++++++++------ static/schemas/source/brand.json | 31 ++++-- 3 files changed, 133 insertions(+), 43 deletions(-) diff --git a/.changeset/distributed-brand-json-impl.md b/.changeset/distributed-brand-json-impl.md index 3de5a53044..92135ae9d5 100644 --- a/.changeset/distributed-brand-json-impl.md +++ b/.changeset/distributed-brand-json-impl.md @@ -4,14 +4,25 @@ `brand.json` gains a fifth variant and distributed publishing model. Additive — existing publishers unchanged. -**New variant — Brand Canonical Document.** A self-published per-brand document carrying the brand's identity attributes plus optional `house_domain` (string, the domain of the brand's parent house). Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. Excludes top-level house-only fields (`house`, `brands`, `brand_refs`, `authorized_operators`) to disambiguate from House Portfolio. +**New variant — Brand Canonical Document.** A self-published per-brand document carrying the brand's identity attributes plus optional `house_domain` (string, the domain of the brand's parent house). Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. Excludes top-level house-only fields (`house`, `brands`, `brand_refs`, `authorized_operators`) and redirect-variant fields (`authoritative_location`, `region`, `note`, `redirect_reason`, `redirect_effective_at`) to disambiguate from the other four variants. -**House Portfolio additions.** Gains `brand_refs[]` — pointer brands whose canonical documents live elsewhere (child-owned data). Each entry: `{ domain, brand_id?, managed_by? }`. `managed_by` (optional) is house-declared, non-trust-bearing — for grouping and discovery, used by holdcos to express agency-network delegation. Required widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` or `brand_refs[]`. +**House Portfolio additions.** Gains `brand_refs[]` — portfolio entries for brands whose canonical documents live elsewhere (child-owned data). Each entry has shape `{ domain, brand_id, managed_by?, effective_at? }`. The entry shape is defined as `#/definitions/portfolio_entry` (the name is distinct from `core/brand-ref.json`, which is the buyer-side schema for identifying brands in media-buy plans). `managed_by` (optional) is house-declared and explicitly non-trust-bearing — it's a directory field for aggregation across houses. `effective_at` (optional) is the publisher-declared timestamp consumers use to age mutual-assertion edges. Required widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` or `brand_refs[]`. -**Typed brand-level trademarks.** New `#/definitions/trademark` extracts the inline house-portfolio shape (`{registry, number, mark}`) as a named definition with optional `status`, `license_type`, `countries`. The existing `brand` definition now accepts typed `trademarks: Trademark[]`, enabling both inline `brands[]` entries and self-publishing Brand Canonical Documents to carry their brand-specific marks. House-level `trademarks[]` remains for corporate-level marks; resolution is union. +**Trust model.** A child Brand Canonical Document declares `house_domain: ""`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Trust resolves at two layers: brand identity (logos/colors/tone/tagline — authoritative on the leaf's TLS alone) and brand relationships (governance, billable inclusion — gated on mutual assertion). A leaf-only edge keeps identity trust and surfaces a self-healing notification SHOULD to the house's `contact.email`. Standalone (no `house_domain`) trumps any third-party portfolio claim. Compliance fields resolve strictest-of (union); `policy_categories` and brand-level `disclaimers[]` enumerated alongside `data_subject_contestation` and `compliance_policies`. -**Trust model.** A child Brand Canonical Document declares `house_domain: ""`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Inline children (`brands[]`) are covered by the parent's document authenticity directly. `managed_by` carries no trust weight. Standalone (no `house_domain`) trumps any third-party portfolio claim. Compliance fields resolve strictest-of house and brand. +**Typed brand-level trademarks.** New `#/definitions/trademark` extracts the inline house-portfolio shape (`{registry, number, mark}`) as a named definition with optional `status`, `license_type`, `licensor_domain` (when `license_type=licensed_in`), `countries`, and `nice_classes` (Nice Classification for cross-industry disambiguation — Delta-airline vs Delta-faucet). The existing `brand` definition now accepts typed `trademarks: Trademark[]`, enabling both inline `brands[]` entries and self-publishing Brand Canonical Documents to carry their brand-specific marks. House-level `trademarks[]` remains for corporate-level marks; resolution is union. -**Cross-array invariant** (validator + lint, not JSON Schema expressible): a `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. +**Conformance invariants** (validator + lint, not JSON Schema expressible): -`brand-json.mdx` is the normative spec — Motivation, the five variants, the trust model, the resolution algorithm, Conformance, and prior art (ads.txt / sellers.json) all live there. +- `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]`; `brand_id` and `domain` MUST each be unique within `brand_refs[]`. +- `house_domain` MUST NOT appear inside `brands[]` entries. +- Mutual-assertion verification MUST follow House Redirects on the house side before comparing membership. +- `managed_by` is a directory field — consumers MUST NOT use it for trust or authorization. Aggregation by `managed_by` is the intended use. +- Standalone trumps third-party claim. +- Compliance strictest-of for `data_subject_contestation`, `compliance_policies`, `policy_categories`, audience exclusions, regulated-category flags, and brand-level `disclaimers[]`. +- Edge aging via `brand_refs[].effective_at` (or consumer's first observation); AAO's reference crawler ages at 180 days. +- Self-healing: leaf-only edges SHOULD trigger consumer-side notification to the house's `contact.email`, rate-limited per `{leaf, house}` pair. + +**Publisher migration.** Free-text values for the existing inline `trademarks[].status` or `trademarks[].countries` properties now must conform to the typed enum (`active|pending|abandoned|cancelled|expired`) and ISO 3166-1 alpha-2 respectively. Publishers using non-conforming values will surface validation errors and need to update; the field shape was previously open via `additionalProperties: true` so this is the only behaviour change visible to existing data. + +`brand-json.mdx` is the normative spec — Motivation, the five variants, the trust model with self-healing notification, Adopting `brand_refs[]`, Out-of-scope cases (JVs, PE-opacity, jurisdictional governance), the resolution algorithm, Trademarks, Conformance, and prior art (ads.txt / app-ads.txt / sellers.json, WebFinger / host-meta) all live there. diff --git a/docs/brand-protocol/brand-json.mdx b/docs/brand-protocol/brand-json.mdx index 2b57cb3240..51371097e3 100644 --- a/docs/brand-protocol/brand-json.mdx +++ b/docs/brand-protocol/brand-json.mdx @@ -1,6 +1,6 @@ --- title: brand.json Specification -description: "brand.json specification for AdCP. File format, variants (portfolio, redirect, agent, authoritative location), brand definition fields, visual guidelines, colorways, type scale, restrictions, and resolution algorithm." +description: "brand.json specification for AdCP. File format, five variants (portfolio, redirect, agent, authoritative location, brand canonical document), brand definition fields, mutual-assertion trust model, visual guidelines, colorways, type scale, trademarks, and resolution algorithm." "og:title": "AdCP — brand.json Specification" --- @@ -38,7 +38,7 @@ The brand.json file supports five variants: | 4 | House Portfolio | A house publishes its brands (inline, by pointer, or both) | | 5 | Brand Canonical Document | A brand self-publishes its own identity (with optional `house_domain` pointer up) | -Variants 1–3 are mutually exclusive with each other and with variants 4–5. Variants 4 and 5 are distinct shapes — a document is either a portfolio or a brand canonical document, never both — but they're related: a house's portfolio can reference brand canonical documents via `brand_refs[]`. +Variants 1–3 are mutually exclusive with each other and with variants 4–5. Variants 4 and 5 compose: a House Portfolio (4) can reference Brand Canonical Documents (5) via `brand_refs[]`, and a Brand Canonical Document can point back at its house via `house_domain`. See [Mutual-assertion trust model](#mutual-assertion-trust-model) for how the two halves resolve. ### 1. Authoritative Location Redirect @@ -198,9 +198,12 @@ The `brand_refs[]` entry shape: | Field | Required | Meaning | |---|---|---| -| `domain` | yes | Where the child's canonical brand.json lives | -| `brand_id` | no | Stable identifier for this brand within the house's portfolio | -| `managed_by` | no | Domain of the entity that operationally manages this brand. **House-declared, non-trust-bearing.** UIs and discovery tools group by `managed_by`; the leaf doesn't have to know about the manager. | +| `domain` | yes | Where the child's canonical brand.json lives. Must be unique within `brand_refs[]`. | +| `brand_id` | yes | Stable identifier for this brand within the house's portfolio. Must be unique within `brand_refs[]` and MUST NOT also appear in `brands[]`. | +| `managed_by` | no | Domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared. **Consumers MUST NOT use it for trust or authorization decisions.** Aggregation across houses ("show me everything BBH manages") is the intended use — it's a directory field, not a trust field. | +| `effective_at` | no | ISO 8601 timestamp when the house established this ownership claim. Consumers age mutual-assertion edges from this date; omit and consumers age from their own first observation. | + +Trust semantics for the relationship between this entry and the child's `house_domain` claim are defined in [Mutual-assertion trust model](#mutual-assertion-trust-model) below. **House with delegation (WPP):** @@ -222,11 +225,11 @@ Holdcos delegate brand management to agency networks. WPP plc owns brands but Og } ``` -`managed_by` is **not part of the trust model.** Consumers MUST NOT use it for trust or authorization decisions. See [Conformance](#conformance) below. +`managed_by` is a **directory field, not a trust field.** It exists so consumers can group brands by who actually runs them day-to-day — the canonical view a buyer-side DSP wants when shopping inventory across an agency network. Consumers MUST NOT use it for trust or authorization decisions; that line still flows through mutual assertion between leaf and house. See [Conformance](#conformance). ### 5. Brand Canonical Document -Self-published per-brand document where the brand owns its own identity attributes. Hosted at the brand's own `/.well-known/brand.json` (or via [authoritative location redirect](#1-authoritative-location-redirect)). Optionally declares its house via `house_domain`; for trust, the named house's `brand_refs[]` must reciprocate. Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. +Self-published per-brand document where the brand owns its own identity attributes. Hosted at the brand's own `/.well-known/brand.json` (or via [authoritative location redirect](#1-authoritative-location-redirect)). A brand may declare its house via `house_domain` or stand alone (no parent house — Patagonia, Liquid Death — omit the field). Trust semantics for the `house_domain` relationship are defined in [Mutual-assertion trust model](#mutual-assertion-trust-model) below. **With a house — Converse under Nike:** @@ -263,30 +266,47 @@ Self-published per-brand document where the brand owns its own identity attribut Use this when a brand has its own domain and wants self-publish authority for its identity (logos, colors, tone, taglines). The brand carries the same identity fields as an inline entry in `brands[]`. -Top-level fields: +**Top-level fields specific to this variant:** | Field | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | Brand identifier (lowercase alphanumeric with underscores) | -| `names` | array | Yes | Localized names (see [Brand definition](#brand-definition)) | -| `house_domain` | string (domain) | No | Pointer to the corporate house this brand belongs to. The named house's `brand_refs[]` MUST reciprocate for mutual-assertion trust. Single-hop — a brand cannot itself have `brand_refs[]`. Omit for standalone brands. | +| `names` | array | Yes | Localized names | +| `house_domain` | string (domain) | No | Pointer to the corporate house this brand belongs to. The brand is treated as standalone if omitted. Single-hop — a brand cannot itself declare `brand_refs[]`. | +| `$schema`, `version`, `last_updated` | string | No | Document metadata | -All other brand-identity fields (`logos`, `colors`, `tone`, `tagline`, `visual_guidelines`, etc. — see [Brand definition](#brand-definition)) apply. +**All other brand-identity fields apply** (`logos`, `colors`, `fonts`, `tone`, `tagline`, `visual_guidelines`, `keller_type`, `parent_brand`, `properties[]`, `industries[]`, `target_audience`, `description`, `agents[]`, `contact`, `trademarks[]`, `data_subject_contestation`, etc. — see [Brand definition](#brand-definition) for the full field list). The document MUST NOT carry house-only fields (`house`, `brands`, `brand_refs`, `authorized_operators`) or redirect fields (`authoritative_location`, `house` as string, `region`, `note`, `redirect_reason`, `redirect_effective_at`). ## Mutual-assertion trust model -Trust between a house and a pointer child requires **both sides** to reciprocate: +Trust resolves at two layers — **brand identity** (logos, colors, tone, tagline, etc.) and **brand relationships** (who owns this brand, who can speak for it). The two layers separate cleanly. Identity is verifiable from a single TLS-served document; relationships require both sides to reciprocate. -| Edge | Both sides match? | Trust | -|---|---|---| -| Inline child in `brands[]` | n/a — house owns the data | Trusted (parent's TLS = the assertion) | -| Pointer child: `house_domain: A`, A's `brand_refs[]` includes child | Yes | **Trusted edge.** Governance propagation, member-feature inheritance, billable inclusion: yes. | -| Pointer child claims `house_domain: A`, A's `brand_refs[]` does not include child | No | One-sided. Surface as "claimed, unverified." Don't extend trust. | -| A's `brand_refs[]` includes child, child has no `house_domain` | No | One-sided in the other direction. Treat child as standalone. | -| Standalone brand (no `house_domain`) | n/a | Trusted as itself. No house relationship. | +| Edge | Identity trust | Relationship trust | Notes | +|---|---|---|---| +| Inline child in `brands[]` | Trusted (parent's TLS covers the data) | Trusted (the house authored the entry) | The classic shape. Parent owns the data. | +| **Mutual assertion**: leaf says `house_domain: A`, A's `brand_refs[]` includes leaf | Trusted (leaf's TLS) | Trusted (both sides reciprocate) | **Full trust.** Governance propagation, member-feature inheritance, billable inclusion all flow. | +| **Leaf-only**: leaf says `house_domain: A`, A's `brand_refs[]` does not include leaf | Trusted (leaf's TLS) | Unverified | Identity is real — the brand controls its own domain. The parent claim is unverified; governance and billable inclusion are blocked pending reciprocation. See [Self-healing through notification](#self-healing-through-notification). | +| **House-only**: A's `brand_refs[]` includes leaf, leaf has no `house_domain` | Trusted (leaf's TLS — as a standalone document) | Unverified in the other direction | The leaf hasn't claimed A. Treat the leaf as standalone; A's claim is supportive metadata. | +| Standalone (no `house_domain`) | Trusted (leaf's TLS) | n/a — no claimed relationship | Patagonia, Liquid Death. The brand is its own thing. | + +**Key asymmetry.** A leaf with verified TLS at its own domain is authoritative for *its own identity attributes* whether or not its parent claim is reciprocated. The relationship layer is what mutual assertion gates. A naïve "claimed, unverified ⇒ ignore the leaf entirely" reading is wrong — the leaf's logos/colors/tone are still real. + +**Standalone trumps third-party claim.** A Brand Canonical Document with no `house_domain` is standalone. If some other house's `brand_refs[]` lists it anyway, the leaf's silence is dispositive: treat as standalone; the third-party claim is unverified metadata only. See [Conformance](#conformance) for the normative statement. Validation is a runtime check (the crawler fetches both documents and compares); the JSON Schema accepts the fields independently. +### Self-healing through notification + +The leaf-only edge case is common in practice — a sub-brand team stands up its own canonical document before the parent's portfolio team has time to add the reciprocal entry. Today this leaves the brand stuck: identity is good, but media buys and governance decisions are blocked. + +The spec defines a self-healing loop: + +1. The house publishes a reachable `contact.email` at the top level of its `brand.json` (already part of the House Portfolio schema). +2. When a consumer encounters a leaf-only edge — leaf claims `house_domain: A`, A's `brand_refs[]` doesn't include leaf — the consumer **SHOULD** notify A's `contact.email` that a leaf is claiming reciprocation and is blocked pending verification. +3. The house team adds the entry to `brand_refs[]`. On the next crawl/re-validation, the edge upgrades to mutual. + +This turns mutual assertion from static two-party coordination into a feedback loop. The publisher of the consumer-side notification mechanism (email today; a typed verification endpoint is a planned follow-up) is responsible for rate-limiting and de-duplication. + ### Field resolution There is no inheritance/override block. Each consumer-side question has a single answer: @@ -315,8 +335,32 @@ See [Resolution algorithm](#resolution-algorithm) below for the full crawler pro Existing redirect variants handle M&A natively: 1. Pre-deal: `dentsu.com/.well-known/brand.json` is a House Portfolio. Dentsu brands' canonical docs say `house_domain: "dentsu.com"`. -2. Deal closes: Dentsu's `brand.json` is replaced with a [House Redirect](#2-house-redirect) → `{ "house": "wpp.com" }`. WPP adds the acquired brands to `brand_refs[]` with `managed_by: "dentsu.com"` for ops continuity. -3. Post-deal: leaves still pointing at `house_domain: "dentsu.com"` resolve through the redirect to WPP. Mutual-assertion holds via the redirect chain. No urgent leaf migration needed. +2. Deal closes: Dentsu's `brand.json` is replaced with a [House Redirect](#2-house-redirect) → `{ "house": "wpp.com" }`. WPP adds the acquired brands to `brand_refs[]` with `managed_by: "dentsu.com"` for ops continuity and `effective_at` set to the deal-close date. +3. Post-deal: leaves still pointing at `house_domain: "dentsu.com"` resolve through the redirect to WPP. Mutual-assertion verification follows the redirect chain (see [Conformance](#conformance)) and holds. No urgent leaf migration needed. + +### Adopting brand_refs[] for an existing portfolio + +An existing House Portfolio publisher (today on inline `brands[]`) doesn't need to migrate. `brand_refs[]` is additive and pull-based — a brand moves out of `brands[]` only when its team decides to self-publish. + +The migration path for a single brand: + +1. The child brand stands up `/.well-known/brand.json` at its own domain as a Brand Canonical Document, declaring `house_domain: ""`. +2. The house team removes the child's entry from `brands[]` and adds `{ domain, brand_id, effective_at }` to `brand_refs[]`. `brand_id` MUST match the value the child uses; otherwise the cross-array uniqueness invariant doesn't help future readers. +3. The crawler picks up the mutual assertion on next refresh. + +Between steps 1 and 2, the leaf-only edge is unverified at the relationship layer but the leaf's *identity* is still trusted on TLS (see [Self-healing through notification](#self-healing-through-notification)). Consumers SHOULD email the house's `contact.email` to prompt step 2. + +The AAO registry consumes both shapes today and will not re-classify a brand that moves from `brands[]` to `brand_refs[]` — the `brand_id` is the stable identifier across the migration. + +### Out of scope + +The single-house, single-hop trust model deliberately doesn't cover three shapes: + +- **Joint ventures with two parents** (Hulu pre-Disney, JVs in pharma, automotive partnerships). A leaf has at most one `house_domain`. JV structures must pick one canonical parent or use a holding entity; the protocol won't model two-parent ownership. +- **PE rollups and white-label arrangements that want opacity.** Mutual assertion publishes ownership at well-known URLs. A holdco that wants to operate brands without public disclosure can use the existing monolithic `brands[]` shape (which doesn't expose the parent to leaf-domain crawlers) but cannot use mutual-assertion. Mutual assertion trades opacity for verifiability — that's the design. +- **Jurisdictional governance divergence** (e.g., a Marriott franchisee in Germany whose data-subject contestation rules differ from the US parent's). Strictest-of resolution means brand-level publishers can only *add* constraints. A brand in a less-regulated jurisdiction cannot drop a house-level rule that doesn't apply to it. Workaround: the house publishes the strictest applicable per-region rule. + +These cases belong in governance / corporate-structure specs, not brand.json. `brand.json` is the brand-identity surface; corporate legal structure is its own concern. ## House definition @@ -949,8 +993,8 @@ To resolve a domain to a canonical brand: - **authoritative_location**: fetch from that URL, continue from step 2. - **house** (string): fetch from house domain, continue from step 2. - **brand_agent**: return agent URL — the agent is authoritative. - - **House Portfolio** (`house` object + `brands[]` and/or `brand_refs[]`): for an inline child, find the brand whose `properties[]` or `id` matches the query; for a pointer child, follow `brand_refs[].domain` and resolve recursively (single hop). - - **Brand Canonical Document** (`id` + `names` at top level): the document is the brand. If `house_domain` is present, fetch the house's `brand.json` to verify reciprocation in its `brand_refs[]` (mutual assertion). Read corporate-level fields (e.g., `data_subject_contestation`) from the house if not present on the brand. + - **House Portfolio** (`house` object + `brands[]` and/or `brand_refs[]`): for an inline child, find the brand whose `properties[]` or `id` matches the query; for a pointer child, follow `brand_refs[].domain` and resolve once — the followed document MUST be a Brand Canonical Document, never another House Portfolio. + - **Brand Canonical Document** (`id` + `names` at top level): the document is the brand. If `house_domain` is present, fetch the house's `brand.json` to verify reciprocation in its `brand_refs[]` (mutual assertion). When the house side is itself a [House Redirect](#2-house-redirect), follow the redirect chain on the house side before comparing. Read corporate-level fields (e.g., `data_subject_contestation`) from the house if not present on the brand; for compliance fields, resolve **strictest-of** house and brand (see [Mutual-assertion trust model](#mutual-assertion-trust-model)). 3. Return canonical brand information. Maximum redirect depth: 3 hops. The brand → house lookup is single-hop (no recursive parent walks). See [Mutual-assertion trust model](#mutual-assertion-trust-model) for trust semantics. @@ -1114,19 +1158,37 @@ Recommended cache TTLs: ## Conformance -These invariants MUST be enforced by validators and crawlers; JSON Schema cannot express them directly: +These invariants MUST be enforced by validators and crawlers; JSON Schema cannot express them directly. + +**Portfolio invariants** - **`brand_id` cross-array uniqueness.** A given `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]` of the same house. Publisher must choose one. - **`brand_id` within-array uniqueness.** A given `brand_id` MUST be unique within `brands[]` and unique within `brand_refs[]` of the same house. -- **Mutual-assertion as the trust primitive.** Consumers MUST NOT extend governance trust (auto-provisioning, member-feature inheritance, billable seat inclusion, inherited compliance fields) through one-sided claims. Mutual assertion (child's `house_domain` matches a `brand_refs[]` entry on the named house) is the canonical trust edge. -- **`managed_by` is not a trust signal.** Consumers MUST NOT use `managed_by` for trust or authorization decisions. UIs SHOULD render it as a unilateral house claim ("WPP says BBH manages this") and SHOULD NOT aggregate cross-house ("BBH's portfolio") without independent confirmation from the named manager. -- **Standalone trumps third-party claim.** A Brand Canonical Document with no `house_domain` is standalone, regardless of any third-party house's `brand_refs[]` claim about it. -- **Compliance fields strictest-of.** For governance fields (`data_subject_contestation`, `compliance_policies`, audience exclusions, regulated-category flags), the resolved value is the union/strictest of house-level and brand-level. Brand-level publishers MUST NOT rely on weakening house-level assertions. -- **180-day TTL.** Mutual-assertion edges that have not been re-validated within 180 days SHOULD be treated as one-sided regardless of last-known state. +- **`domain` within-array uniqueness.** Each `brand_refs[].domain` MUST be unique within the array. Two entries pointing at the same domain with different `brand_id` values is undefined — there can be only one canonical pointer per domain per house. +- **`house_domain` placement.** `house_domain` MUST NOT appear on entries inside `brands[]`. It is a Brand Canonical Document top-level field; an inline child cannot carry a parent pointer of its own. + +**Trust invariants** + +- **Mutual-assertion is the relationship-trust edge.** Consumers MUST NOT extend relationship trust (auto-provisioning, member-feature inheritance, billable seat inclusion) through one-sided claims. Mutual assertion (child's `house_domain` matches a `brand_refs[]` entry on the named house) is the canonical trust edge. +- **Identity is TLS-only.** A leaf brand's own identity attributes (logos, colors, tone, tagline, visual_guidelines) are authoritative based on the leaf's TLS-served document alone, regardless of mutual-assertion state. A leaf-only relationship claim does not invalidate the leaf's identity. +- **House Redirects on the house side MUST be followed.** When verifying mutual assertion, if the named house's `brand.json` is a House Redirect, the consumer MUST follow the redirect chain (up to the 3-hop limit) before comparing `brand_refs[]` membership. Otherwise post-acquisition leaves silently lose trust. +- **Standalone trumps third-party claim.** A Brand Canonical Document with no `house_domain` is standalone, regardless of any third-party house's `brand_refs[]` claim about it. The leaf's silence is dispositive. (Stated once here; descriptive prose elsewhere defers to this clause.) +- **`managed_by` is a directory field, not a trust field.** Consumers MUST NOT use `managed_by` for trust or authorization decisions. Aggregation by `managed_by` ("show me everything BBH manages") is the intended use — it's an operational directory across houses, not a trust assertion. + +**Resolution invariants** + +- **Compliance fields strictest-of.** For governance fields — including `data_subject_contestation`, `compliance_policies`, `policy_categories`, audience exclusions, regulated-category flags, and brand-level `disclaimers[]` that the house also publishes — the resolved value is the union/strictest of house-level and brand-level. Brand-level publishers MUST NOT rely on weakening house-level assertions. This is distinct from relationship trust — strictest-of is a resolution rule, not a trust gate. +- **Edge aging.** Mutual-assertion edges SHOULD be aged: consumers SHOULD treat an edge as one-sided when the gap between the publisher-declared `brand_refs[].effective_at` (or, if absent, the consumer's first observation) and the last successful re-validation exceeds the consumer's chosen TTL. AAO's reference crawler ages at 180 days; consumers MAY choose differently. + +**Self-healing** + +- **Leaf-only edges SHOULD trigger notification.** When a consumer encounters a leaf claiming `house_domain: X` with no reciprocal entry on X, the consumer SHOULD notify X's `contact.email` so the house team can complete the reciprocation. Notifying parties MUST rate-limit per `{leaf, house}` pair to avoid flooding. ## Prior art -The mutual-assertion trust primitive mirrors the IAB Tech Lab's [`ads.txt`](https://iabtechlab.com/ads-txt/) and [`sellers.json`](https://iabtechlab.com/sellers-json/) reciprocal-publication model: a buyer is trusted as a seller's reseller iff both sides publish the relationship at well-known URLs. Same trust shape, same non-cryptographic "who claims what about whom" verification, same fallback to one-sided / unverified for partial publication. A deployed, durable industry pattern. +The mutual-assertion trust primitive mirrors the IAB Tech Lab's [`ads.txt`](https://iabtechlab.com/ads-txt/) and [`sellers.json`](https://iabtechlab.com/sellers-json/) reciprocal-publication model — and the [`app-ads.txt`](https://iabtechlab.com/wp-content/uploads/2019/03/app-ads.txt-v1.0-final-.pdf) extension that proved the pattern moves cleanly from web bundles to mobile apps. A buyer is trusted as a seller's reseller iff both sides publish the relationship at well-known URLs. Same trust shape, same non-cryptographic "who claims what about whom" verification, same fallback to one-sided / unverified for partial publication. A deployed, durable industry pattern. + +The well-known URL plus structured JSON resource discovery shape predates ads.txt — see [WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) (RFC 7033) and [host-meta](https://datatracker.ietf.org/doc/html/rfc6415) (RFC 6415) for the IETF analogue. `brand.json` borrows this convention via [RFC 8615](https://datatracker.ietf.org/doc/html/rfc8615). Within AdCP, the [provenance verifier contract](https://github.com/adcontextprotocol/adcp/pull/3468) (seller-publishes / buyer-represents / seller-confirms) uses the same family of construction for a different field family. diff --git a/static/schemas/source/brand.json b/static/schemas/source/brand.json index 032f18f52d..0193d26674 100644 --- a/static/schemas/source/brand.json +++ b/static/schemas/source/brand.json @@ -40,18 +40,27 @@ "enum": ["owned", "licensed_in", "licensed_out"], "description": "Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted." }, + "licensor_domain": { + "$ref": "#/definitions/domain", + "description": "Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise." + }, "countries": { "type": "array", "items": { "type": "string", "minLength": 2, "maxLength": 2 }, "description": "ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit." + }, + "nice_classes": { + "type": "array", + "items": { "type": "integer", "minimum": 1, "maximum": 45 }, + "description": "Nice Classification class numbers (1-45) covered by this registration. Disambiguates marks across industries (e.g., Delta-airline vs Delta-faucet). Omit if scope is implicit from registry." } }, "required": ["registry", "number", "mark"], "additionalProperties": true }, - "brand_ref": { + "portfolio_entry": { "type": "object", - "description": "Reference to a pointer brand owned by this house. The child publishes its own canonical brand.json at the named domain. House-side declaration; mutual-assertion trust requires the child's house_domain to reciprocate. See docs/brand-protocol/brand-json.mdx", + "description": "A house's ownership entry for a brand that publishes its own canonical brand.json elsewhere. The publisher (the house) asserts 'I own this brand, hosted at this domain, effective on this date.' Mutual-assertion trust requires the child's house_domain to reciprocate. Distinct from core/brand-ref.json (which identifies brands in media-buy plans). See docs/brand-protocol/brand-json.mdx", "properties": { "domain": { "$ref": "#/definitions/domain", @@ -59,14 +68,19 @@ }, "brand_id": { "$ref": "#/definitions/brand_id", - "description": "Stable brand identifier within the house portfolio" + "description": "Stable brand identifier within the house portfolio. Required so the cross-array uniqueness invariant (brand_id MUST NOT appear in both brands[] and brand_refs[]) is enforceable." }, "managed_by": { "$ref": "#/definitions/domain", - "description": "Optional domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared, non-trust-bearing — used for grouping and discovery only. Verifiers MUST NOT use this field for trust or authorization decisions. The child does not need to reciprocate this claim." + "description": "Optional domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared. Consumers MUST NOT use it for trust or authorization decisions. Aggregation across houses ('show me everything BBH manages') is the intended use; trust is unaffected." + }, + "effective_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the house established this ownership claim. Consumers age mutual-assertion edges from this date for TTL purposes. Optional; absent means the consumer ages from its own first observation." } }, - "required": ["domain"], + "required": ["domain", "brand_id"], "additionalProperties": false }, "localized_name": { @@ -1450,8 +1464,8 @@ }, "brand_refs": { "type": "array", - "description": "Pointer brands owned by this house (child-owned data). Each entry references a brand whose canonical document is published elsewhere — typically at the brand's own domain. Use when a child brand wants self-publish authority. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. A brand_id MUST NOT appear in both brands[] and brand_refs[]; brand_id values MUST be unique within brand_refs[]. See docs/brand-protocol/brand-json.mdx", - "items": { "$ref": "#/definitions/brand_ref" }, + "description": "Portfolio entries for brands owned by this house that publish their own canonical brand.json elsewhere (child-owned data). Each entry asserts ownership plus where the child's document lives. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. Invariants: a brand_id MUST NOT appear in both brands[] and brand_refs[]; brand_id and domain MUST each be unique within brand_refs[]. See docs/brand-protocol/brand-json.mdx", + "items": { "$ref": "#/definitions/portfolio_entry" }, "minItems": 1 }, "contact": { "$ref": "#/definitions/contact" }, @@ -1496,6 +1510,9 @@ { "required": ["brands"] }, { "required": ["brand_refs"] }, { "required": ["authorized_operators"] }, + { "required": ["authoritative_location"] }, + { "required": ["redirect_reason"] }, + { "required": ["redirect_effective_at"] }, { "required": ["region"] }, { "required": ["note"] } ] From e98ac3fd16cf2d5041bd06023aa9e1a117f90c47 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 04:33:55 -0400 Subject: [PATCH 13/13] docs(release-notes): kick off 3.1.0 entry with distributed brand.json headline 3.1 doesn't ship today; minor releases accumulate from changesets. This opens the 3.1 section with PR #4505 as the headline feature so the narrative space exists when subsequent 3.1 changesets land. Includes adopter-action table for the publisher-visible behaviour change (trademark string drift) and links to the four design follow-ups (#4521 verification endpoint, #4522 JV multi-parent, #4523 PE-opacity tradeoff, #4524 manager-edge reciprocation). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/reference/release-notes.mdx | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/reference/release-notes.mdx b/docs/reference/release-notes.mdx index 836488e89e..1944c1208d 100644 --- a/docs/reference/release-notes.mdx +++ b/docs/reference/release-notes.mdx @@ -9,6 +9,55 @@ High-level summaries of major AdCP releases with migration guidance. For the at- --- +## Version 3.1.0 (unreleased) + +**Status:** Accumulating — minor release. The published 3.0.x line remains the stable surface; 3.1 features land here as their changesets accumulate. SDKs pin to a specific 3.x and pick up minors at their own cadence. + +**Headline:** **Distributed `brand.json`.** A brand can now publish its **own** canonical document on its own domain while the corporate house declares ownership via a portfolio pointer. The hierarchy stays one level deep — only houses declare ownership — and trust between a leaf and a house resolves via mutual assertion (both sides reciprocate). Identity attributes (logos, colors, tone, tagline) trust on the leaf's TLS alone; relationship trust (governance propagation, billable inclusion) gates on the reciprocal entry. + +Additive over 3.0 — every existing brand.json publisher continues to validate unchanged. + + +**Upgrading from 3.0.x?** No code changes required. Schemas remain wire-compatible. SDK consumers bump `ADCP_VERSION` to `3.1.0` on release to pick up the new variant and field shapes. The one publisher-visible behaviour change: free-text values for `trademarks[].status` or `trademarks[].countries` now validate against the typed enum / ISO 3166-1 alpha-2 — non-conforming values surface as schema errors. + + +### Adopter action + +| If you are… | What you need to do | +|---|---| +| A 3.0-conformant production agent | Nothing. Stable schemas remain wire-compatible with 3.0.0. | +| A brand on the existing inline `brands[]` shape | Nothing. Pull-based migration — you move out of `brands[]` only when you decide to self-publish. | +| A sub-brand team that wants self-publish authority | Stand up `/.well-known/brand.json` at your own domain as a Brand Canonical Document. Declare `house_domain: ""`. Ask the parent house team to add `{ domain, brand_id, effective_at }` to their `brand_refs[]`. Mutual assertion completes on the next crawl. | +| A house running the AAO crawler today (or any consumer that walks brand hierarchy) | Read [`brand.json` § Mutual-assertion trust model](/docs/brand-protocol/brand-json#mutual-assertion-trust-model). The 3-tier read — identity-trusted vs relationship-trusted vs unverified — replaces the binary "is this in my portfolio" check for `brand_refs[]` pointer children. The 180-day TTL is already AAO's reference behavior. | +| A publisher using free-text `status` or `countries` on `trademarks[]` entries | Move `status` values to the enum (`active`, `pending`, `abandoned`, `cancelled`, `expired`) and `countries` values to ISO 3166-1 alpha-2 codes. The fields were untyped via `additionalProperties: true`; they're now typed. Non-conforming values surface validation errors. | +| An agency named in a house's `managed_by` claim | Nothing required at the protocol level. Aggregation by `managed_by` ("show me everything BBH manages") is the intended use; trust is not extended through it. Manager-side reciprocation is tracked as a follow-up. | + +### Distributed `brand.json` — new 5th variant and `brand_refs[]` (#4505, closes #3409 / #3533 / #3764 / #3910 / #3909) + +**New variant — Brand Canonical Document.** A self-published per-brand document carrying the brand's identity attributes (logos, colors, tone, visual guidelines, etc.) plus optional `house_domain` pointer. Standalone brands (no parent) omit the pointer. Excludes top-level house-only and redirect-variant fields to disambiguate against the other four variants. + +**House Portfolio additions — `brand_refs[]`.** Portfolio entries for brands whose canonical documents live elsewhere. Each entry has shape `{ domain, brand_id, managed_by?, effective_at? }`. A house may mix inline `brands[]` (parent owns the data) and pointer `brand_refs[]` (child owns the data) freely; a given `brand_id` appears in exactly one. + +**Mutual-assertion trust model.** A leaf says `house_domain: A`; A's `brand_refs[]` includes the leaf. Both halves are crawler-readable from well-known URLs. Same shape as IAB's `ads.txt` / `sellers.json` / `app-ads.txt` reciprocal-publication pattern. Identity is TLS-only; relationships are mutual-assertion-gated. A leaf-only edge keeps identity trust and triggers a self-healing notification SHOULD to the house's `contact.email`. + +**`managed_by` is a directory field.** House-declared, non-trust-bearing. Aggregation across houses ("show me everything BBH manages") is the intended use — it's the operational directory a buyer-DSP wants. Consumers MUST NOT use it for trust or authorization decisions; that line still flows through mutual assertion between leaf and house. + +**Typed brand-level trademarks.** New `#/definitions/trademark` extracts the inline house-portfolio shape (`{registry, number, mark}`) as a named definition with optional `status`, `license_type`, `licensor_domain` (when `license_type=licensed_in`), `countries`, and `nice_classes` (Nice Classification for cross-industry disambiguation — Delta-airline vs Delta-faucet). Both inline `brands[]` entries and self-publishing Brand Canonical Documents now typed-publish their marks. House-level `trademarks[]` remains for corporate-level marks; resolution between the two is union. + +**Compliance fields strictest-of.** For governance fields (`data_subject_contestation`, `compliance_policies`, `policy_categories`, audience exclusions, regulated-category flags, brand-level `disclaimers[]`), the resolved value is the union/strictest of house-level and brand-level. Brand-level publishers cannot weaken house-level assertions; they can only add stricter constraints. Identity fields stay brand-wins; this asymmetry is intentional — brand teams own brand identity, legal owns compliance. + +**Out-of-scope cases** documented in the spec: JVs with two parents (Hulu pre-Disney), PE-rollups wanting opacity, jurisdictional governance divergence. These belong in governance / corporate-structure specs, not brand.json — see [follow-ups](https://github.com/adcontextprotocol/adcp/issues/4522) and [#4523](https://github.com/adcontextprotocol/adcp/issues/4523) for the design discussions. + +**Follow-ups tracking related work:** +- [#4521](https://github.com/adcontextprotocol/adcp/issues/4521) — typed `verification_endpoint` for the self-healing loop (today: email) +- [#4522](https://github.com/adcontextprotocol/adcp/issues/4522) — JV / two-parent shape +- [#4523](https://github.com/adcontextprotocol/adcp/issues/4523) — PE-opacity vs mutual-assertion tradeoff +- [#4524](https://github.com/adcontextprotocol/adcp/issues/4524) — manager-edge reciprocation for `managed_by` + +See [`brand.json` § Distributed publishing](/docs/brand-protocol/brand-json) for the full normative spec including resolution algorithm, Conformance MUSTs, adoption path, and prior art. + +--- + ## Version 3.0.6 **Status:** Patch release — stable-surface no-op for 3.0-conformant agents