Skip to content

feat(adagents): publisher_domains compact form, revoked_publisher_domains, streaming fetch caps (closes #729)#753

Merged
bokelley merged 2 commits into
mainfrom
bokelley/issue-729-adagents-scope
May 20, 2026
Merged

feat(adagents): publisher_domains compact form, revoked_publisher_domains, streaming fetch caps (closes #729)#753
bokelley merged 2 commits into
mainfrom
bokelley/issue-729-adagents-scope

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Implements the full scope of issue #729 (adcp#4504 — publisher_domains[] compact form on publisher_properties selectors) plus the scope-expansion item from the issue thread (top-level revoked_publisher_domains[] on adagents.json with a Reason enum), and rewrites the outbound fetch path with two-tier streaming size caps and an If-None-Match / If-Modified-Since cache.

Built in two commits:

  1. 5160ab0b — feature implementation (schemas, generated types, validator, resolver, fetch rewrite, 24 new tests + 122 ported).
  2. fcb967c1 — review fixups after running the diff through three expert subagents (protocol, security, code review).

What's in scope

Schema (cached, hand-patched for 3.0):

  • publisher-property-selector.json gains optional publisher_domains[] (XOR with publisher_domain, by_id selectors excluded).
  • adagents.json gains top-level revoked_publisher_domains[] with Reason enum.

The cached schemas will be overwritten on the next make regenerate-schemas once upstream 3.0.10+ ships and ADCP_VERSION is bumped — the runtime / test changes are independent of the cached schema text.

Runtime validation (validation/legacy.py):

  • validate_publisher_properties_item enforces both XORs (selector and publisher_domain).
  • validate_revoked_publisher_domain_entry enforces shape + Reason enum.
  • validate_adagents plumbing fixed (was keyed off agents instead of authorized_agents — pre-existing typo that made the new XOR validator dead at the integration level until this PR).

Resolver (adagents.py):

  • _fanout_publisher_properties expands compact entries one-per-domain, preserving order across mixed compact+expanded arrays.
  • filter_revoked_selectors + _get_revoked_publisher_domains apply revocation precedence to both publisher_properties[] selectors and top-level properties[].

Fetch path (adagents.py):

  • Rewritten onto httpx.AsyncClient.stream with two-tier caps (MAX_POINTER_BYTES=5MiB first hop, MAX_AUTHORITATIVE_BYTES=20MiB second hop).
  • Content-Length pre-check + per-chunk running-total guard.
  • follow_redirects=False so HTTP 30x cannot bypass the SSRF gate that protects authoritative_location (the only sanctioned cross-host delegation path).
  • _validate_publisher_domain now calls the same SSRF gate as _validate_redirect_url (factored into _check_safe_host), so first-hop IP literals and internal hostnames (localhost, .local, .internal, metadata.google.internal, RFC 1918, link-local, multicast, reserved) are rejected.
  • New AdagentsCacheEntry / AdagentsFetchResult types + fetch_adagents_with_cache send If-None-Match / If-Modified-Since; 304 → cache-lifetime refresh. Cache validators truncated above 256 bytes before persisting.

Expert review — closed in this PR

Severity Finding Resolution
Security · Critical follow_redirects=True bypasses SSRF gate on HTTP 30x Set False; only authoritative_location follows.
Security · Critical First-hop publisher domain has no IP-literal gate _check_safe_host called from _validate_publisher_domain.
Security · High Etag/Last-Modified replayed unbounded 256-byte cap on persisted validators.
Security · Medium except Exception + unbounded body in error except json.JSONDecodeError, message truncated to 200 chars.
Protocol · Blocker validate_adagents walked agents not authorized_agents Fixed; integration validator now actually runs.
Code · Must-fix Dead "304/" token in redirect-hop error Removed.
Code · Must-fix Dead is_redirect ternary in not_modified return Removed; only first-hop 304 is reachable.
Code · Must-fix Misleading "original retained" comment on _fanout Comment fixed.
Code · Should-fix _REVOCATION_REASONS drift risk vs generated enum Drift-detection unit test added.
Code · Should-fix Streaming-cap test passed even if running-total guard deleted Multi-chunk test added.
Code · Should-fix No mixed-compact-and-expanded test Added.
Code · Should-fix No last_modified-only cache test Added.

Deferred follow-ups (tracked, not blocking)

  • DNS pinning to harden against rebinding (security H1) — needs a custom httpx transport.
  • Pydantic model_validator for selector XOR — datamodel-codegen can't emit allOf[not[required[both]]] + anyOf[required[either]], so the typed Pydantic surface is laxer than the JSON Schema. The runtime validator covers the dict-parsing path; typed consumers will need an explicit gate.
  • Revocation filtering on the inline_properties branchfilter_revoked_selectors covers publisher_properties[] and the top-level properties[]; the inline path is currently unfiltered. Worth confirming spec intent before adding.
  • AdagentsCacheEntry.body vs AdagentsFetchResult.data field-name inconsistency — defer to a follow-up rename to avoid bouncing the public API in this PR.

Test plan

  • ruff check src/ tests/test_adagents.py — passes
  • mypy src/adcp/ — 807 source files, no issues
  • pytest tests/ — 4,821 passed, 34 skipped, 1 xfailed (no failures, no flakes)
  • 146 adagents tests pass: 122 ported onto the streaming mock helper, 24 new across four classes (compact form, revoked domains, fetch-with-cache, size caps)
  • CI green across Python 3.10–3.13

Notes for reviewers

  • per-authorized_agents[] last_updated is intentionally omitted — the merged upstream spec PR does not carry that field, confirmed against the latest upstream schema. The original issue thread discussed it; the spec community removed it.
  • The new public exports in adcp.__init__.py: AdagentsCacheEntry, AdagentsFetchResult, fetch_adagents_with_cache, filter_revoked_selectors. Public-API snapshot updated.

🤖 Generated with Claude Code

bokelley and others added 2 commits May 20, 2026 07:41
…ains, streaming fetch caps (#729)

Implements full scope of issue #729 plus the scope-expansion comment:

- publisher-property-selector schema gains optional publisher_domains[] compact
  form (XOR with publisher_domain; by_id selectors excluded from compact form).
- adagents.json gains top-level revoked_publisher_domains[] with Reason enum.
- Resolver: _fanout_publisher_properties expands compact entries one-per-domain;
  filter_revoked_selectors applies revocation precedence over both
  publisher_properties[] selectors and top-level properties[].
- Fetch path rewritten onto httpx.AsyncClient.stream with two-tier size caps
  (MAX_POINTER_BYTES=5MiB first hop, MAX_AUTHORITATIVE_BYTES=20MiB second hop)
  and Content-Length pre-check. New fetch_adagents_with_cache sends
  If-None-Match / If-Modified-Since; 304 treated as cache-lifetime refresh.
- Runtime validation: two XORs enforced on publisher_properties[] entries;
  by_id rejects publisher_domains; validate_revoked_publisher_domain_entry
  plumbed into validate_adagents.
- 24 new tests across four classes; 122 existing adagents tests ported onto
  the new streaming mock helper.

Cached schemas under schemas/cache/3.0/ are hand-patched and will be
overwritten by the next \`make regenerate-schemas\` once upstream 3.0.10+
ships and ADCP_VERSION is bumped — that's fine; the runtime/test changes
are independent of the cached schema text.

Per-authorized_agents[] last_updated (also discussed in the spec PR thread)
is intentionally not included — the merged upstream spec PR does not carry
that field, confirmed against the latest schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three reviewer passes (protocol, security, code) on 5160ab0 surfaced one
must-fix per area; this commit closes them.

Security:
- `_stream_capped` now passes `follow_redirects=False`. HTTP 30x cannot be
  used to slip past the SSRF gate that protects `authoritative_location`
  (which is the only sanctioned cross-host delegation path).
- `_validate_publisher_domain` calls the same SSRF gate (`_check_safe_host`)
  used for redirect targets, so IP literals and internal hostnames are
  rejected on the first hop too. `is_multicast` added to the IP class set.
- Cache validators (ETag / Last-Modified) are truncated above 256 bytes
  before being persisted, so a hostile origin cannot inflate every
  subsequent conditional request.
- The JSON-decode error path catches `json.JSONDecodeError` (not bare
  `Exception`) and truncates the upstream-derived message to 200 chars
  to bound log injection.

Protocol:
- `validate_adagents` keyed off `agents`; the schema field is
  `authorized_agents`. The new `validate_publisher_properties_item` XOR
  enforcer was therefore dead at the integration level. Now lit up.

Code review:
- Drop dead "304/" token from the redirect-hop error message.
- Drop dead ternary in the 304-on-first-hop return — `is_redirect` can
  never be true there.
- Fix misleading comment on `_fanout_publisher_properties` (the original
  compact entry is *not* retained — the docstring claimed otherwise).
- Add drift-detection test that asserts `_REVOCATION_REASONS` matches
  the generated `Reason` enum.

Tests:
- Multi-chunk streaming-cap test (exercises the running-total guard
  without relying on a single oversized chunk).
- Mixed compact + expanded entries in the same `publisher_properties[]`
  array, with order preservation.
- Cache entry carrying only `Last-Modified` (no ETag) — verify the
  conditional headers and the 304 cache-hit path.
- Three existing test fixtures updated to be schema-compliant
  (`authorization_type` + `property_ids`) so the now-live integration
  validator no longer rejects them as pre-discriminator legacy.

Deferred follow-ups (not blocking this PR):
- DNS pinning to harden against rebinding attacks (security H1).
- Pydantic `model_validator` for selector XOR — datamodel-codegen can't
  emit `allOf[not[required[both]]]`; the runtime validator covers the
  dict path, but typed Pydantic consumers still need an explicit gate.
- Revocation filtering on the `inline_properties` branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 352d1bb into main May 20, 2026
16 checks passed
@bokelley bokelley deleted the bokelley/issue-729-adagents-scope branch May 20, 2026 17:12
bokelley added a commit that referenced this pull request May 20, 2026
#754)

Mirrors the adagents.json streaming-fetch posture (`_stream_capped` uses
`follow_redirects=False`): ads.txt is a publisher-controlled URL and a
transparent 30x by httpx would bypass `_check_safe_host` on the
resolved Location, re-opening the same SSRF surface the main path
already closed.

Surfaced as a defense-in-depth follow-up by the round-2 security
review on PR #753 — was pre-existing behavior, not regressed by that
PR, but worth closing.

ads.txt fetch is best-effort by construction (any non-200 returns an
empty MANAGERDOMAIN list); publishers who 30x their ads.txt now fall
through to "no managerdomain found", and the SDK surfaces the
publisher's original 404 — the same outcome as if the publisher had
no ads.txt at all. Acceptable for a fallback path.

Adds `test_ads_txt_30x_is_not_followed` covering the new behavior.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 20, 2026
… models (#756)

Closes the documented Pydantic-XOR gap surfaced by the round-1 protocol
review on #729 / PR #753.

datamodel-codegen cannot translate the publisher-property-selector
JSON Schema's `allOf[not[required[both]]] + anyOf[required[either]]`
construct into Pydantic field constraints, so direct instantiation of
`PublisherPropertySelector1` / `…3` silently accepts payloads the schema
rejects (notably `{selection_type: "all"}` with neither
`publisher_domain` nor `publisher_domains` set).

This change lets Pydantic consumers close the gap with a single call:

    selector = PublisherPropertySelector1.model_validate(payload)
    validate_publisher_properties_item(selector)  # raises if XOR fails

The helper now accepts either a dict (existing wire-form path) or any
object with a `.model_dump()` method (i.e. a Pydantic model). For
anything else it raises a clear error.

Tests cover both the model and the dict input forms plus the
type-error path. Auto-enforcement at parse time (a `model_validator`
attached post-class to the generated selectors) would require either
hand-patching generated_poc/ — forbidden by the codebase's strict
layering rule — or hooking Pydantic core-schema internals, which is
fragile. A separate follow-up issue tracks that work.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 21, 2026
…tors (#749 Part 1)

Layers inline-resolution on top of PR #753's publisher_domains[] fan-out
and revoked_publisher_domains[] support. _resolve_agent_properties now
returns resolved property dicts (not selector dicts) for publisher_properties
authorization_type, sourced from the parent file's top-level properties[]
indexed by publisher_domain.

- Pre-builds domain → properties index once per call so per-domain lookups
  are O(1) — avoids O(N×M) at cafemedia scale (6,843 properties × 6,800
  domains = ~46M comparisons under the old linear scan).
- Inline resolution honors revoked_publisher_domains[] transparently via
  the existing revoked_top_level pre-filter (revoked domains never enter
  the per-domain index, so they're skipped by construction).
- Fail-closed on unknown selection_type and empty selector criteria
  (property_tags=[] / property_ids=[]) per CLAUDE.md "no fallbacks" in
  authorization-decision paths. Fail-fast on properties missing the
  required property_id field.
- Cafemedia/interchange.io scale test (6,843 properties × 6,800 domains)
  is wall-clock-bounded to catch O(N×M) regressions.
- Two pre-existing TestRevokedPublisherDomains tests + the
  TestPublisherDomainsCompactForm resolution test updated to assert on
  resolved property dicts (the new contract) instead of selector dicts.
- Dead filter_revoked_selectors post-pass removed from
  get_properties_by_agent and get_all_properties — revocation is enforced
  upstream via the index, so the post-filter was structurally redundant.

Closes #749 Part 1. Federated fallback (when inline yields no match for a
domain) lives in companion PR #752's async helpers.
bokelley added a commit that referenced this pull request May 21, 2026
… 2+3 of #749)

Squashed onto main to resolve conflicts with #753 (publisher_domains compact
form, revoked_publisher_domains) and main HEAD. Final state of this PR:

- fetch_agent_authorizations_from_directory: async wrapper for GET /v1/agents/
  {agent_url}/publishers (per adcp#4823 / adcp#4828). Returns AgentDirectoryLookup
  with spec-conformant envelope (agent_url, directory_indexed_at, publishers,
  next_cursor). Accepts include=["properties"] to request per-publisher
  property_ids[] (per adcp#4894).
- detect_publisher_properties_divergence: full (publisher_domain, property_id)
  set-diff when directory returns property_ids[]; falls back to count-only
  comparison against directories that haven't deployed adcp#4894 yet.
  max_concurrency=20 default semaphore caps concurrent fetches at managed-
  network scale. sample_size=200 default opt-in for full sweep.
- AgentPublisherEntry / AgentDirectoryLookup / PublisherDivergence /
  DivergenceReport dataclasses; DiscoveryMethod / PublisherStatus Literal enums.
- AAO_PUBLISHER_DIRECTORY_URL env var supported (matches salesagent convention).
- Tests use respx.mock against spec-shaped JSON payloads.

Closes #749 Parts 2 and 3. Companion to #750 (Part 1: inline-resolution).
bokelley added a commit that referenced this pull request May 21, 2026
…tors (#749 Part 1)

Layers inline-resolution on top of PR #753's publisher_domains[] fan-out
and revoked_publisher_domains[] support. _resolve_agent_properties now
returns resolved property dicts (not selector dicts) for publisher_properties
authorization_type, sourced from the parent file's top-level properties[]
indexed by publisher_domain.

- Pre-builds domain → properties index once per call so per-domain lookups
  are O(1) — avoids O(N×M) at cafemedia scale (6,843 properties × 6,800
  domains = ~46M comparisons under the old linear scan).
- Inline resolution honors revoked_publisher_domains[] transparently via
  the existing revoked_top_level pre-filter (revoked domains never enter
  the per-domain index, so they're skipped by construction).
- Fail-closed on unknown selection_type and empty selector criteria
  (property_tags=[] / property_ids=[]) per CLAUDE.md "no fallbacks" in
  authorization-decision paths. Fail-fast on properties missing the
  required property_id field.
- Cafemedia/interchange.io scale test (6,843 properties × 6,800 domains)
  is wall-clock-bounded to catch O(N×M) regressions.
- Two pre-existing TestRevokedPublisherDomains tests + the
  TestPublisherDomainsCompactForm resolution test updated to assert on
  resolved property dicts (the new contract) instead of selector dicts.
- Dead filter_revoked_selectors post-pass removed from
  get_properties_by_agent and get_all_properties — revocation is enforced
  upstream via the index, so the post-filter was structurally redundant.

Closes #749 Part 1. Federated fallback (when inline yields no match for a
domain) lives in companion PR #752's async helpers.
bokelley added a commit that referenced this pull request May 22, 2026
…tors (#749 Part 1)

Layers inline-resolution on top of PR #753's publisher_domains[] fan-out
and revoked_publisher_domains[] support. _resolve_agent_properties now
returns resolved property dicts (not selector dicts) for publisher_properties
authorization_type, sourced from the parent file's top-level properties[]
indexed by publisher_domain.

- Pre-builds domain → properties index once per call so per-domain lookups
  are O(1) — avoids O(N×M) at cafemedia scale (6,843 properties × 6,800
  domains = ~46M comparisons under the old linear scan).
- Inline resolution honors revoked_publisher_domains[] transparently via
  the existing revoked_top_level pre-filter (revoked domains never enter
  the per-domain index, so they're skipped by construction).
- Fail-closed on unknown selection_type and empty selector criteria
  (property_tags=[] / property_ids=[]) per CLAUDE.md "no fallbacks" in
  authorization-decision paths. Fail-fast on properties missing the
  required property_id field.
- Cafemedia/interchange.io scale test (6,843 properties × 6,800 domains)
  is wall-clock-bounded to catch O(N×M) regressions.
- Two pre-existing TestRevokedPublisherDomains tests + the
  TestPublisherDomainsCompactForm resolution test updated to assert on
  resolved property dicts (the new contract) instead of selector dicts.
- Dead filter_revoked_selectors post-pass removed from
  get_properties_by_agent and get_all_properties — revocation is enforced
  upstream via the index, so the post-filter was structurally redundant.

Closes #749 Part 1. Federated fallback (when inline yields no match for a
domain) lives in companion PR #752's async helpers.
bokelley added a commit that referenced this pull request May 22, 2026
…ors + publisher_domains[] fan-out (#750)

* feat(adagents): inline-resolution path for publisher_properties selectors (#749 Part 1)

Layers inline-resolution on top of PR #753's publisher_domains[] fan-out
and revoked_publisher_domains[] support. _resolve_agent_properties now
returns resolved property dicts (not selector dicts) for publisher_properties
authorization_type, sourced from the parent file's top-level properties[]
indexed by publisher_domain.

- Pre-builds domain → properties index once per call so per-domain lookups
  are O(1) — avoids O(N×M) at cafemedia scale (6,843 properties × 6,800
  domains = ~46M comparisons under the old linear scan).
- Inline resolution honors revoked_publisher_domains[] transparently via
  the existing revoked_top_level pre-filter (revoked domains never enter
  the per-domain index, so they're skipped by construction).
- Fail-closed on unknown selection_type and empty selector criteria
  (property_tags=[] / property_ids=[]) per CLAUDE.md "no fallbacks" in
  authorization-decision paths. Fail-fast on properties missing the
  required property_id field.
- Cafemedia/interchange.io scale test (6,843 properties × 6,800 domains)
  is wall-clock-bounded to catch O(N×M) regressions.
- Two pre-existing TestRevokedPublisherDomains tests + the
  TestPublisherDomainsCompactForm resolution test updated to assert on
  resolved property dicts (the new contract) instead of selector dicts.
- Dead filter_revoked_selectors post-pass removed from
  get_properties_by_agent and get_all_properties — revocation is enforced
  upstream via the index, so the post-filter was structurally redundant.

Closes #749 Part 1. Federated fallback (when inline yields no match for a
domain) lives in companion PR #752's async helpers.

* refactor(adagents): address review follow-ups on #750

- Cite adcp#4827 in _resolve_publisher_property_selectors docstring.
- isinstance(list) guard on property_tags / property_ids in both
  publisher_properties resolution and the pre-existing property_tags /
  property_ids authorization branches — string-iterating-as-tags is now
  caught and resolves to [].
- Lift domain index build out of per-agent loop; _build_domain_index
  module helper used by both get_all_properties and get_properties_by_agent,
  index passed through _resolve_agent_properties → _resolve_publisher_
  property_selectors. O(N+M) instead of O(agents × N).
- Document "no federated fallback" on get_properties_by_agent and
  get_all_properties docstrings; point at PR #752's federated helpers.
- Cafemedia scale test wall-clock budget 2.0s → 5.0s for shared-runner
  margin.

Bot review: #750
bokelley added a commit that referenced this pull request May 22, 2026
* chore(schemas): regenerate from ADCP 3.0.12 bundle

Bumps src/adcp/ADCP_VERSION from 3.0.7 to 3.0.12 and re-syncs all
generated artifacts (schemas/cache/, src/adcp/types/_generated.py,
src/adcp/types/generated_poc/, src/adcp/types/_ergonomic.py,
SCHEMA_DELTAS.md, skills/) from the upstream protocol bundle.

Upstream additions:
- tmp/identity-match-request.json: +seller_agent_url (required),
  package_ids now optional (was required).
- protocol/get-adcp-capabilities-response.json: account.supported_billing
  is now conditionally required when supported_protocols contains
  "media_buy" (encoded via allOf/if/then).
- adcp/legacy webhook authentication (Bearer/HMAC-SHA256) marked
  deprecated in descriptions; RFC 9421 is the preferred profile.
- creative/asset-types/index.json: lastUpdated bumped 2026-05-08 to
  2026-05-13.

Upstream regressions vs. the PR #753 hand-patched cache:
- adagents.json: revoked_publisher_domains[] is NOT in upstream 3.0.12
  (anticipated landing pre-3.0.10 didn't happen).
- core/publisher-property-selector.json: publisher_domains[] compact
  form is NOT in upstream 3.0.12 (same anticipated landing missed).

Per issue #771 the right call is to ship the regen and document the
divergence rather than re-apply the hand-patch and wait for 3.0.13+.
The SDK-side dict-layer helpers (validate_publisher_properties_item,
validate_revoked_publisher_domain_entry, _fanout_publisher_properties,
get_properties_by_agent) still implement the compact-form / revocation
contract for adopters consuming raw adagents.json bytes — only the
Pydantic-model layer loses the field.

Closes #771

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

* test: align fixtures with ADCP 3.0.12 generated types

Four test adjustments tracking the schema regen from the previous
commit:

- tests/test_client.py: IdentityMatchRequest fixture gains
  seller_agent_url (now required upstream).
- tests/test_capabilities_response_shape_validation.py: the
  ``account``/``supported_billing`` invariant assertions also accept
  the structured ``issues[]`` blob, since upstream 3.0.12 encodes the
  requirement via allOf/if/then and the schema-driven step now reports
  the missing ``/account`` pointer before the explicit invariant check
  fires.
- tests/test_adagents.py: drop the dead ``Reason`` enum cross-check
  (upstream removed RevokedPublisherDomain.reason); the SDK's
  ``_REVOCATION_REASONS`` frozenset is pinned directly. Drop the
  ``PublisherPropertySelector1`` XOR-from-Pydantic case (the field
  ``publisher_domains`` no longer exists on the generated model).
- tests/test_publisher_selector_xor_autoenforce.py: skip the four
  compact-form cases with a pointer back to issue #771. The
  Pydantic-layer XOR patch in ``adcp.types.aliases`` has no field to
  fire on; SDK-layer enforcement still lives in
  ``adcp.validation.legacy.validate_publisher_properties_item``.

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 May 22, 2026
…dits (#795)

* feat(schemas): patches/ post-process infrastructure

Hand-edits to the regenerated schema cache used to get silently
overwritten by ``make regenerate-schemas`` — that's exactly how PR #753's
forward-looking ``revoked_publisher_domains[]`` + ``publisher_domains[]``
compact-form patches got wiped on PR #791's bump to 3.0.12. The patches
were invisible to anyone running the regen, and the loss only surfaced
on line-by-line diff review of the regen output.

This introduces ``schemas/patches/`` as a tracked, reviewable layer of
hand-edits applied AFTER the upstream-verbatim extraction in
``scripts/sync_schemas.py``. Each ``.patch`` file is a unified diff with
a comment header (Patch / Reason / Filed / Upstream status / Drop when),
applied in lex order from the repo root via ``patch -p1``.

The state machine in ``apply_tracked_patches`` classifies each patch as:

- **Alive** — forward-applies cleanly → apply, continue.
- **Dead** — reverse-applies cleanly (upstream landed it) → exit
  non-zero with the patch name + directive to delete the file.
- **Broken** — neither direction applies → exit non-zero with the patch
  name + directive to either update the hunks or remove the patch.

Dead and broken both fail loudly because silently no-op'ing on dead
would let stale ``.patch`` files linger forever, and silently skipping
broken would let the SDK ship a cache whose patched fields don't
actually exist in the working tree.

Patch-application runs ONCE at the end of ``main()`` (after all
primary + preview bundles have been extracted), not inside ``_sync_one``
per-bundle. Per-bundle would misclassify a 3.0 patch as "dead" during
the subsequent 3.1 preview pass because the cache wouldn't reset
between passes.

The existing ``make check-schema-drift`` target picks up patch-apply
without changes — it already re-runs ``sync_schemas.py`` and diffs the
cache. With patches in the directory, the diff now validates that
patches still apply AND the resulting cache matches what's checked in.

The directory ships empty in this commit. The two #753-restoration
patches follow in a separate commit once PR #791 (3.0.12 regen) lands
on main — they need the post-regen cache as the diff base.

Refs #791, #753

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

* feat(schemas): restore #753 + #792 hand-edits via tracked patches

Three patches restore prior hand-edits to the 3.0 schema cache that
were silently dropped on the 3.0.7 → 3.0.12 regen (#791):

01-adagents-revoked-publisher-domains.patch
  ``adagents.json`` gains top-level ``revoked_publisher_domains[]`` with
  the Reason enum. SDK dict-layer helpers
  (``validate_revoked_publisher_domain_entry``,
  ``filter_revoked_selectors``) implement the contract today; this
  patch restores the field on the Pydantic-model layer so adopters get
  parity across both code paths. Filed in PR #753 — upstream took the
  shape into 3.1.0-beta.x rather than 3.0.x, so the patch stays alive
  until the SDK moves to a 3.1 floor.

02-publisher-property-selector-publisher-domains.patch
  ``publisher-property-selector.json`` gains optional
  ``publisher_domains[]`` (compact form) on the `all` and `by_tag`
  selectors, XOR with the singular ``publisher_domain``. SDK helpers
  (``_fanout_publisher_properties``, ``validate_publisher_properties_item``,
  ``get_properties_by_agent``) implement the contract; same Pydantic-
  layer parity restoration. Filed in PR #753 — same upstream-status as
  patch 01 (landed in 3.1.0-beta.x).

03-manifest-signal-owned-discovery-only.patch
  ``manifest.json`` removes ``signal_owned`` from
  ``activate_signal.specialisms`` and ``activate_signal`` from
  ``signal_owned.exercised_tools``. Owned signal agents are
  discovery-only by design; upstream's 3.0.12 manifest still includes
  the old two-specialism shape, which would require owned signal
  agents to implement ``activate_signal`` (defeating the specialism
  purpose) and make conformance runners exercise marketplace
  activation. Filed in PR #792 — SDK self-correction. Drops when
  upstream updates the manifest shape.

Each patch carries its own audit-trail header (Patch / Reason / Filed
/ Upstream status / Drop when) so the next reader has full context
without needing git archaeology. See ``schemas/patches/README.md`` for
the convention.

Pydantic regen (``make regenerate-schemas``) picks up all three
restored fields:

  src/adcp/types/_generated.py            — RevokedPublisherDomain, PublisherDomain (per-arm)
  src/adcp/types/generated_poc/adagents.py
  src/adcp/types/generated_poc/core/publisher_property_selector.py

SCHEMA_DELTAS.md now reports "no field-shape changes detected" because
the post-patch generated types match the prior committed state. PR #791
had recorded the deletions as part of its regen; this commit reverses
them via the patches infra.

Tests: ``pytest tests/`` — 5026 passed, 30 skipped, 1 xfailed. ruff +
mypy clean.

Refs #753, #791, #792

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.

1 participant