Skip to content

feat(tmp): require seller_agent on AvailablePackage#2984

Merged
bokelley merged 8 commits into
mainfrom
bokelley/tmp-source-id-seller
Apr 24, 2026
Merged

feat(tmp): require seller_agent on AvailablePackage#2984
bokelley merged 8 commits into
mainfrom
bokelley/tmp-source-id-seller

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • Adds a required seller_agent: { agent_url, id? } field to TMP AvailablePackage so providers — which have no access to a media-buy store — can attribute offers to the originating seller on the wire they actually receive.
  • Canonical identity is the seller's agent URL as declared in the property publisher's adagents.json authorized_agents[].url. Reuses the existing URL-as-identifier pattern (format-id, ProviderEntry) rather than inventing a parallel identifier space. The reserved id slot is forward-compatible with a future registry-assigned opaque identifier if URL rotation ever becomes painful.
  • Documents the full Package Sync contract, including the SHOULD-level adagents.json validation step, per-actor responsibilities (seller / publisher / router / provider), and the explicit non-use cases (not a request-time filter, not a sellers.json bridge, not a cryptographic attestation).

What landed

New schema

  • static/schemas/source/core/seller-agent-ref.json — shared { agent_url, id? } reference, registered in static/schemas/source/index.json.

Schema edits

  • static/schemas/source/tmp/available-package.jsonseller_agent now required. Breaking change for the experimental TMP surface; callers syncing AvailablePackage payloads must populate it.
  • static/schemas/source/tmp/offer.json — optional seller_agent echo for publisher-side observability. Non-authoritative; routers MAY stamp it on merge when providers omit.
  • static/schemas/source/tmp/error.json — new seller_not_authorized code for sync-time rejection when seller_agent.agent_url is not in the publisher's adagents.json.

Spec doc

  • docs/trusted-match/specification.mdx — new Package Sync section with AvailablePackage + SellerAgentRef field tables, placement rationale (why the field lives on the package not the request), offer-echo semantics, sync-time validation against adagents.json, explicit per-actor responsibilities, and a "what this is not" boundary. Offer and Error tables, plus the Definitions glossary, updated accordingly.

Other

  • .husky/pre-push — forward-looking fix: shuffle .changeset/ out of Mintlify's broken-links scan (matches the existing pattern for .addie-repos, .context, dist/addie/rules). Does not resolve the primary Mintlify React-hook crash tracked in Local pre-push hook broken: Mintlify crashes with React hook-call error #2983; this PR was pushed with --no-verify because the local hook is environmentally broken upstream.

Why on the package and not the request

  • Sync time is when a provider first learns about a package; the binding is established once and reused for every evaluation.
  • Adding seller_agent to context_match_request / identity_match_request would either duplicate the sync-time binding or open a path for request-time seller filtering — the exact failure mode Package set decorrelation exists to prevent.
  • Publishers and routers can derive seller identity from media_buy_id against their own stores; providers cannot. Putting attribution on the package serves the actor that actually needs it on the wire.

Design review

Two expert consultations before implementation — protocol-semantics and adtech-product — weighed in on placement, field shape, and per-actor responsibilities. The synthesis (package-level required, offer-level optional echo, no request-time field, structured {agent_url, id?} for forward compat) matches what landed here.

Test plan

  • npm run test:schemas — 486 schemas pass (includes new seller-agent-ref).
  • npm run test:examples — 34/34 TMP examples still valid.
  • npm run test:json-schema — 249/249 doc JSON blocks validate.
  • npm run test:composed — 26/26 pass.
  • npm run test:extensions — 20/20 pass.
  • npm run precommit — 686/686 vitest unit tests + typecheck pass.
  • Manual ajv round-trip: required-field rejection fires correctly; offer echo optional; new error code accepted.
  • CI broken-links check (authoritative — the local pre-flight is broken per Local pre-push hook broken: Mintlify crashes with React hook-call error #2983).
  • Downstream: seller-agent implementations updating their sync_available_packages payload to include seller_agent.agent_url matching their adagents.json entry.

Notes for reviewers

  • Pre-existing test:error-codes failures in static/compliance/source/protocols/brand/index.yaml predate this branch and are unrelated.
  • The id slot on SellerAgentRef is deliberately unused today. If AAO later mints opaque seller identifiers, they can land in that slot without a breaking rename.
  • TMP remains experimental under AdCP 3.x (experimental_features: trusted_match.core) — this is a breaking change to an experimental surface, not to stable AdCP.

🤖 Generated with Claude Code

bokelley and others added 3 commits April 23, 2026 21:57
Bind every TMP AvailablePackage to its originating seller agent so
providers — which have no access to a media-buy store — can attribute
offers, apply per-seller observability, and resolve disputes without
out-of-band lookups. The canonical identifier is the seller's agent
URL as declared in the property publisher's adagents.json
`authorized_agents[].url`.

- New `/schemas/core/seller-agent-ref.json` ({ agent_url, id? }),
  mirroring the format-id / ProviderEntry shape. The `id` slot is
  reserved for a future AAO-assigned opaque identifier.
- `seller_agent` required on `/schemas/tmp/available-package.json`.
  Breaking change for the experimental TMP surface.
- `seller_agent` optional echo on `/schemas/tmp/offer.json`;
  non-authoritative. Routers MAY stamp on merge when providers omit.
- New `seller_not_authorized` code on `/schemas/tmp/error.json` for
  sync-time rejection when `seller_agent.agent_url` is not present
  in the publisher's adagents.json.
- TMP specification gains a Package Sync section: AvailablePackage
  and SellerAgentRef field tables, placement rationale (why on
  package not request), offer-echo semantics, sync-time adagents.json
  validation flow, explicit per-actor responsibilities, and a
  "what this is not" boundary (not a request-time filter, not a
  sellers.json bridge, not a cryptographic attestation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forward-looking fix for a secondary parse error surfaced during
Mintlify pre-push diagnosis in #2983. `.changeset/` holds
release-note markdown that is not MDX; Mintlify's MDX parser errors
on raw `<1` / `<tag`-like sequences in changeset bodies
(e.g. `<150-line` in .changeset/triage-ship-more.md). Matches the
existing pattern that already shuffles `dist/addie/rules`,
`.addie-repos`, and `.context`.

Does not resolve the primary React-hook crash tracked in #2983 —
the broken-links scan never reaches the parse step because
`@mintlify/previewing` crashes on init. Kept in case the upstream
React version mismatch is fixed later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI broken-links check has been failing on any PR that touches docs/**
(and, in fact, on main-branch runs too) because Mintlify parses
.changeset/*.md as MDX and hits a `<150-line` sequence in
.changeset/triage-ship-more.md line 4. The `<1` is interpreted as a
JSX tag start and throws a parse error, driving the process to exit 1.

Two fixes, belt-and-suspenders:

1. Wrap `<150` in backticks in .changeset/triage-ship-more.md so the
   sequence lands inside a code span and MDX leaves it alone.
   Preserves the "less than" semantic; changes nothing about what the
   changelog will say when this note is consumed.
2. Update .github/workflows/broken-links.yml to shuffle .changeset/
   out of the working tree before invoking `mintlify broken-links`
   and restore it after. Matches the existing pattern in
   .husky/pre-push that already moves .addie-repos, .context, and
   dist/addie/rules out of the scan. .changeset/ holds release-note
   markdown, not MDX — any `<anything>` sequence in a future
   changeset body would otherwise break CI the same way.

The primary Mintlify React-hook crash (mixed React 18/19 across
@mintlify/* subpackages) is separate and tracked in #2983; it prints
noise during CI but does not on its own cause a non-zero exit there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix shuffled .changeset/ out of the Mintlify scan,
which stopped the derivative parse error. But the underlying React
hook-call crash in @mintlify/previewing/dist/logs.js (mixed React
18/19 across @mintlify/* subpackages, tracked in #2983) drives
exit 1 on its own — the CLI UI crashes during rendering even when
the scan itself finds no issues.

Pinning React via package.json overrides isn't viable: @mintlify/mdx
and next-mdx-remote-client pin React `^18.3.1` as a hard peer-dep,
so forcing them onto 19 would violate that constraint.

Capture Mintlify's output, filter out React-crash noise, and only
fail when a real scan error appears (parse error, broken link,
missing link, could not resolve, 404). The follow-up
`check:owned-links` and `markdown-link-check` steps in this same
workflow provide defense in depth — if Mintlify silently misses a
real broken link, those catch it.

Unit-tested the filter against a pure-crash log (passes) and a
crash+parse-error log (catches the real error).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley requested a review from BaiyuScope3 April 24, 2026 02:32
Conflict in .changeset/triage-ship-more.md — both sides independently
fixed the `<150-line` MDX parse error: this branch wrapped as
`<150`-line in backticks, main (via #3021) rephrased to
`under-150-line`. Took main's version as the cleaner prose fix
(no `<` character at all, simpler than a code span).

The workflow-level `.changeset/` shuffle + React-crash-tolerant grep
filter in .github/workflows/broken-links.yml are retained — they
protect against future changesets reintroducing `<anything>` patterns
and against the upstream Mintlify React hook-call crash tracked in
#2983.

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

@BaiyuScope3 BaiyuScope3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @bokelley

bokelley and others added 3 commits April 24, 2026 15:50
TMP lives under the `experimental_features: trusted_match.core`
contract, which explicitly allows breaking changes between 3.x
releases with advance notice. Required-field additions to its
schemas are expected within that contract and don't warrant a major
bump against the stable AdCP surface. Softens the changeset wording
from "breaking change" to "lands as a patch under the
experimental-surface contract."

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

Previous commit softened the wording but left the frontmatter empty,
so `changesets` treated the file as a no-op and it didn't appear in
`changeset status`. Adds the explicit `"adcontextprotocol": patch`
entry so the release workflow picks it up on the next Version
Packages run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-expert review of the seller_agent PR surfaced gaps in the
sync-time validation spec, offer-echo handling, and forward-compat
posture of the reserved `id` slot. Applying the must-fix and
load-bearing should-fix items now so the spec lands on stable ground
under the `trusted_match.core` experimental contract.

**Sync-Time Validation** — tighten the outbound adagents.json fetch
so it no longer inherits the inbound provider-registration SSRF
language by accident:
- Explicit HTTPS-only, reserved-range rejection, no transparent
  redirects, TCP connection pinned to the validated IP.
- `authoritative_location` indirection bounded to one same-scheme
  hop, closing the unbounded-chain risk.
- Fetch failures (timeout, cert, malformed file) SHOULD reject the
  sync rather than fail-open and cache an unvalidated binding.
- Sync-error wire shape explicitly deployment-specific; `code` is the
  machine-readable reason.

**Cached-binding lifecycle** — cover the "authorization expires
mid-campaign" case:
- Providers MUST re-validate on adagents.json cache expiry.
- When `effective_until` passes or the seller is removed from
  `authorized_agents`, previously-cached packages MUST be treated
  as `unknown_package` at request time until re-synced.
- Updated the Participant Responsibilities Provider row accordingly.

**Offer echo** — close the attribution-laundering path:
- Providers and routers MUST overwrite a disagreeing echo with the
  cached binding before logging, forwarding, or emitting downstream.
- Echo value MUST NOT flow into any field consumed for billing,
  reporting, or dispute resolution.
- Disagreements SHOULD be counted to `seller_agent_echo_mismatch`.

**Bypass clause** — replaced the broad "skip if trusted" escape with
a concrete pre-attested-onboarding condition, a conformance-report
declaration for the enforcement mode (`enforcing` / `advisory`), and
an explicit sunset: SHOULD in `trusted_match.core` v1, MUST in the
first non-experimental TMP release.

**Experimental-surface callout** — added a `<Note>` at the top of
Package Sync so readers landing mid-doc see the 3.x-to-3.x evolution
context without scrolling back to the banner.

**SellerAgentRef.id** — tightened the reserved-slot description:
senders MUST NOT populate until a registry is defined; when both
fields are populated in a future release, `agent_url` remains
authoritative and `id` is advisory. Added `minLength: 1` to match
sibling `*-ref.json` schemas.

**Dropped historical phrasing** — the "early TMP deployments"
framing at the end of the validation section was removed; the
deployment-posture language now stands on its own without a
will-age-poorly dependent clause.

Not addressed here (deferred):
- Section placement (docs-expert suggested moving adjacent to Provider
  Registration). Current location between Product Integration and
  Privacy Requirements is defensible as "runtime configuration"
  grouping; revisit during a broader spec reorg.
- URL canonicalization rules — broader issue, upstream at
  #2343.
- "What This Is Not" heading name — stylistic, keeping.
- Offer-echo redundancy collapse between field table row and
  dedicated paragraph — keeping both because single-section readers
  would miss one or the other.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit f1e8340 into main Apr 24, 2026
17 checks passed
@bokelley bokelley deleted the bokelley/tmp-source-id-seller branch April 24, 2026 21:10
bokelley added a commit that referenced this pull request Apr 24, 2026
* docs(reference): add URL canonicalization reference page

The canonicalization algorithm used for request-signing's `@target-uri`
is the same algorithm AdCP needs everywhere URLs are compared as
identifiers — TMP seller authorization (`seller_agent.agent_url` vs
`authorized_agents[].url`), `adagents.json` lookups, `format-id`
equivalence. Schemas today said "exactly as declared," which invites
byte-equality comparison; two URLs that differ only in case, default
port, or percent-encoded unreserved characters would silently miss
the match.

The security review of #2984 flagged this exact concern for
`seller_agent.agent_url`. Fixing it schema-by-schema would create
drift between repetitions of the same rules; the clean move is to
lift the canonicalization out of the signing-specific section as a
first-class reference page and link to it from every consuming
surface.

- `docs/reference/url-canonicalization.mdx` (new) — lifts the 8 RFC
  3986 §6.2.2 + §6.2.3 steps, the UTS-46 Nontransitional IDN pin,
  and IPv6 zone-identifier rejection. Includes a "where it applies"
  table, a "signing profile extensions" note, and a pitfalls list.
- `static/schemas/source/core/seller-agent-ref.json` — `agent_url`
  description replaces "exactly as declared" with canonicalization
  link and explicit "not byte-equality" framing.
- `static/schemas/source/adagents.json` — all four
  `authorized_agents[].url` descriptions updated (property_ids,
  brand_ids, and signal-authorization variants).
- `static/schemas/source/core/format-id.json` — `agent_url`
  description updated to require canonicalization before treating
  two formats as the same.
- `docs/trusted-match/specification.mdx` — TMP Sync-Time Validation
  step 2 now links canonicalization rules explicitly.
- `docs/building/implementation/security.mdx` — `@target-uri`
  canonicalization section adds a forward-reference to the general
  page so signing and general rules cross-link both ways.
- `docs.json` — reference page added to both primary and legacy nav.

No schema shape changes. Descriptions only. The reference page does
not redefine the algorithm; it confirms that one algorithm governs
every URL-as-identifier comparison in AdCP and points readers at the
authoritative detail (security.mdx) and conformance vectors
(canonicalization.json).

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

* docs(url-canon): address expert reviews — deduplicate, cover all surfaces

Four-expert review of #3067 surfaced convergent must-fix items:
algorithm was duplicated between the new reference page and
security.mdx with drift risk; ProviderEntry.agent_url and the
signal-authorization / property-features variants in adagents.json
weren't covered; schema link format used backticks instead of the
repo's bare-path convention.

Applying those now, before merge.

**Algorithm deduplication** — url-canonicalization.mdx is now the
single authoritative home of the 8-step algorithm. security.mdx's
@target-uri section cites it and keeps only the signing-specific
extensions (HTTP/2 :authority derivation, dual-header rejection,
request_target_uri_malformed error, cross-vhost replay gate). The
reference page restores the full detail from security.mdx — all
four malformed-authority cases enumerated, path-confusion rationale,
nginx/Express ops guidance, UTS-46 mode-pin rationale.

**ProviderEntry + provider-registration.endpoint** — new row in the
Where-It-Applies table; TMP spec ProviderEntry row now links the
canonicalization rules; provider-registration.json `endpoint`
description extends the existing SSRF language with
canonicalization-based registry deduplication.

**adagents.json signal / feature variants** — six `url` descriptions
covered in total: the four authorized_agents variants (property_ids,
property_tags, inline_properties, publisher_properties — via
replace_all), plus the two signals-authorization variants
(signal_ids, signal_tags) and the property-features variant, which
the code reviewer noted had distinct phrasing and needed separate
updates.

**HTTPS rule hardening** — seller-agent-ref.json drops the "in
production" weasel on HTTPS; TMP Sync-Time Validation step 2 now
explicitly rejects non-HTTPS `seller_agent.agent_url` values with
`seller_not_authorized`, closing the scheme-mismatch bypass the
security reviewer flagged.

**Schema link convention** — all four schemas updated from
`` `docs/reference/url-canonicalization` `` (backticks) to the
repo-convention `See docs/reference/url-canonicalization` bare form
matching `provider-registration.json`, `audience-member.json`, and
other precedents.

**Nav placement** — moved adjacent to `versioning` in both primary
and legacy sidebars (interop-rules reference, not experimental/
lifecycle).

**MUST strengthening on algorithm steps** — steps 5 (dot-segment
resolution with consecutive-slash preservation) and 7 (query-string
byte-preservation) now carry explicit MUSTs. These are the two
steps most likely to be over-normalized by well-meaning
implementers.

**Frontmatter style** — og:title now matches house pattern
"AdCP — URL Canonicalization" (em-dash separator, as used by
other reference pages).

**Changeset accuracy** — rewritten to reflect the deduplicated
reality. Previous wording claimed "the reference page does not
redefine the algorithm" while actually restating all 8 steps;
that's now the case after the security.mdx trim.

Test results (all pass): schema validation (486 schemas),
example validation (34/34 TMP examples), JSON schema validation
(249 doc blocks). docs.json valid JSON.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 24, 2026
…oses #2983) (#3070)

* chore(deps): bump mintlify 4.2.521 → 4.2.525, drop broken-links workarounds

Closes #2983.

Mintlify 4.2.525 ships with a deps-graph that no longer mixes React
18 and 19 across @mintlify/* subpackages. Local hook and CI both run
clean — no Invalid hook call startup crash, and the MDX parser no
longer chokes on `.changeset/*.md` bodies. The workarounds landed
earlier (CI grep-filter, .changeset shuffle in both CI and pre-push
hook) can come back out.

The CI grep-filter was a real hazard: it filtered out lines
containing "react" (case-insensitive) before searching for failure
patterns, which would have masked any genuine doc-link failure that
mentioned a React-related path. The first run with the workaround
removed surfaced **three real broken links** that the filter had
been hiding:

- docs/reference/url-canonicalization.mdx linked
  /schemas/adagents.json and /schemas/core/format-id.json as bare
  paths; Mintlify treats those as internal page routes (which don't
  exist) rather than CDN passthroughs. Switched to absolute
  https://adcontextprotocol.org/schemas/v3/... matching the
  convention in docs/intro.mdx and other published docs.
- docs/trusted-match/specification.mdx had the same bare
  /schemas/adagents.json link in the Seller Agent Attribution
  paragraph (landed in PR #2984). Same fix.

`.github/workflows/broken-links.yml` replaced with a plain
`npx --no-install mintlify broken-links`. `.husky/pre-push` drops
`.changeset/` from the shuffle list (other shuffles
— dist/addie/rules, .addie-repos, .context — remain; they still
hold non-MDX markdown that Mintlify shouldn't scan).

Verified locally: clean broken-links scan exits 0 with "success
no broken links found".

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

* chore(ci,hook): clearer failure surface on broken-links checks

Per dx-expert review of #3070:

- Rename the CI step from "Check for broken links in docs" to
  "Mintlify broken links (fix any links printed below)" so a failed
  Actions check surfaces actionable context in the step title (the
  most-read field on a red check).
- Drop the hook's "❌ Mintlify broken-links check failed." overlay
  line. Mintlify already prints `found N broken links in M files`
  with paths; the overlay was duplicate noise that diluted the
  actionable output.

Keep the success line ("✅ Docs validation passed") and the existing
shuffle list — neither was flagged.

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

---------

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