Skip to content

feat(registry): community-mirror lifecycle + upsert client (JS #2183/#2187 parity)#929

Merged
bokelley merged 3 commits into
mainfrom
claude/issue-925-registry-community-mirror
Jun 6, 2026
Merged

feat(registry): community-mirror lifecycle + upsert client (JS #2183/#2187 parity)#929
bokelley merged 3 commits into
mainfrom
claude/issue-925-registry-community-mirror

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Jun 6, 2026

Closes #925

Adds the persisted community-mirror adagents lifecycle to RegistryClient, matching the JS SDK shapes from adcp-client#2183 / #2187.

Methods added

  • publish_community_mirror_adagents(platform, config, *, auth_token)PUT /api/registry/mirrors/{platform}
  • get_community_mirror_adagents(platform)GET /api/registry/mirrors/{platform}; returns None on HTTP 404; hydrates superseded_by from the wrapper when the inner catalog omits it; rejects mismatched-platform / non-catalog bodies
  • list_community_mirror_adagents(*, limit=None, offset=None)GET /api/registry/mirrors
  • upsert_community_mirror_adagents(config, *, platform=None, auth_token) → resolves the platform key from the platform arg, then config["platform"], then a single consistent properties[].platform value (ambiguous → error), then publishes

Plus a module-level build_community_mirror_adagents(config) builder that emits authorized_agents: [], strips the platform routing key, and rejects authorization claims / generator-only flags. Platform keys are trimmed/lowercased and validated against ^[a-z0-9_-]{1,64}$.

Deviation from JS

The JS PRs add hand-written TS interfaces (PublishCommunityMirrorAdagentsResponse, CommunityMirrorAdagentsCatalog, etc.) — these are registry-API DTOs, not AdCP protocol schemas, so there is no generated Pydantic model in adcp.types to validate against. Following the existing create_adagents convention, these methods take/return dict[str, Any]. Tests therefore assert the wire shape directly (HTTP method, URL path, request body) rather than .model_validate().

Tests

tests/test_registry.py — wire-shape assertions: PUT/GET method + /api/registry/mirrors/{platform} path, request body (authorized_agents: [], no platform key, superseded_by passthrough), None on 404, superseded_by hydration, platform validation/inference/ambiguity, and auth header.

Verification: pytest tests/test_registry.py (138 passed), make lint, make typecheck all pass.

🤖 Generated with Claude Code

@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented Jun 6, 2026

Heads-up for reviewers: the community-mirror lifecycle types here are dict[str, Any] as a deliberate stopgap. The registry OpenAPI spec does not define these endpoints, so there is no generated adcp.types.registry model to use yet (the adjacent create_adagents is dict for the same reason). Filed upstream adcontextprotocol/adcp#5384 to add the endpoints + schemas to static/openapi/registry.yaml. Once that lands, run make regenerate-registry and swap the dicts for the generated types. Tracking: #925.

@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented Jun 6, 2026

Noted — dict[str, Any] is intentional here, consistent with the existing create_adagents convention, pending the upstream schema work in adcontextprotocol/adcp#5384. No action needed on this PR; the migration path (make regenerate-registry → swap dicts for generated types) is clearly documented in the comment above.


Generated by Claude Code

@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented Jun 6, 2026

Stopgap resolved. adcontextprotocol/adcp#5385 (tracking the #5384 ask) is merged, adding the community-mirror lifecycle endpoints + schemas to static/openapi/registry.yaml. As of 3baca82 the dict[str, Any] community-mirror client is retyped to the generated Pydantic models:

  • publish_community_mirror_adagents / upsert_community_mirror_adagents -> CommunityMirrorPublishResponse
  • get_community_mirror_adagents -> CommunityMirrorGetResponse | None (full wrapper; the catalog-only authorized_agents: [] invariant is now enforced by the model, and superseded_by is read straight off the wrapper, so the prior inner-catalog hydration is dropped)
  • list_community_mirror_adagents -> CommunityMirrorListResponse
  • new delete_community_mirror_adagents(platform, *, force=False, auth_token) -> CommunityMirrorDeleteResponse, with the 409 not-superseded case mapped to RegistryError
  • build_community_mirror_adagents no longer emits authorized_agents (the publish body is catalog-only; the service forces [])

Spec provenance: vendored from adcp main's static/openapi/registry.yaml (the authoritative source the website serves), since the public agenticadvertising.org/openapi/registry.yaml deploy is still pending. scripts/generate_registry_types.py gained rename-map entries to keep the new generated names stable/semantic.

create_adagents stays dict[str, Any] for now — typing it to CreateAdagentsResponse would have required updating out-of-scope tests in tests/test_registry_new.py, so it's deferred rather than landed half-typed.

Full local gate green: pytest tests/test_registry*.py tests/test_public_api.py tests/test_import_layering.py (312 passed), make lint, make typecheck-all, make validate-generated. Public-API snapshot unchanged.


Generated by Claude Code

@bokelley bokelley marked this pull request as ready for review June 6, 2026 21:50
@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented Jun 6, 2026

Acknowledged — 3baca82 lands the typed models and delete_community_mirror_adagents, and create_adagents deferral is noted. No further action from triage; the PR is ready for review.


Generated by Claude Code

aao-ipr-bot[bot]
aao-ipr-bot Bot previously approved these changes Jun 6, 2026
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Clean additive vendoring of the registry community-mirror lifecycle. Right shape: the public surface only grows — new RegistryClient methods, a new build_community_mirror_adagents builder, and new generated DTOs — so feat(registry): is the correct semver signal and there's nothing breaking on the wire.

Things I checked

  • Additive enums, no removed arms. DiscoveryMethod gains adagents_authoritative + community_catalog, Status1 gains community, Source5 gains community — all on deserialization-side response models (PublisherLookupResult, Property1, AdagentsJson). Adding members only widens what an adopter can parse; nothing removed, no discriminator-key change. ad-tech-protocol-expert: sound.
  • get() catalog-only invariant moved to the type layer. The dropped manual authorized_agents-empty check is correctly replaced by CommunityMirrorAdagentsJson.authorized_agents carrying max_length=0; a non-empty list raises ValidationError_parse wraps to ...invalid response..., and test_rejects_non_catalog_mirror / test_rejects_malformed_success_response switched their match accordingly. max_length=0 is a load-bearing protocol invariant of the mirror contract (authorized_agents: [] forced server-side), not incidental strictness — right call, and correctly scoped to the mirror DTO only (AdagentsJson / CommunityMirrorCatalogDocument keep an unbounded list).
  • delete() 409 path. _request_ok with default expected_status=200 raises RegistryError(status_code=409) before _parse runs; test_maps_409_not_superseded_to_registry_error asserts exactly that. force{"force": "true"} threads through _request's client.request(...) branch.
  • Platform inference + consistency assert. _community_mirror_platform_from_config (config.platform → single consistent properties[].platform → ambiguity error) and the pre-flight property-vs-platform assert both fire before the HTTP call; test_rejects_property_platform_mismatch / test_rejects_ambiguous_property_platforms assert request.assert_not_called().
  • No public export removed. CompanySearchResult (types/registry.py:1211) and FindCompanyResult (:1479) both survive — the schema $ref swap is path-inline only. supported_versions array→string is an unnamed path-response property, not a generated public model field. No deserialization break.
  • Rename-map pins. The five CommunityMirrorPublishRequest{1..5} anyOf variants and AdagentsJson1 get stable rename entries — the correct defense against the codegen-renumbering churn CLAUDE.md warns about.
  • Test plan honest. Wire-shape assertions against mocks are the right level for an HTTP client (per CLAUDE.md); no unchecked manual-verification box claiming an unvalidated path. tests/test_registry.py:test_exposes_wrapper_superseded_by correctly pins the dropped-hydration behavior (result.superseded_by set, result.adagents_json.superseded_by is None).

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

  • apply_renames corrupts generated prose. scripts/generate_registry_types.py rewrites class names with a global re.sub(r"\b{old}\b", new, content) across the whole file — including Field(description=...) / examples=[...] literals. This PR adds generic English words (Brand, Format, Collection, Data, Success, Severity) to that map, and the fallout is already in the diff: Policy example now reads "CreateAdagentsData subjects must provide... consent" and FormatSummary reads "ResolvedPropertyEntry IDs this format applies to". Strings only — no broken identifiers, no validation/wire impact, tests + typecheck pass — but the current mitigation is one-off str.replace patches in fix_spec_reality_gaps (it already carries "Founding AgentMember" → "Founding Member"). Adding more generic-word renames to a substitution that rewrites prose is going to keep generating Mad Libs in the docstrings; worth teaching apply_renames to skip string literals instead of patching them one at a time.
  • CommunityMirrorCatalogDocument is an unreferenced public export. Renamed from the codegen's AdagentsJson1 and added to __all__, but nothing references it — CommunityMirrorGetResponse.adagents_json uses CommunityMirrorAdagentsJson. Either confirm it's an intentional surface or drop it from the rename map so it stays an internal numbered class. (code-reviewer.)

Minor nits (non-blocking)

  1. Inconsistent normalization in platform inference. _community_mirror_platform_from_config returns the raw config["platform"] (e.g. "Meta") in one branch but the normalized value in the single-property branch. Harmless — publish_community_mirror_adagents re-normalizes downstream — but normalizing both branches (or neither) reads cleaner.

LGTM. Follow-ups noted below.

bokelley and others added 3 commits June 6, 2026 19:43
…#2187 parity)

Add persisted community-mirror adagents lifecycle methods to RegistryClient,
matching the JS SDK route/body shapes:

- publish_community_mirror_adagents -> PUT /api/registry/mirrors/{platform}
- get_community_mirror_adagents -> GET /api/registry/mirrors/{platform}
  (returns None on HTTP 404), hydrating superseded_by from the wrapper
- list_community_mirror_adagents -> GET /api/registry/mirrors (limit/offset)
- upsert_community_mirror_adagents -> helper with platform inference from the
  config or a single consistent properties[].platform value

A build_community_mirror_adagents helper emits authorized_agents: [], strips
the platform routing key, and rejects authorization claims and generator-only
flags. Platform keys are trimmed/lowercased and validated against
^[a-z0-9_-]{1,64}$.

Closes #925

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

adcp#5385 added the community-mirror lifecycle and schemas to the registry
OpenAPI spec. Vendor the updated spec from adcp main, regenerate the registry
types, and retype the community-mirror client methods from dict[str, Any] to
the generated Pydantic models.

- publish/upsert -> CommunityMirrorPublishResponse
- get -> CommunityMirrorGetResponse | None (full wrapper; superseded_by is on
  both the wrapper and adagents_json, so the prior hydration is dropped and the
  catalog-only invariant is enforced by the model)
- list -> CommunityMirrorListResponse
- add delete_community_mirror_adagents -> CommunityMirrorDeleteResponse, mapping
  the 409 not-superseded case to RegistryError
- build helper stops emitting authorized_agents (publish body is catalog-only;
  the service forces authorized_agents: [])

Add stable rename-map entries for the new generated collisions
(AdagentsJson1, the five CommunityMirrorPublishRequest anyOf variants) and for
generic inline names surfaced by the spec sync (Brand/Format/Collection/Data/
Success/Severity/DiscoveryMethod1/AuthorizationType/SelectionType/
PublisherProperty).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the claude/issue-925-registry-community-mirror branch from 3baca82 to 9416ea3 Compare June 6, 2026 23:44
@bokelley bokelley enabled auto-merge (squash) June 6, 2026 23:46
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Approving. Additive feat(registry): minor that mirrors the JS lifecycle (adcp-client #2183/#2187) cleanly — the catalog-only invariant is enforced at the type layer, and the new methods reuse the existing _request/_request_ok/_parse plumbing rather than reinventing error handling.

code-reviewer: sound-with-caveats. ad-tech-protocol-expert: sound-with-caveats. No MUST FIX: no runtime error on the happy path, no credential path, no breaking public-API change, catalog fail-closed preserved.

Things I checked

  • Catalog-only invariant is load-bearing and fail-closed. CommunityMirrorAdagentsJson.authorized_agents is required with max_length=0 (src/adcp/types/registry.py), so a registry that ever returns a non-empty authorized_agents fails model_validate and get_community_mirror_adagents raises via _parse. A community mirror must never assert sales authorization; this is the right shape. tests/test_registry.py::test_rejects_non_catalog_mirror covers it.
  • 404/409 handling. get_community_mirror_adagents uses _request(..., allow_404=True)None (registry.py:1413); delete_community_mirror_adagents lets 409 surface as RegistryError(status_code=409) through the default expected_status=200, with force=True as the documented escape hatch. Both tested.
  • Platform mismatch guard. get_* rejects a registry response whose platform disagrees with the normalized request key (registry.py:1414-1416). Fail-closed beats trusting the echo.
  • Enum additions are additive, not discriminator changes. DiscoveryMethod (+adagents_authoritative, community_catalog), Status1 (+community), Source5 (+community) are plain string enums on ordinary fields — no discriminated-union arm removed, no forward-compat hazard.
  • Test coverage is real, not nominal. 28 new cases across build/publish/get/list/upsert/delete, including ambiguity inference, single-property inference, the 409 mapping, and the catalog-only rejection. No unchecked manual-verification boxes in the test plan; this is a wire-shape unit suite and doesn't need a live round-trip.

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

  • The codegen rename is a whole-file regex and it's corrupting generated prose. scripts/generate_registry_types.py:114-118 runs re.sub(rf"\b{old}\b", new, content) across the entire file, so renames hit Field/example strings, not just type references. The new "Data": "CreateAdagentsData" entry rewrites the GDPR boilerplate in Policy to "CreateAdagentsData subjects must provide freely given... consent" — a data-subject right reassigned to a response model — and the pre-existing "Property": "ResolvedPropertyEntry" mangles the new FormatSummary field descriptions ("Property IDs" → "ResolvedPropertyEntry IDs"). Cosmetic today (description metadata only, CI is green, no wire/runtime impact) — but "Data" is a dangerously generic token, and the day an enum value or field name is the bare word Data this silently corrupts the contract. Scope the reference substitution to type contexts (after : [ -> class ( | , =) or skip string-literal lines. The class-definition sub at L107-112 is already anchored correctly; only the second sub is the offender.
  • build_community_mirror_adagents hard-requires formats, but the wire contract accepts five variants. CommunityMirrorPublishRequest is an anyOf over formats / properties / placements / collections / signals (any one non-empty). The builder rejects everything but formats (registry.py), and since publish_*/upsert_* both route through it, a spec-valid properties-only or signals-only mirror cannot be published through the SDK. ad-tech-protocol-expert confirms the JS builder is identically narrow, so this is deliberate cross-SDK parity rather than a divergence — but _community_mirror_platform_from_config infers the key from properties[].platform, advertising a path that then can't complete without formats. Either relax the builder to "at least one non-empty facet" or document the limitation on the builder docstring.
  • Two unrelated upstream spec changes rode along in the regen. supported_versions flips array→string and CompanySearchResultFindCompanyResult in schemas/registry-openapi.yaml. ad-tech-protocol-expert confirms both net to zero diff on the public Python surface (inline path response / rename-map no-op), so not a breaking SDK change — but call them out in the PR body so the next regen reviewer doesn't read them as noise, and confirm the deployed registry actually serves supported_versions as a scalar.

Minor nits (non-blocking)

  1. Stale PR description. The body says get_community_mirror_adagents "hydrates superseded_by from the wrapper" and that the builder "emits authorized_agents: []" — both describe the first commit's behavior. The final code does neither (the docstrings correctly say no hydration is needed and the body is catalog-only). Code and tests are consistent; just the prose is behind.
  2. Asymmetric platform normalization. _community_mirror_platform_from_config returns the raw config["platform"] un-normalized while the properties[] branch normalizes before dedup (registry.py). A config with platform: "Meta " and properties[].platform: "meta" wouldn't be detected as consistent. Downstream _normalize_* cleans the published key, so harmless — normalize the config branch too for symmetry.

LGTM. Follow-ups noted below.

@bokelley bokelley merged commit 11e5cda into main Jun 6, 2026
26 checks passed
@bokelley bokelley deleted the claude/issue-925-registry-community-mirror branch June 6, 2026 23:52
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.

feat(registry): community-mirror lifecycle + upsert client (JS #2183/#2187 parity)

1 participant