feat(trusted-match): tmpx_providers map + provider_id charset constraint#5689
Conversation
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>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
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.
tmpxis retained, still optional, still populated by routers; new consumers read the map, old consumers keep reading the string.ad-tech-protocol-expert: sound —minoris correct and in fact conservative, sincetmp/identity-match-response.json:15carriesx-status: experimentaland additive changes to experimental surfaces are patch-eligible per playbook. - Truly optional.
tmpx_providersis absent from therequiredarray (identity-match-response.json:44-49); root is alreadyadditionalProperties: 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
typeconst discriminator (line 19) is untouched.scripts/audit-oneof.mjssurface 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: trueontmpx. The prose says DEPRECATED but the machine-readable keyword is the repo convention — 21 instances, including the directly-analogous field-level cases atcore/format.json:557andcore/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:688still says the TMPX is "generated once per Identity Match evaluation… all impressions… share the same TMPX value (same nonce, same tokens)," and:229says "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-expertflagged 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_idis an unconstrainedtype: string(tmp/provider-registration.json:9-12), so a value with a space,}, or:would produce an unsafe macro token. Either add apropertyNames.pattern(e.g.^[a-zA-Z0-9_-]+$) on both this map and theprovider_iddefinition, or note macro-name safety is out of scope.code-reviewerandad-tech-protocol-expertindependently raised this.
Minor nits (non-blocking)
- Add
minProperties: 1totmpx_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) - 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. - "decryption master" (
specification.mdx:700) is new vocabulary; the rest of the TMP docs saykid/ 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.
|
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 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:
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>
|
Agree with the core point, and there's a concrete bug underneath it: Scope-wise, I'd keep the redesign out of this PR. For this PR specifically: constrain Triaged by Claude Code. Session: https://claude.ai/code/cse_01CBUYf5vmiUQ2vWJooKs3XP Generated by Claude Code |
There was a problem hiding this comment.
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):$defsis a sibling ofpropertiesnot nested,#/$defs/TmpxMacroresolves from both ref sites, andminItems:1/maxItems:2/pattern ^[A-Z][A-Z0-9_]*$/maxLength:64are consistent across all three array sites (registration, response root, per-providermacros).minItems:1correctly bars the empty-macros[]representation the router-arch text forbids. tmpx_providerswire shape is sound —additionalPropertiesmap keyed by provider, value{macros:[TmpxMacro]}withrequired:[\"macros\"]andadditionalProperties:false. Conventional draft-07 string→object idiom.- Deprecation of
tmpxis handled right: field kept, description-only change, removed-in-4.0 deferred to next major.minorchangeset is correct — purely additive plus an additive deprecation. Changeset present and accurate at.changeset/tmp-tmpx-providers.md. additionalProperties: trueon 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-139still describes the singular{TMPX}model, andIMPRESSION_IDat :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 attmpx_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_providersdescription says keys "MUST be valid provider_ids" but nothing enforces it — considerpropertyNames/minProperties:1if 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-sidetmpx_macrosor the router-mergedtmpx_providersdepending on which hop it is — an agent reader could populate both.
Minor nits (non-blocking)
- Stale PR body. The Summary still describes the abandoned design —
tmpx_providersasprovider_id→ wire string and a{TMPX_<provider_id>}macro pattern. The shipped schema isprovider_id→{macros:[{name,value}]}, and the spec explicitly forbids deriving names fromprovider_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.
…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>
There was a problem hiding this comment.
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 isadditionalProperties: {type: string, minLength: 1}; provider-registration'sanyOf(context/identity) is pre-existing. No audit-walker exposure. - Additive on the response.
tmpx_providersis absent fromrequired(identity-match-response.json:44-49) and rootadditionalPropertiesstaystrue— old consumers readingtmpxkeep working. - Deprecation framing is consistent across schema, both
.mdxfield tables, the narrative, and the inventory-behavior section: routers MAY still populatetmpx,tmpx_providersauthoritative 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-reviewerno Blocker,ad-tech-protocol-expertsound-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 — yettmpx_providerskeys ARE theprovider_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 totmpx_providers:
```json
"propertyNames": { "pattern": "^[A-Za-z0-9_]+$", "maxLength": 64 }
```
matchingprovider-registration.json:9-12. Bothcode-reviewer(Major) andad-tech-protocol-expertflagged 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.
minoris valid (TMP isx-status: experimental; main is in pre-mode so it ships beta). But the directly analogous experimental-TMP additive-field change (#5411seller_agent_url) was deliberately classifiedpatchto avoid cutting a minor. If this is meant for the 3.0.x line,patchis the convention;minorpulls it onto the next minor. Confirm the target line. - Deprecation-notice clock. Eventual
tmpxremoval is breaking on an experimental surface, which perexperimental-status.mdx:41-47wants 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)
- Stale
{TMPX}obligations prose. The "Publisher obligations" paragraph above§Inventory-specific behavior(specification.mdx:694area) 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. - Macro delimiter ambiguity.
_is both the{TMPX_<id>}separator and a legalprovider_idcharacter, so{TMPX_foo_bar}is ambiguous between providerfoo_barand a hypotheticalfoo+barscheme. Publisher trafficks the exact synthesized string so it's not a bug — worth one line given the charset was chosen for macro safety. - 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 landedMap<string, string>is correct in source, but confirm the rendered table matches.
LGTM. Land the propertyNames key constraint and the rest are follow-ups.
- 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>
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>
- 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>
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
tmpxfield onIdentityMatchResponsecarries 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_idhad no charset constraint, so{TMPX_<provider_id>}wasn't guaranteed to be a legal ad-server macro token. The fix is a charset pattern onprovider_idat 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 —
mergeIdentityResponsescurrently 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— addpattern: ^[A-Za-z0-9_]+$andmaxLength: 64toprovider_id, with description noting the trafficking role ({TMPX_<provider_id>}macro name).static/schemas/source/tmp/identity-match-response.json— addtmpx_providersas anadditionalProperties: stringmap ofprovider_id→ opaque TMPX wire string (kid.base64url_nopad(ciphertext)). The legacy single-stringtmpxfield is marked DEPRECATED with "Removed in 4.0" framing; routers MAY continue to populate it as a transitional convenience, withtmpx_providersauthoritative when both are present.docs/trusted-match/specification.mdx—tmpx_providers; thetmpxrow is updated with the deprecation notice.provider_idcharset constraint and its trafficking role (the schema this PR actually changes — corrects the cross-wire flagged in the bot review).{TMPX_<provider_id>}trafficking pattern, and points at the existing 255-char macro budget with priority-ordered truncation for size-budget cases.§Inventory-specific behaviorupdated to direct publishers at the{TMPX_<provider_id>}macro per provider and to requireprovider_idalongside each TMPX value in DOOH play logs.docs/trusted-match/router-architecture.mdx— adds a normativeTMPX collectionparagraph under §"Identity Match fan-out" that MUSTs the per-provider map on multi-provider fan-outs, allows the back-compattmpx, 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:
tmpx_macroson provider-registration).provider-registration.jsonis 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.Both are tracked for a follow-up issue.
Compatibility
Additive on the new field; the
provider_idcharset constraint is a new schema constraint on an existing field. Existing registrations that already use macro-safeprovider_idstrings (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 ontmpxends at 4.0.Test plan
npm run test:schemas— full schema validation suite passesnpm run lint:schema-links— cleannpm run test:docs-nav— clean🤖 Generated with Claude Code