Skip to content

feat(trusted-match): tmpx_providers map + provider_id charset constraint#5689

Merged
ohalushchak-exadel merged 3 commits into
mainfrom
ohalushchak-exadel/tmp-tmpx-providers
Jun 26, 2026
Merged

feat(trusted-match): tmpx_providers map + provider_id charset constraint#5689
ohalushchak-exadel merged 3 commits into
mainfrom
ohalushchak-exadel/tmp-tmpx-providers

Conversation

@ohalushchak-exadel

@ohalushchak-exadel ohalushchak-exadel commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

The TMP router's identity-match fan-out reaches multiple identity providers, each of which may emit a TMPX exposure token. The current single tmpx field on IdentityMatchResponse carries only one token — when more than one provider mints, the others are silently lost on the wire, and the publisher can never recover which provider minted which token. Per-provider attribution matters because the publisher fires each provider's token through {TMPX_<provider_id>}-style macros configured in their ad server; without per-provider tokens, the multi-provider case silently drops impression accounting.

This PR also fixes a concrete bug bokelley flagged in review: provider_id had no charset constraint, so {TMPX_<provider_id>} wasn't guaranteed to be a legal ad-server macro token. The fix is a charset pattern on provider_id at the registration boundary, so the synthesized macro name is always trafficable as-is.

Surfaced while implementing the router-architecture conformance pass at adcontextprotocol/adcp-go — mergeIdentityResponses currently last-wins-collapses TMPX because the wire shape has nowhere to put more than one. No prior issue/RFC.

Changes

  • static/schemas/source/tmp/provider-registration.json — add pattern: ^[A-Za-z0-9_]+$ and maxLength: 64 to provider_id, with description noting the trafficking role ({TMPX_<provider_id>} macro name).

  • static/schemas/source/tmp/identity-match-response.json — add tmpx_providers as an additionalProperties: string map of provider_id → opaque TMPX wire string (kid.base64url_nopad(ciphertext)). The legacy single-string tmpx field is marked DEPRECATED with "Removed in 4.0" framing; routers MAY continue to populate it as a transitional convenience, with tmpx_providers authoritative when both are present.

  • docs/trusted-match/specification.mdx

    • IdentityMatchResponse field table gains a row for tmpx_providers; the tmpx row is updated with the deprecation notice.
    • Provider Registration field table documents the provider_id charset constraint and its trafficking role (the schema this PR actually changes — corrects the cross-wire flagged in the bot review).
    • Narrative paragraph below the IdentityMatchResponse table explains the per-provider attribution rationale and the {TMPX_<provider_id>} trafficking pattern, and points at the existing 255-char macro budget with priority-ordered truncation for size-budget cases.
    • §Inventory-specific behavior updated to direct publishers at the {TMPX_<provider_id>} macro per provider and to require provider_id alongside each TMPX value in DOOH play logs.
  • docs/trusted-match/router-architecture.mdx — adds a normative TMPX collection paragraph under §"Identity Match fan-out" that MUSTs the per-provider map on multi-provider fan-outs, allows the back-compat tmpx, and explicitly forbids collapsing per-provider tokens into a single string (loses attribution, not recoverable downstream).

  • Changeset — minor bump (new optional field, additive deprecation of an existing field, additive constraint on an existing field).

What's NOT in this PR (tracked as follow-ups)

bokelley's scope-down feedback on the original wider proposal:

  • Provider-declared macro names (tmpx_macros on provider-registration). provider-registration.json is shared config that other TMP features will extend; stable macro-name declaration deserves its own design pass rather than landing under this PR's diff.
  • Multi-chunk TMPX values. The existing 255-char GAM macro budget with priority-ordered truncation (specification.mdx, "Size budget") is the protocol's current answer for "token too big for one macro." Building ordered chunking on top of that requires evidence that the existing mechanism is insufficient — gated on a router actually hitting truncation in production.

Both are tracked for a follow-up issue.

Compatibility

Additive on the new field; the provider_id charset constraint is a new schema constraint on an existing field. Existing registrations that already use macro-safe provider_id strings (the common case — IDs are typically alphanumeric handles) pass unchanged. Registrations using exotic characters would now fail validation; given the field is publisher-assigned and typically a short opaque slug, this is expected to bite very few or zero deployments. The deprecation cycle on tmpx ends at 4.0.

Test plan

  • npm run test:schemas — full schema validation suite passes
  • npm run lint:schema-links — clean
  • npm run test:docs-nav — clean
  • Pre-commit (unit / server unit / typecheck) passes locally
  • Spec preview renders the updated tables correctly
  • CI green on the PR

🤖 Generated with Claude Code

The router's identity-match fan-out reaches multiple identity providers,
each of which MAY mint a TMPX exposure token. The current single
`tmpx` field can only carry one of those tokens — anything beyond one
is lost on the wire, and the publisher can never recover which
provider minted which token. Per-provider attribution matters because
the publisher fires each provider's token through its own creative
macro (`{TMPX_<provider_id>}` pattern); without it the multi-provider
case silently drops impression accounting.

Add `tmpx_providers` to `tmp/identity-match-response.json` — an object
mapping `provider_id` -> TMPX wire string. Routers MUST emit this map
on multi-provider fan-outs and SHOULD also emit it for single-provider
responses. The legacy singular `tmpx` field is deprecated in favor of
the map; routers MAY continue to populate it as a transitional
convenience for consumers that haven't migrated, with `tmpx_providers`
authoritative when both are present. Removed in 4.0.

router-architecture.mdx §"Identity Match fan-out" gets a normative
TMPX-collection paragraph that codifies the behavior and explicitly
forbids collapsing per-provider tokens into a single string.

specification.mdx IdentityMatchResponse field table adds the new row;
the §"Inventory-specific behavior" subsection points publishers at
the `{TMPX_<provider_id>}` macro form for the multi-provider case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jun 24, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
adcp 🟢 Ready View Preview Jun 24, 2026, 1:19 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes Jun 24, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additive field on an experimental surface, back-compat preserved, changeset type correct. This is the right shape: tmpx_providers keys tokens by provider_id instead of last-wins-collapsing them into one string, which is exactly the attribution that mergeIdentityResponses was silently dropping on multi-provider fan-out.

Things I checked

  • Not a breaking wire change. tmpx is retained, still optional, still populated by routers; new consumers read the map, old consumers keep reading the string. ad-tech-protocol-expert: sound — minor is correct and in fact conservative, since tmp/identity-match-response.json:15 carries x-status: experimental and additive changes to experimental surfaces are patch-eligible per playbook.
  • Truly optional. tmpx_providers is absent from the required array (identity-match-response.json:44-49); root is already additionalProperties: true, so older validators never rejected it. additionalProperties: {type: string, minLength: 1} is valid draft-07 and enforces the doc's "values MUST be non-empty / omit rather than empty-string" rule.
  • No oneOf/discriminator impact. Flat property add; the type const discriminator (line 19) is untouched. scripts/audit-oneof.mjs surface unaffected.
  • Changeset present and correctly scoped (.changeset/tmp-tmpx-providers.md, minor).
  • Schema/table/router-arch trio agree on field name, optionality, and wire format (kid.base64url_nopad(ciphertext)), and on "map is authoritative when both present."

Follow-ups (non-blocking — file as issues)

  • Schema should set deprecated: true on tmpx. The prose says DEPRECATED but the machine-readable keyword is the repo convention — 21 instances, including the directly-analogous field-level cases at core/format.json:557 and core/postal-area-support.json:74. Codegen and agent tooling read the keyword, not the English. This is the one edit I'd actually make before the next pass. (docs-expert: High — but it's machine-readability, not wire drift, so it doesn't block.)
  • Stale single-token language in the replay-protection section. specification.mdx:688 still says the TMPX is "generated once per Identity Match evaluation… all impressions… share the same TMPX value (same nonce, same tokens)," and :229 says "The TMPX returned… MUST encode the resolved identities" (singular). With N providers there are N tokens and N nonces; the "same TMPX value" invariant no longer holds. ad-tech-protocol-expert flagged this as the load-bearing doc gap — it sits in the security/replay reasoning. Prose-only, so a follow-up, but worth closing before 4.0 framing hardens.
  • Macro-name safety for {TMPX_<provider_id>}. The docs introduce a creative macro that embeds the map key. provider_id is an unconstrained type: string (tmp/provider-registration.json:9-12), so a value with a space, }, or : would produce an unsafe macro token. Either add a propertyNames.pattern (e.g. ^[a-zA-Z0-9_-]+$) on both this map and the provider_id definition, or note macro-name safety is out of scope. code-reviewer and ad-tech-protocol-expert independently raised this.

Minor nits (non-blocking)

  1. Add minProperties: 1 to tmpx_providers (identity-match-response.json). The field's own description says values must be non-empty and the router MUST omit non-emitting providers rather than represent them — an empty {} map is meaningless. ~12 map fields in the schema tree already forbid the empty case. (code-reviewer)
  2. Required cell reads "No" with an embedded "Required by router conformance when…" (specification.mdx:212). The conditional obligation is real but the bare "No" reads as self-contradictory to a skimmer — No (conditional) matches how the narrative and router paragraph treat it. Deliberate schema-vs-conformance split, just surface it.
  3. "decryption master" (specification.mdx:700) is new vocabulary; the rest of the TMP docs say kid / HPKE key. Consider aligning. (docs-expert)

Test plan: the two unchecked boxes ("spec preview renders," "CI green") are cosmetic/automated, not manual verification of the behavior this PR changes — no honesty problem there.

LGTM. Follow-ups noted below.

@bokelley

Copy link
Copy Markdown
Contributor

I think the core direction is right, but I’d change the shape slightly before this lands.

The important contract is not “derive a macro from provider_id.” Each provider should have its own stable TMPX macro namespace / macro names that are part of provider configuration. For example Scope3 might traffic S3_TMPX, Walmart Connect might traffic WMC_TMPX, etc. When a publisher sets this up in GAM or another ad server, they need to know the provider-specific macro names ahead of time; arbitrary provider_id strings are the wrong source of truth for those names.

I’d also build for multi-macro values now, even if we cap it conservatively at first. Some providers may need more than one ad-server macro slot for the opaque value. For example, a provider might emit two chunks:

"tmpx_providers": {
  "scope3": {
    "macros": [
      { "name": "S3_TMPX_1", "value": "k1.part1..." },
      { "name": "S3_TMPX_2", "value": "part2..." }
    ]
  },
  "walmart_connect": {
    "macros": [
      { "name": "WMC_TMPX_1", "value": "k7.value..." }
    ]
  }
}

The exact field names can change, but I think the response should communicate exact macro/value pairs, not just a provider-id-to-token map. Ordered chunks let us start with “up to two per provider” and expand later if needed without another shape change.

On encoding: the router response should carry the exact URL-safe macro value to substitute. Publishers should not choose base64/base85/raw-byte behavior or transform these values. For the protocol surface, each value should be an opaque, URL-safe wire string with a defined format and size/chunking limit. Platforms that can carry raw bytes can optimize privately, but the interoperable contract should be “put this exact string into this exact macro.”

So I’d reframe the PR around:

  1. Provider registration declares stable TMPX macro names or a stable macro namespace.
  2. IdentityMatchResponse returns tmpx_providers[provider_id].macros[].
  3. Each macro entry includes the exact macro name and exact URL-safe value to substitute.
  4. Multi-chunk values are ordered and initially capped, e.g. two chunks per provider.
  5. tmpx remains transitional, but the authoritative model is provider-scoped macro/value pairs.

That keeps the fundamental win of this PR — preserving per-provider TMPX attribution across router fan-out — while making the trafficking contract explicit enough for GAM/ad-server integrations.

Reframed per the PR review feedback: deriving macro names from
`provider_id` at runtime is wrong because the ad-server line items
(GAM key-values, VAST URL macros, DOOH play-log fields) are
configured against stable, provider-namespaced names ahead of time
(e.g. `S3_TMPX_1` for one provider, `WMC_TMPX_1` for another). The
trafficking contract has to communicate exact macro/value pairs, and
it has to be ready for multi-chunk TMPX values when an opaque token
exceeds one macro slot.

Schema shape now:

- `tmp/provider-registration.json` adds `tmpx_macros`: an ordered list
  of stable ad-server macro names the provider's TMPX response fills.
  Pattern-validated, capped at 2 entries in v1; the cap MAY rise
  without a shape change. Required when `identity_match` is true and
  the provider emits TMPX.

- `tmp/identity-match-response.json` adds two fields and a shared
  `TmpxMacro` definition (`{name, value}` — provider-namespaced name,
  opaque URL-safe value).
  * `tmpx_macros[]` (provider-emitted): ordered macro/value pairs the
    identity agent emits, names drawn from its registered list.
  * `tmpx_providers` (router-populated): map of `provider_id ->
    { macros: [TmpxMacro] }`. The router merges every provider's
    `tmpx_macros` into this map, keyed by provider_id, preserving
    ordering and chunking.

Spec narrative updated to match: IdentityMatchResponse and
ProviderEntry tables surface the new fields, §"Inventory-specific
behavior" walks the per-macro substitution flow ("substitute this
exact string into this exact macro"), and router-architecture.mdx
adds a normative TMPX-collection paragraph that MUSTs the merge,
preserves chunk order, and explicitly forbids synthesizing macro
names from provider_id at runtime or collapsing per-provider values
into a single string.

The legacy single-token `tmpx` field stays deprecated; routers MAY
populate it with one provider's first-slot value for back-compat,
with `tmpx_providers` authoritative when both are present. Removed
in 4.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley

Copy link
Copy Markdown
Contributor

Agree with the core point, and there's a concrete bug underneath it: provider_id in provider-registration.json has no charset constraint (no pattern, unlike countries's ^[A-Z]{2}$), so {TMPX_<provider_id>} isn't guaranteed to be a legal ad-server macro token. Worth fixing regardless of how the broader redesign lands.

Scope-wise, I'd keep the redesign out of this PR. provider-registration.json is shared config other TMP features will extend too — stable macro-name declaration deserves its own design pass, not a field added under this PR's diff. The multi-chunk case is also still hypothetical: the spec already has a mechanism for "token too big for one macro" — the 255-char GAM budget with priority-ordered truncation (specification.mdx, "Size budget"). I'd want evidence that's insufficient before stacking ordered chunking on top of it.

For this PR specifically: constrain provider_id to a macro-safe charset (e.g. ^[A-Za-z0-9_]+$) so {TMPX_<provider_id>} is always legal, and note in the docs that the macro name is the publisher's provider_id verbatim, not a separately registered name. tmpx_providers ships as designed. Worth a tracked follow-up issue for "provider-declared macro names + multi-chunk values," gated on a router actually hitting size-budget truncation in production rather than building ahead of that signal.


Triaged by Claude Code. Session: https://claude.ai/code/cse_01CBUYf5vmiUQ2vWJooKs3XP


Generated by Claude Code

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right shape on the wire, wrong wiring in the docs. The tmpx_providers map is the correct fix for multi-provider TMPX attribution — but the registration field is documented against the wrong entity, and the worked examples use real company names. Two blocks.

MUST FIX

1. Cross-wired schema/doc drift on tmpx_macros. The schema field landed in static/schemas/source/tmp/provider-registration.json (router config, keyed by provider_id). The doc row landed in the ProviderEntry table at specification.mdx:379 — the product-level trusted_match.providers[] entry, keyed by agent_url and backed by static/schemas/source/core/product.json:484-531, which this PR does not touch (and which has additionalProperties: true, so it silently accepts the field without validating it). The Provider Registration table at specification.mdx:321-332, which is the human doc for the schema that actually changed, got no row at all.

So the field is backed-but-undocumented in its own table, and documented-but-unbacked in a different one. The failure mode for an adopter: they read ProviderEntry, register tmpx_macros on the product's trusted_match.providers[] (agent_url-keyed) where it's accepted and ignored, while the router reads macros from provider-registration.json (provider_id-keyed) — the trafficked names never reach the router and TMPX substitution silently no-ops. ad-tech-protocol-expert and docs-expert both graded this blocking. Move the row to the Provider Registration table (before the status row at specification.mdx:332), and repoint the cross-links — the tmpx_macros and TmpxMacro rows both say names come from "the provider's registered `tmpx_macros` list (see [ProviderEntry])"; after the move those point to Provider Registration. While you're there: the row's "Required when `identity_match` is true" wording overstates the schema, which adds no `if/then` for `tmpx_macros` — the actual contract is conditional on the provider emitting TMPX.

2. Real company names in new examples. .agents/playbook.md:91 — "Never use real company names (brands, agencies, holding companies) in new examples." The PR uses Scope3 (S3_TMPX_1, S3_TMPX_2) and Walmart Connect (WMC_TMPX_1) as worked examples. This isn't only prose — the names are baked into schema descriptions in provider-registration.json and the tmpx_providers description in identity-match-response.json, so they ship on the wire surface. Re-cast against the playbook's fictional set (Pinnacle → PIN_TMPX_1, Nova → NOVA_TMPX_1).

Things I checked

  • Schema mechanics are clean (code-reviewer): $defs is a sibling of properties not nested, #/$defs/TmpxMacro resolves from both ref sites, and minItems:1/maxItems:2/pattern ^[A-Z][A-Z0-9_]*$/maxLength:64 are consistent across all three array sites (registration, response root, per-provider macros). minItems:1 correctly bars the empty-macros[] representation the router-arch text forbids.
  • tmpx_providers wire shape is sound — additionalProperties map keyed by provider, value {macros:[TmpxMacro]} with required:[\"macros\"] and additionalProperties:false. Conventional draft-07 string→object idiom.
  • Deprecation of tmpx is handled right: field kept, description-only change, removed-in-4.0 deferred to next major. minor changeset is correct — purely additive plus an additive deprecation. Changeset present and accurate at .changeset/tmp-tmpx-providers.md.
  • additionalProperties: true on the response root means the new fields validate even unlisted — but they're properly declared, no issue.

Follow-ups (non-blocking — file as issues)

  • static/schemas/source/enums/universal-macro.json:61,138-139 still describes the singular {TMPX} model, and IMPRESSION_ID at :139 references "{TMPX} decode." The provider-namespaced names (*_TMPX_1) are intentionally not enum members — that's defensible, but the enum should carry a deprecation note pointing at tmpx_providers, and the spec should state explicitly that these names are not universal-macro members so nobody files a phantom "missing enum value" bug.
  • tmpx_providers description says keys "MUST be valid provider_ids" but nothing enforces it — consider propertyNames/minProperties:1 if you want the prose to be load-bearing rather than advisory.
  • The IdentityMatchResponse table now carries three overlapping fields (tmpx_macros, tmpx_providers, tmpx). State explicitly that a single message instance carries the provider-side tmpx_macros or the router-merged tmpx_providers depending on which hop it is — an agent reader could populate both.

Minor nits (non-blocking)

  1. Stale PR body. The Summary still describes the abandoned design — tmpx_providers as provider_id → wire string and a {TMPX_<provider_id>} macro pattern. The shipped schema is provider_id{macros:[{name,value}]}, and the spec explicitly forbids deriving names from provider_id. The changeset is accurate; the body is not. Update it so the next reviewer grades the right design.

Fix the table placement and the brand names and this is a clean additive change. ship it once those two land.

@ohalushchak-exadel ohalushchak-exadel changed the title feat(trusted-match): tmpx_providers map on IdentityMatchResponse feat(trusted-match): provider-scoped TMPX macro trafficking on IdentityMatchResponse Jun 26, 2026
…set constraint

Per the PR-review feedback (#5689#issuecomment-4810391726): the
ad-server-trafficked macro name is part of operational setup, but
declaring per-provider macro names + multi-chunk values touches a
config surface other TMP features will extend and stacks on top of
the existing 255-char macro budget with priority-ordered truncation
(specification.mdx, "Size budget"). There's no evidence in production
that the existing budget mechanism is insufficient — building ahead
of that signal couples two design surfaces unnecessarily.

This commit drops the registered-macro-names + multi-chunk redesign
and keeps the smaller win: `tmpx_providers` as a map keyed by
`provider_id` -> opaque token string. The concrete bug bokelley flagged
— `provider_id` had no charset constraint, so `{TMPX_<provider_id>}`
wasn't guaranteed to be a legal ad-server macro token — is fixed by
adding `pattern: ^[A-Za-z0-9_]+$` and `maxLength: 64` to `provider_id`
in `tmp/provider-registration.json`. The macro name is the publisher's
`provider_id` verbatim wrapped in `{TMPX_…}`; charset constraints at
the registration boundary keep that token legal in every supported ad
server without additional encoding.

Also resolves the two MUST-FIX items from #5689's review
(#5689#pullrequestreview-4580162485):
- the schema/doc cross-wire on `tmpx_macros` — the field is gone now,
  and the new `provider_id` charset doc lands on the Provider
  Registration table (the one for the schema this PR actually
  touches), not on the product-level ProviderEntry table.
- the real-company-name examples — gone with the registered-macro
  shape; the surviving narrative example uses a placeholder
  `pinnacle_id`, matching .agents/playbook.md.

Schema changes vs main:
- `tmp/provider-registration.json` adds the `provider_id` pattern +
  maxLength constraint.
- `tmp/identity-match-response.json` adds `tmpx_providers` as an
  `additionalProperties: string` map, and deprecates the singular
  `tmpx` (removed in 4.0).

Spec narrative: IdentityMatchResponse field table surfaces
`tmpx_providers` and the deprecated `tmpx`; Provider Registration table
documents the `provider_id` charset constraint and its trafficking
role; §"Inventory-specific behavior" walks the `{TMPX_<provider_id>}`
substitution per surface; router-architecture.mdx §"Identity Match
fan-out" adds the normative `TMPX collection` paragraph that MUSTs
the merge and forbids collapsing per-provider tokens.

Provider-declared macro names + multi-chunk TMPX values are tracked as
a follow-up gated on routers actually hitting the 255-char macro
budget in production.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ohalushchak-exadel ohalushchak-exadel changed the title feat(trusted-match): provider-scoped TMPX macro trafficking on IdentityMatchResponse feat(trusted-match): tmpx_providers map + provider_id charset constraint Jun 26, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right shape for the problem: one TMPX-per-provider collapsed into a single field is unrecoverable downstream, so a provider_id → token map preserves attribution across the router fan-out. Additive on the wire, tmpx deprecation is coherent. Approving — but the keys gap below is load-bearing and should land before merge.

Things I checked

  • No new oneOf. The map is additionalProperties: {type: string, minLength: 1}; provider-registration's anyOf (context/identity) is pre-existing. No audit-walker exposure.
  • Additive on the response. tmpx_providers is absent from required (identity-match-response.json:44-49) and root additionalProperties stays true — old consumers reading tmpx keep working.
  • Deprecation framing is consistent across schema, both .mdx field tables, the narrative, and the inventory-behavior section: routers MAY still populate tmpx, tmpx_providers authoritative when both present, provider omitted (not empty-string) when it mints nothing. "Removed in 4.0" stated uniformly.
  • Changeset present (.changeset/tmp-tmpx-providers.md) — wire-touching PR is covered. Both experts ran clean: code-reviewer no Blocker, ad-tech-protocol-expert sound-with-caveats.

Follow-ups (non-blocking — file as issues)

  • Constrain the map keys before merge. This is the one that matters. The whole PR exists to keep {TMPX_<provider_id>} macro-safe — yet tmpx_providers keys ARE the provider_ids the publisher wraps in {TMPX_<key>}, and they're unconstrained. The description asserts "Map keys MUST be valid provider_ids" but the schema only validates values; a key like `"évil key!"` passes. The `provider_id` pattern at the registration boundary does not transitively reach the response wire. Add to tmpx_providers:
    ```json
    "propertyNames": { "pattern": "^[A-Za-z0-9_]+$", "maxLength": 64 }
    ```
    matching provider-registration.json:9-12. Both code-reviewer (Major) and ad-tech-protocol-expert flagged this as the central correctness gap. Non-blocking only because the field is additive and a conforming router emits safe keys — but it closes the exact hole the charset constraint was added to close, so land it here rather than chasing it later.
  • Single-provider case: SHOULD vs MUST drift. The spec table says SHOULD populate for one provider (specification.mdx:212); router-architecture's new paragraph says the router MUST collect any emitted token. The conditional-required ("Required by router conformance when more than one provider emitted") also can't be expressed in schema since "more than one emitted" is router-internal state. Pick one normative level and make the three locations agree.
  • Changeset bump. minor is valid (TMP is x-status: experimental; main is in pre-mode so it ships beta). But the directly analogous experimental-TMP additive-field change (#5411 seller_agent_url) was deliberately classified patch to avoid cutting a minor. If this is meant for the 3.0.x line, patch is the convention; minor pulls it onto the next minor. Confirm the target line.
  • Deprecation-notice clock. Eventual tmpx removal is breaking on an experimental surface, which per experimental-status.mdx:41-47 wants the notice published in release notes/changelog ~6 weeks ahead. Right now the deprecation lives only in field descriptions. Start the clock somewhere consumers look.

Minor nits (non-blocking)

  1. Stale {TMPX} obligations prose. The "Publisher obligations" paragraph above §Inventory-specific behavior (specification.mdx:694 area) still describes a singular {TMPX} pass-through; the inventory section right below it was rewritten to per-provider {TMPX_<provider_id>}. Reads as if a single macro is the only model.
  2. Macro delimiter ambiguity. _ is both the {TMPX_<id>} separator and a legal provider_id character, so {TMPX_foo_bar} is ambiguous between provider foo_bar and a hypothetical foo+bar scheme. Publisher trafficks the exact synthesized string so it's not a bug — worth one line given the charset was chosen for macro safety.
  3. Spec-preview render unverified. The "[ ] Spec preview renders the updated tables" box is unchecked. An intermediate commit carried a different Map<string, {macros: ...}> shape for this row; the landed Map<string, string> is correct in source, but confirm the rendered table matches.

LGTM. Land the propertyNames key constraint and the rest are follow-ups.

@ohalushchak-exadel ohalushchak-exadel merged commit 96a8c1d into main Jun 26, 2026
32 checks passed
@ohalushchak-exadel ohalushchak-exadel deleted the ohalushchak-exadel/tmp-tmpx-providers branch June 26, 2026 16:32
ohalushchak-exadel added a commit that referenced this pull request Jun 26, 2026
- Stale `provider_id` framing (carried over from #5689): the field's
  description still justified its charset constraint by "trailing
  token in the per-provider TMPX macro name `{TMPX_<provider_id>}`,"
  which is the model this PR moves away from. Rewrite the prose in
  both provider-registration.json and the Provider Registration table
  to point at `tmpx_providers` keying and the registered `tmpx_macros`
  names, and explicitly state macro names MUST NOT be derived from
  `provider_id` at runtime. Keep the charset constraint — it's
  harmless and still useful for logs/metrics/legacy `{TMPX}`
  substitutions.

- Schema-vs-prose drift on mutual exclusivity: spec said `tmpx_macros`
  and `tmpx_providers` are "mutually exclusive per hop," but the
  schema defines both as independent optionals with no `not`/`oneOf`
  (the same schema serves both hops, so the exclusivity can't be
  expressed without splitting). Soften the wording to descriptive
  ("by convention, provider->router populates `tmpx_macros`,
  router->publisher populates `tmpx_providers`") and add a SHOULD on
  the consumer side instead of a normative MUST.

- Unenforced "REQUIRED when ...": the registration `tmpx_macros` row
  asserted "REQUIRED when `identity_match` is true and the provider
  emits TMPX," but "emits TMPX" isn't a schema-visible predicate, so
  no `if`/`then` could enforce it. Drop the "Conditional / REQUIRED
  when ..." framing, restate as a normative invariant in prose
  ("a provider that emits TMPX MUST also register this list"), and
  explicitly call out that the schema cannot enforce this.

- Experimental notice window: experimental-status.mdx asks for ~6
  weeks' published notice before a breaking change to an experimental
  surface. `tmpx_providers` shipped days ago, so the literal window
  can't apply; note the situation in the changeset rather than block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley pushed a commit that referenced this pull request Jun 28, 2026
Picks up the redesign that was reverted from #5689 ahead of merge to
ship the smaller "tmpx_providers map" win first. The trafficking
contract has to communicate exact macro/value pairs, not provider_id
→ opaque string, because the ad-server line items (GAM key-values,
VAST URL macros, DOOH play-log fields) are configured against stable,
provider-namespaced names ahead of time — synthesizing macro names
from provider_id at runtime breaks that operational model. The wire
shape also needs to be ready for multi-chunk TMPX values when an
opaque token exceeds one macro slot.

Schema:

- `tmp/provider-registration.json` adds `tmpx_macros`: ordered list
  of stable ad-server macro names this provider's TMPX response fills.
  Pattern `^[A-Z][A-Z0-9_]*$`, capped at 2 entries in v1; the cap MAY
  rise without a shape change. Required when identity_match is true
  and the provider emits TMPX.

- `tmp/identity-match-response.json` adds two fields and a shared
  `TmpxMacro` definition (`{name, value}` — provider-namespaced name,
  opaque URL-safe value):
  * `tmpx_macros[]` (provider-emitted): ordered macro/value pairs the
    identity agent emits, names drawn from its registered list.
  * `tmpx_providers` reshaped from `Map<provider_id, string>` to
    `Map<provider_id, {macros: [TmpxMacro]}>`. Router merges every
    provider's `tmpx_macros` into this map, preserving ordering and
    chunking.

BREAKING CHANGE TO AN EXPERIMENTAL SURFACE (sanctioned by
`x-status: experimental` on identity-match-response.json):
`tmpx_providers` was introduced in #5689 as a Map<provider_id, string>
(opaque token per provider) and is reshaped here. Consumers that
adopted the v1 shape between #5689 and this change MUST migrate to
read each provider's `macros[*].value` rather than a single string.
The window between the two PRs is small and the surface is
experimental, so the migration cost is bounded.

The legacy single-token `tmpx` field stays deprecated; routers MAY
populate it with one provider's first-slot value for back-compat,
with `tmpx_providers` authoritative when both are present. Removed
in 4.0.

Spec narrative updated to match: IdentityMatchResponse and Provider
Registration field tables surface the new fields and the reshape;
the IdentityMatchResponse table also clarifies that the provider-side
`tmpx_macros` and the router-merged `tmpx_providers` are mutually
exclusive per hop. New `**TMPX macro trafficking**` paragraph + a new
`TmpxMacro` sub-table walk the registration → response → trafficking
flow. `§Inventory-specific behavior` rewritten to point at the
declared macro names (not provider_id-derived). `router-architecture.mdx`
TMPX-collection paragraph rewritten to MUST the merge, preserve chunk
order, forbid synthesizing macro names from provider_id, and forbid
collapsing per-provider values into a single string.

All worked examples use the fictional brand set per `.agents/playbook.md`
(Pinnacle → PIN_TMPX_1, Nova → NOVA_TMPX_1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley pushed a commit that referenced this pull request Jun 28, 2026
- Stale `provider_id` framing (carried over from #5689): the field's
  description still justified its charset constraint by "trailing
  token in the per-provider TMPX macro name `{TMPX_<provider_id>}`,"
  which is the model this PR moves away from. Rewrite the prose in
  both provider-registration.json and the Provider Registration table
  to point at `tmpx_providers` keying and the registered `tmpx_macros`
  names, and explicitly state macro names MUST NOT be derived from
  `provider_id` at runtime. Keep the charset constraint — it's
  harmless and still useful for logs/metrics/legacy `{TMPX}`
  substitutions.

- Schema-vs-prose drift on mutual exclusivity: spec said `tmpx_macros`
  and `tmpx_providers` are "mutually exclusive per hop," but the
  schema defines both as independent optionals with no `not`/`oneOf`
  (the same schema serves both hops, so the exclusivity can't be
  expressed without splitting). Soften the wording to descriptive
  ("by convention, provider->router populates `tmpx_macros`,
  router->publisher populates `tmpx_providers`") and add a SHOULD on
  the consumer side instead of a normative MUST.

- Unenforced "REQUIRED when ...": the registration `tmpx_macros` row
  asserted "REQUIRED when `identity_match` is true and the provider
  emits TMPX," but "emits TMPX" isn't a schema-visible predicate, so
  no `if`/`then` could enforce it. Drop the "Conditional / REQUIRED
  when ..." framing, restate as a normative invariant in prose
  ("a provider that emits TMPX MUST also register this list"), and
  explicitly call out that the schema cannot enforce this.

- Experimental notice window: experimental-status.mdx asks for ~6
  weeks' published notice before a breaking change to an experimental
  surface. `tmpx_providers` shipped days ago, so the literal window
  can't apply; note the situation in the changeset rather than block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants