From 6c3f4047a0d76f6c45fbdb3a3eb776a5def213b9 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 29 Apr 2026 11:00:02 -0400 Subject: [PATCH 1/6] 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 93d9efe6d5..c6cf8ad7c8 100644 --- a/docs.json +++ b/docs.json @@ -442,6 +442,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 3730896db3d3c5511f6a1cd2798cd2a6ca44719d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 29 Apr 2026 16:34:20 -0400 Subject: [PATCH 2/6] 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 c6cf8ad7c8..93d9efe6d5 100644 --- a/docs.json +++ b/docs.json @@ -442,12 +442,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 424e0f4a336b6038949b786610d66c5593d30bba Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 1 May 2026 12:12:50 -0400 Subject: [PATCH 3/6] =?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 6634bc918571aac88e6cae90cec240ddb077bbcb Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 1 May 2026 12:45:51 -0400 Subject: [PATCH 4/6] 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 e5537a999b11cbddbb9c52171682970f045a37ac Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 19:51:21 -0400 Subject: [PATCH 5/6] =?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 91f80f5be76821e2bbcbe1e5bebf33302fd45d8f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 20:02:25 -0400 Subject: [PATCH 6/6] =?UTF-8?q?docs(brand-protocol):=20RFC=20v3=20tighteni?= =?UTF-8?q?ng=20=E2=80=94=20normative=20managed=5Fby,=20conformance,=20str?= =?UTF-8?q?ictest-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