Skip to content

feat(cimd): standalone Client ID Metadata Document publisher - #1711

Open
omrishiv wants to merge 4 commits into
agentic-community:mainfrom
omrishiv:feat/cimd-standalone
Open

feat(cimd): standalone Client ID Metadata Document publisher#1711
omrishiv wants to merge 4 commits into
agentic-community:mainfrom
omrishiv:feat/cimd-standalone

Conversation

@omrishiv

Copy link
Copy Markdown
Contributor

Refs: #992 (publisher slice — see Not covered below; does not close it)
Parent epic: #988 (Phase 4 CIMD)

What

Publishes the registry's own Client ID Metadata Document (CIMD) so the registry can identify itself as an OAuth client (by URL) to external CIMD-aware IdPs, without pre-registration or DCR. Per the CIMD draft, the document's URL is the client_id.

GET /oauth/client-metadata.json      # public, unauthenticated, cacheable

Opt-in (CIMD_PUBLISHER_ENABLED, default off → 404). Returns client_id, client_name, client_uri, redirect_uris, grant_types, response_types, token_endpoint_auth_method: none, scope, and optional logo_uri/contacts, with Cache-Control: public, max-age=<CIMD_CACHE_TTL>.

Why this is a standalone branch

The original CIMD work was built on top of #1692 (the gateway token proxy), which was closed. The CIMD publisher has no functional dependency on the token proxy — it references no token-proxy symbols. This branch is the publisher rebased cleanly onto main, so #992's publisher can land independently of the closed token-proxy effort. (The token-proxy-coupled bits from the original branch — the TestCimdClientIdPassthrough tests and the python-multipart dep — were intentionally dropped; they belong with a token proxy, not the publisher.)

Design decisions

  • Path /oauth/client-metadata.json, not /.well-known/…. A CIMD is a client-published resource at an operator-chosen public path — RFC 8615 .well-known is for authority/server documents,. /oauth/client-metadata.json matches the MCP spec's own example and the AT-Protocol convention. [Phase 4] Publish CIMD (Client ID Metadata Document) for AI Registry Tools MCP client #992 explicitly left the path "TBD, track the draft," so this satisfies that note rather than contradicting it.
  • Root-mounted, rides the public catch-all. The route is served by a root-mounted cimd_router; the gateway nginx catch-all location / proxies to the registry with no auth_request, so the AS fetches it unauthenticated with zero nginx changes.
  • client_id/redirect_uris derive from the egress OAuth callback base (settings.egress_oauth_callback_base = EGRESS_OAUTH_CALLBACK_BASE_URL or REGISTRY_URL), not registry_url alone — so a deployment whose external callback host differs from registry_url still publishes a client_id and redirect that share the origin the IdP redirects back to. A single canonical build_cimd_client_id_url guarantees the published URL and any future outbound client_id can't drift.
  • Kept the safe defaults: opt-in gate, enforce_https on the client_id, and public, max-age caching.

Config surface (three-surface parity)

All 7 registry-consumed, non-secret params are wired first-class:

Env Setting Helm (registry.app.*) Terraform var Type / default
CIMD_PUBLISHER_ENABLED cimd_publisher_enabled cimdPublisherEnabled cimd_publisher_enabled bool / false
CIMD_CACHE_TTL cimd_cache_ttl cimdCacheTtl cimd_cache_ttl int / 3600
CIMD_CLIENT_NAME cimd_client_name cimdClientName cimd_client_name str / "AI Registry Tools"
CIMD_REDIRECT_URIS cimd_redirect_uris cimdRedirectUris cimd_redirect_uris str CSV / ""
CIMD_SCOPE cimd_scope cimdScope cimd_scope str / ""
CIMD_LOGO_URI cimd_logo_uri cimdLogoUri cimd_logo_uri str / ""
CIMD_CONTACTS cimd_contacts cimdContacts cimd_contacts str CSV / ""
  • System Config UI: new CIMD Publisher group in config_routes.py CONFIG_GROUPS (all non-sensitive).
  • Docs: row block in docs/unified-parameter-reference.md.
  • Docker: .env.example + all three compose files (typed ${VAR:-false}/${VAR:-3600} defaults so an empty value never breaks Pydantic).
  • Helm: charts/registry values + deployment (bool/int via toString | quote) + reserved-env-names.txt (chart-managed, not extraEnv-shadowable).
  • Terraform: root + module variables, module passthrough, ecs-services.tf env (bool/int via tostring()), terraform.tfvars.example.

Testing

  • tests/integration/test_cimd_publisher.py — document shape, required fields, field-order stability, cache header, client_id == served URL parity, callback-base precedence over registry_url, disabled→404, and a root-mount/no-prefix invariant that reads main.py so a future rename/prefix can't silently break parity. 6 passed.
  • Helm helm template charts/registry renders CIMD_PUBLISHER_ENABLED: "true", CIMD_CACHE_TTL: "1200" (bool/int coercion correct).
  • Terraform terraform fmt -check clean on all .tf files.
  • ruff check/format, pre-commit (syntax, fast tests) clean.

Not covered by this PR (so #992 is not closed by mistake)

  1. Outbound client_id wiring — nothing today sends the registry's CIMD URL as its client_id on an outbound /authorize (egress uses per-server operator credentials). There is no such flow to wire yet; build_cimd_client_id_url is the single source of truth the future outbound path must reuse. [Phase 4] Publish CIMD (Client ID Metadata Document) for AI Registry Tools MCP client #992 frames this as forward-looking ("once AI Registry Tools calls external MCP servers").
  2. Fake-AS end-to-end ([Phase 4] Publish CIMD (Client ID Metadata Document) for AI Registry Tools MCP client #992 acceptance) — a test of that outbound flow; can't be written until (1) exists.
  3. [Phase 4] CIMD consumer: accept CIMD URL as client_id on /authorize #993 (CIMD consumer) — accepting a CIMD URL as client_id on /authorize//oauth/token is not in this branch. The original approach ("Option Y": the token proxy forwards client_id verbatim to the upstream IdP) depended on [Phase 3] RFC 8707 resource enforcement + gateway token proxy (IdP-signed MCP tokens) #1692's token proxy, which was dropped here. [Phase 4] CIMD consumer: accept CIMD URL as client_id on /authorize #993 returns with the token-proxy / OAuth-discovery track.

Files

20 files, +518 / −7. Publisher core: registry/auth/oauth_metadata.py, registry/api/wellknown_routes.py, registry/main.py, registry/core/config.py. Surface wiring + docs + tests as above.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Decoupled from the token-proxy work (PR agentic-community#1692, closed). Contains only the
CIMD publisher: build_cimd_document/build_cimd_client_id_url, the
/.well-known/mcp-client-metadata route, cimd_* settings, docs and the
publisher integration test. References no token-proxy symbols.

Dropped from feat/cimd (both belong to agentic-community#1692's token proxy, not the
publisher): TestCimdClientIdPassthrough in test_token_proxy.py and the
python-multipart auth-server dependency.
…ack base

Repoint the CIMD publisher from /.well-known/mcp-client-metadata to the
idiomatic /oauth/client-metadata.json (a client-published resource at an
operator-chosen public path per draft-ietf-oauth-client-id-metadata-document
and MCP/AT-Protocol convention, not a .well-known authority document).

- Root-mounted cimd_router; rides the public catch-all nginx location / (no
  auth_request), so no nginx change and the AS fetches it unauthenticated.
- Derive client_id + default redirect from settings.egress_oauth_callback_base
  (egress_oauth_callback_base_url or registry_url), ported from oauth-2-1, so
  both share the externally reachable origin the IdP redirects to.
- Keep the opt-in gate, HTTPS enforcement, and public max-age caching. Tests
  cover the new path, client_id parity, callback-base precedence, and the
  root-mount/no-prefix invariant.
The 7 registry-consumed CIMD settings (cimd_publisher_enabled, cimd_cache_ttl,
cimd_client_name, cimd_redirect_uris, cimd_scope, cimd_logo_uri, cimd_contacts)
are now first-class on every surface, mirroring SSRF_ALLOWED_HOSTS /
GATEWAY_GENERIC_PROXY_ENABLED (registry-consumed, non-secret):

- config_routes.py: new 'CIMD Publisher' CONFIG_GROUPS group (System Config UI)
- docs/unified-parameter-reference.md: 7 rows
- .env.example + docker-compose{,.podman,.prebuilt}.yml: typed ${VAR:-default}
  so empty values never reach Pydantic (bool/int)
- charts/registry values.yaml + deployment.yaml (bool/int via toString|quote)
  + reserved-env-names.txt (chart-managed, not extraEnv-shadowable)
- terraform/aws-ecs: root + module variables, module passthrough, ecs-services
  env (bool/int via tostring()), tfvars.example

Verified: helm template renders true/1200; terraform fmt clean; CONFIG_GROUPS
order unique + all non-sensitive; publisher tests 6 passed.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The 7 CIMD publisher env vars (env[16]-env[22], after the SSRF pair) shifted
every positional env assertion after env[15] by 7. Update the index map + the
extraEnv/chart-var assertions to match, and add explicit CIMD_* index checks.
Renders verified against helm template. (Pre-existing, unrelated: the stack
chart's extra_env_forwarding_test 'yaml: line 99' error reproduces on main.)
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