Repository navigation
Conversation
A scoping doc for surfacing the requesting MCP client's product identity to Internet Identity, so the id.ai consent screen can show which vetted product is connecting (a name and a logo) instead of an anonymous bridge. The design: - The MCP server puts a curated product-name slug in the II connect-link fragment, and serves the matching name and logo from two GET endpoints on the same origin II already validates for the #4091 callback check. - Branding is shown for vetted products only; the name and logo are curated server-side and never taken from client-supplied DCR metadata (open DCR is unauthenticated, so rendering client strings would be a consent-phishing vector). - Nothing keys on the OAuth client_id: those are per-registration UUIDs that churn on reinstall, expiry, and LRU eviction. Product identity is derived from the vetted redirect vendor instead (the hosted-redirect allow-list), which is stable and self-authenticating. - v1 covers self-authenticating web connectors only. Native/desktop apps authenticate over loopback (no verifiable identity), so they stay anonymous until a signed software_statement path lands (deferred). Docs-only; requires a coordinated counterpart change on the id.ai side, which the doc specifies as the boundary contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
Adds a scoping/design document defining a proposed “client branding” extension for MCP→Internet Identity authorization, allowing id.ai to show a vetted connector’s product name and logo on the consent screen by using a curated server-side product slug and same-origin endpoints.
Changes:
- Add
docs/scoping-client-branding.mddescribing the proposed contract/flow, vetting model, endpoints, security considerations, and phased rollout plan.
Comments suppressed due to low confidence (4)
docs/scoping-client-branding.md:55
- The
POST /oauth/registercitation is off: the handler isregisteratsrc/auth.rs:1897(the current1888line is insidegranted_grant_types). Please update the reference to match the actual endpoint implementation.
Dynamic client registration is **unauthenticated and open** (`POST /oauth/register` takes all
callers, `auth.rs:1888`). So any client-supplied `client_name` / `logo_uri` is attacker-
controlled. Rendering it on a consent screen would be a phishing gift: a hostile client
docs/scoping-client-branding.md:70
- The
include_str!reference for the inlined DFINITY logo points toauth.rs:1161, but the logo include is currentlyCONNECT_LOGO_SVGatsrc/auth.rs:1170(1161 is in the preceding doc/comment block). Updating the citation will keep the “verified” claim accurate.
client `logo_uri`. Logos are compiled-in assets (like the DFINITY logo already inlined into the
callback page via `include_str!`, `auth.rs:1161`).
docs/scoping-client-branding.md:77
- The
client_idminting citation appears to be pointing at the invalid-redirect error branch (src/auth.rs:1913), not where theclient-{uuid_v4}value is created (src/auth.rs:1922). Updating the line reference will prevent confusion when someone goes to verify the claim.
- Minted fresh per registration: `client-{uuid_v4}` (`auth.rs:1913`), a new random value each
time anyone registers.
docs/scoping-client-branding.md:185
- This section says the
/brandingresponses are “cacheable”, but earlier the metadata endpoint is specified asno-store(and later you reiterateno-storefor the JSON). Consider clarifying that only the logo bytes are intended to be cacheable, while the JSON metadata should beno-store(or adjust the endpoint contract if you intend caching for both).
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
is a `404`. Responses are small, cacheable, and served from compiled-in assets.
- **Logo rendering on II (the one II-side must).** SVG is acceptable as the logo format, but only
if II renders it through a fixed-size `<img src=...>`. It must never inline server-returned SVG
into the consent DOM: inlined SVG can execute script, an `<img>`-loaded SVG cannot. (A raster
logo would sidestep the concern entirely, but `<img>`-rendered SVG is safe and keeps the crisp
vector mark.)
- **CORS / caching** mirror the `#4091` well-known: `Access-Control-Allow-Origin` for the JSON
metadata (II uses `fetch`), and `no-store` so an intermediary cannot serve a mapping stale after
a curation change.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot noted the "verified, file:line" citations had drifted from src/auth.rs. The doc's numbers were taken from the pre-merge #98 branch; after #98 (the MCP05 redirect fix) merged, everything below redirect_uri_permitted shifted by about nine lines. Re-synced every citation against current main and verified each against the source: authorize 865 -> 874 validate_client 890 (call) -> 759 (definition) #4091 well-known 1082-1109 -> 1091-1118 error copy 923 -> 932 ii_mcp_url 1062 -> 1071 connect_callback_url 1088 -> 1097 auth_callbacks 1097 -> 1106 logo include_str! 1161 -> 1170 register 1888 -> 1897 client-{uuid} 1913 -> 1922 Unchanged (above the shift): make_room_for_client (340), DEFAULT_ALLOWED_REDIRECTS (399), the path-pin comment (383-391), and the loopback-exempt note (393). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
docs/scoping-client-branding.md:20
- This section says “Two decisions frame everything below,” but it lists three bullets. Update the count to match to avoid confusion in the scoping doc.
Two decisions frame everything below:
docs/scoping-client-branding.md:177
- This bullet says responses are “cacheable,” but a few lines later the doc specifies
no-storefor the JSON metadata. Clarify which endpoint(s) are intended to be cacheable vs explicitly non-cacheable so implementers don’t pick conflicting headers.
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
is a `404`. Responses are small, cacheable, and served from compiled-in assets.
aterga
left a comment
There was a problem hiding this comment.
Review from the II-side design point of view
Answering §10, since it's addressed to the II team. Grounded in the current II frontend/backend (the id.ai/mcp connect flow, the declared-callback allow-list, and the production CSP).
The identity/trust model is right — no changes needed there
- Curated, server-side product identity, never client-supplied. Correct. II isn't a party to the client↔server OAuth, so anything the client can set (DCR
client_name,logo_uri) is unauthenticated and phishable. Deriving identity from the vetted redirect vendor is a sound proxy: II's declared-callback allow-list (fetch of/.well-known/ii-auth-callbacks, fail-closed, exact match) already means a connect can only reach us over an origin that published that manifest, so the vendor is effectively attested by the same mechanism. - Product slug, not
client_id. Correct. The connect link is attacker-craftable andclient-{uuid_v4}churns / is LRU-evicted, so aclient_idin the fragment would be a spoofable, unstable display key. A closed slug space resolved server-side is the right identifier. - Additive / backward-compatible contract. Confirmed on our end — the link parser ignores unknown fragment params today, so an added
connectoris safe for old II, and old servers simply omit it. The only II code cost is one field added to the link schema.
The one real gap: logo transport will hit II's CSP (§10 Q2)
The doc's model — II renders '<img src="{issuer}/branding/{slug}/logo">' pointing at the callback origin — is blocked by II's Content-Security-Policy. This is the main thing to revise.
II's production CSP is:
img-src 'self' data: https://*.googleusercontent.com;
connect-src 'self' https:;
So:
- A cross-origin
'<img src="https://<callback-origin>/…/logo">'won't load —img-srcallows only'self',data:, and one fixed Google-avatar host. We don't want to widenimg-srctohttps:, since that reopens exactly the cross-origin image surface we keep closed. fetch()to any HTTPS origin is allowed (connect-src 'self' https:).
Recommended contract instead: II fetch()es the branding from the trust-confirmed callback origin and renders the logo as a data: URI (already permitted by img-src data:, no CSP change). Simplest shape: serve one metadata document with the logo inlined as a bounded base64 data: image, so II makes a single request to a single origin and never needs a second cross-origin image hop. That also answers §10 Q4 — one doc with an inline logo beats separate name/logo requests for us.
Note this flips one assumption in the doc: "No CORS needed for <img>" doesn't apply, because we reach the logo via fetch() (which does need the CORS header you already specify) and then render data:.
SVG vs raster (§10 Q2 cont.)
The doc's point that an <img>-loaded SVG can't execute script is technically true. But once II is building a data: URI from fetched bytes, I'd steer away from SVG: it keeps a latent "never inline this" invariant that future contributors won't know, whereas raster (PNG) is strictly safer and removes the question entirely. Recommend the curated logos be PNG, with II applying a raster-only content-type allow-list, a byte-size cap, a decode step, and a fail-closed fallback to the anonymous icon on any validation failure — i.e. apply the same hardening the doc reserves for a hypothetical Phase-3 open path, in v1, because II is the side putting bytes on screen.
Trust ordering — please state it explicitly
connector={slug} lives in the fragment, which is attacker-craftable (a malicious link can claim &connector=claude). Safety comes entirely from where II fetches branding: only from the callback origin, and only after that origin is trust-confirmed — i.e. after the certified trusted_url gate confirms the server origin, and after the callback is matched against the origin's declared allow-list. The slug is display-only routing; the authority for "is this really ChatGPT" is that the branding came from an origin II independently trust-confirmed, never the fragment's claimed value. Worth spelling out so the II implementation doesn't fetch branding on the honest-untrusted / unverified path.
§10, point by point
- Fragment param + endpoint paths —
connector={slug}is fine (additive; parser ignores unknowns). Prefer a single metadata endpoint with an inline logo over a separate/logoroute. - Render logo via fixed-size
'<img src=…>'? Not cross-origin (CSP blocks it). Yes viafetch()+data:, or an inline base64data:logo in the JSON — and please make it raster, not SVG. - "Verified connector" treatment + name delivery — deliver the name in the JSON, not mirrored in the fragment (fragment is untrusted; JSON comes from the trust-confirmed origin). The exact verified-vs-anonymous consent UI is ours to specify and I'll take it to the II design owners; the security contract we need from the server is: branding is shown ⇔ trust-confirmed origin + declared callback + valid slug + valid logo; otherwise fall back to the anonymous screen.
- Single doc vs. separate requests — single metadata document with the logo inlined as base64
data:. One request, one origin, no CSP friction.
Minor
- Security section is solid: no client-supplied content, no SSRF in v1 (logos bundled at build time), closed slug space with 404 otherwise. If the Phase-3 open self-service path is ever built, the SSRF-pinned client + https-only + size/dimension caps + raster-only + decode-then-re-encode is exactly right — but the rendering hardening (size cap / raster / fail-closed) should be II-side and present from v1, not deferred.
- Keeping native/desktop (loopback-redirect) connectors anonymous until a signed
software_statementis the correct v1 cut — those are exempt from the callback allow-list, so there's no vetted-vendor anchor to derive identity from.
Bottom line: the trust model needs no changes. The one substantive revision is the logo transport — replace "II renders a cross-origin <img src>" with "II fetches from the trust-confirmed origin (or reads an inline base64 logo from the metadata JSON) and renders a data: raster image, failing closed to the anonymous screen." That's the only part that breaks on contact with II's CSP.
Generated by Claude Code
Address the review pass on the scoping doc: * Fix stale code citations to the current snapshot: get_info (tools.rs:1883), oql_needs_origin_error (1495), err (2287), the resource-not-found calls (2067/2078), and the skill:// resource generation loop (2033-2045). * Replace the reference to a non-existent `scoping-client-branding.md` with the client-branding scoping proposal (PR #103). * Correct the CIMD SSRF note: `markdown_url_for_base` is NOT a reusable SSRF guard (host-string compare only, ignores ports, no private-IP rejection; its 169.254 rejection is incidental to the fixed skills origin). A dedicated SSRF-safe fetcher is needed; the closest building block is discover.rs's ip_is_global. * Drop `oql-schema://` from the public-cacheable resources: it is caller-gated (requires derivation_origin/account, runs under the user's delegated agent), so a public cache keyed only by canister ID could leak one identity's schema to another. Keep it a tool or an identity-keyed private resource. * Fix the x-mcp-header section: there is no top-level `network` field or destructive-op discriminator to mirror into Mcp-Param-*; annotating absent fields would fail the mandated header/body validation. Use Mcp-Name for per-operation policy; scope any new param separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the imcp2 side of the client-branding extension (docs/scoping-client-branding.md, PR #103): let II show WHICH vetted product is asking for a connect, instead of an anonymous "some bridge". - New `branding` module: a server-curated table of vetted connectors (ChatGPT, Claude, Cursor, Grok, Perplexity, Google Antigravity), keyed on the request's already-validated `redirect_uri` vendor — never on the client-supplied `client_name`/`logo_uri`, which open DCR makes attacker-controlled (rendering them would be a consent-phishing gift). A connect that doesn't resolve to a vetted web vendor — including every loopback (native-app) redirect — keeps the status-quo anonymous screen. - `authorize` resolves the connector for the validated redirect and passes its slug on the II connect link as `&connector=<slug>` (additive fragment param; an II that doesn't know it ignores it). - Two issuer-rooted GET endpoints II fetches same-origin with the #4091-validated callback: `/branding/{slug}` (name + absolute logo URL + `verified`, `no-store`) and `/branding/{slug}/logo` (SVG, `nosniff`). Both 404 outside the curated set so the path can't probe. The bundled logos are ORIGINAL neutral placeholders (a generic "verified connector" mark), deliberately not reproductions of vendor logos — each vendor's own licensed mark is dropped into its `Connector::logo` later. Actually rendering this needs II's counterpart change (parse the slug, fetch these endpoints, render); until then it is inert and harmless. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first draft put the product slug in the connect-link fragment
(`&connector=<slug>`) and served an unbound `GET {issuer}/branding/{slug}`.
The fragment is written by whoever drives the browser, so that let a
consent screen vouch for a product the server never tied to the connect.
The contract now matches what #200 implements: II asks
`GET {issuer}/branding?state=` and the server answers from the connect's
validated redirect, with nothing about branding in the link. The revision
also:
- states precisely what branding vouches for (where the authorization code
is delivered, not who started the connect or which vendor account ends up
holding the grant) and the residual risks that follow;
- spells out the II side as numbered requirements: issuer derivation from
the validated callback, response acceptance, one callback and one state
for everything, an exact-origin trust list gating the whole treatment,
and safe rendering (CSP, <img>, wording);
- notes that branding covers only compiled-in callbacks, never operator
allow-list entries, and documents the logo hardening;
- replaces line-number citations with symbol references.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CIMD shipped in #191 (opt-in) and #203 (on by default, with OAUTH_CIMD_ENABLED as a kill switch), so the scoping plan now records what was built instead of proposing it: - the implemented gate, flow, fetcher, validation, caching, single-flight and in-flight bounds, configuration and rollback, by symbol rather than by line number; - answers to the plan's open questions, and where the build departed from the plan: a domain-and-subdomain gate instead of exact origins, no cache floor or ETag revalidation, the same-origin redirect rule, discovery's SSRF guard tightened rather than left unchanged, and the rate cap dropped on purpose; - branding moved to #103/#200 and keyed on the validated redirect. A client_id-domain key would have let any local program pose as a loopback CIMD client of a vetted vendor; - the DoS residuals as built, the tests that pin the behaviour, and an index of where it lives. Section numbers 3.4 and 3.5, which the code cites, keep their meaning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and #203) (#143) * docs: scope CIMD (Client ID Metadata Documents) to replace open DCR Expands the top-ranked alignment improvement into an implementable plan. Recommends a trust-policy-gated, additive design: fetch a Client ID Metadata Document only when the client_id URL's host is already on the curated vendor allow-list, keep open DCR for everything else, and never trust the document's display fields. This collapses the new outbound-fetch surface to a finite set of vetted hosts instead of an arbitrary-URL SSRF primitive on the unauthenticated /authorize path, while still delivering spec alignment and a DNS/TLS-authenticated domain key for branding. Folds in the alignment PR's review correction: skills.rs's markdown_url_for_base is NOT a usable SSRF guard (host-string compare only); the real building block is discover.rs's address-pinned fetcher (resolve_public_url + site_client + ssrf_redirect_policy), which the plan extracts into a shared module as its Phase 0. Covers the fetch/validate/ cache flow, allow-list re-keying, branding subsumption, a security analysis, phasing, and open questions — all cited against current code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * CIMD doc: origin-exact gating, strict reader, string match, mandatory rate cap Address the review on the CIMD scoping doc: * Trust policy matches exact HTTPS ORIGINS, not bare hosts. resolve_public_url uses the caller-supplied port (discover.rs:1113), so a host-only gate would let https://claude.ai:8443/... reach an unvetted port; require the default 443 (and reject userinfo/other non-canonical authority) before any DNS/fetch. * CIMD must use a STRICT capped reader, not discovery's best-effort one (discover.rs:1207-1228 returns lossy/partial text without signaling — a truncated body whose prefix is valid JSON would be accepted as metadata at the auth boundary). Fail closed on over-limit/stream-error/invalid-UTF-8. * client_id validation is a plain STRING match against the requested URL — no normalization (hosted redirects use exact string membership, auth.rs:648, and normalizing would mint aliases that disagree with the raw-URL cache key). * Make the outbound-DoS control mandatory: the cache does NOT bound misses, because an attacker can vary the URL PATH on a vetted host to mint unlimited distinct keys and concurrent 15s fetches. A per-host + global concurrency/rate cap (plus negative-caching) is now a Phase 1 acceptance criterion, not an optional nicety. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * CIMD doc: disable cross-origin redirects (discovery policy leaks the gate) Review fix: the doc said to keep discovery's redirect policy, but ssrf_redirect_policy / redirect_hop_ok (discover.rs:1145-1153) follow any global-IP literal and any same-host hop WITHOUT a port check — so a vetted client_id could redirect the fetch to an unvetted public IP or to vetted-host:8443, escaping the exact-origin gate of §3.1. A CIMD is served directly at its URL, so §3.3 now says to disable redirects entirely (or, if truly needed, require an exact same-origin hop), never the discovery policy. §5's SSRF bullet notes the control. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * CIMD doc: never negative-cache per-request redirect failures (poisoning) Review fix: §3.4 folded request-specific redirect checks into "validation," so negative-caching every validation failure by URL would let an attacker poison a valid client — request a real CIMD URL with a non-member redirect_uri, the URL gets cached as invalid, and legitimate redirects then hit the negative entry. Split validation into document-intrinsic (cacheable: fetch/JSON/client_id==URL) and per-request (never cached: redirect membership + redirect_uri_permitted, re-run every request against the positively-cached document). §3.5 and the §5 DoS bullet now negative-cache only document-intrinsic failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: turn the CIMD scoping into a design record of what shipped CIMD shipped in #191 (opt-in) and #203 (on by default, with OAUTH_CIMD_ENABLED as a kill switch), so the scoping plan now records what was built instead of proposing it: - the implemented gate, flow, fetcher, validation, caching, single-flight and in-flight bounds, configuration and rollback, by symbol rather than by line number; - answers to the plan's open questions, and where the build departed from the plan: a domain-and-subdomain gate instead of exact origins, no cache floor or ETag revalidation, the same-origin redirect rule, discovery's SSRF guard tightened rather than left unchanged, and the rate cap dropped on purpose; - branding moved to #103/#200 and keyed on the validated redirect. A client_id-domain key would have let any local program pose as a loopback CIMD client of a vetted vendor; - the DoS residuals as built, the tests that pin the behaviour, and an index of where it lives. Section numbers 3.4 and 3.5, which the code cites, keep their meaning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CIMD record: untrusted URLs are refused, eviction is by expiry, withdrawals wait for expiry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CIMD record: log sampling protects the default (info) level, not debug Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…authorization session (#200) * Surface vetted-connector branding to Internet Identity (name + logo) Implements the imcp2 side of the client-branding extension (docs/scoping-client-branding.md, PR #103): let II show WHICH vetted product is asking for a connect, instead of an anonymous "some bridge". - New `branding` module: a server-curated table of vetted connectors (ChatGPT, Claude, Cursor, Grok, Perplexity, Google Antigravity), keyed on the request's already-validated `redirect_uri` vendor — never on the client-supplied `client_name`/`logo_uri`, which open DCR makes attacker-controlled (rendering them would be a consent-phishing gift). A connect that doesn't resolve to a vetted web vendor — including every loopback (native-app) redirect — keeps the status-quo anonymous screen. - `authorize` resolves the connector for the validated redirect and passes its slug on the II connect link as `&connector=<slug>` (additive fragment param; an II that doesn't know it ignores it). - Two issuer-rooted GET endpoints II fetches same-origin with the #4091-validated callback: `/branding/{slug}` (name + absolute logo URL + `verified`, `no-store`) and `/branding/{slug}/logo` (SVG, `nosniff`). Both 404 outside the curated set so the path can't probe. The bundled logos are ORIGINAL neutral placeholders (a generic "verified connector" mark), deliberately not reproductions of vendor logos — each vendor's own licensed mark is dropped into its `Connector::logo` later. Actually rendering this needs II's counterpart change (parse the slug, fetch these endpoints, render); until then it is inert and harmless. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Bind connector branding to the authorization session The first cut let the connect-link fragment carry `&connector=<slug>` and served `GET {issuer}/branding/{slug}` unconditionally. The fragment is craftable by any native client that drives the navigation, so a loopback session could have presented itself to Internet Identity as "verified Claude". Branding is now answered per session: II asks `GET {issuer}/branding?state=` with the `state` it already holds, and the server derives the connector from that pending, unexpired connect's validated `redirect_uri`. Nothing in the fragment asserts branding (the connector param is gone from `ii_mcp_url`), and a loopback or unlisted session gets the same 404 as an unknown one. The lookup is read-only, so it cannot disturb the handshake. Branding and redirect validation now share one host rule (`host_key` + `host_is_or_under`), so a trailing-dot `claude.ai.` redirect, which validation accepts, brands as Claude instead of reading as anonymous; the allow-list parser and the CIMD trust match use the same helpers. A redirect validation would refuse never resolves to a connector, and a test holds the curated domains equal to the compiled-in allow-list vendors. The DCR router test now drives the 200 path over HTTP (authorize -> the `state` in II's link -> `/branding?state=`), including `no-store` and both claude.ai spellings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Branding docs: spell out the validation invariant Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the branding routes on mcp_router Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Brand only compiled-in callbacks; harden the branding responses connector_for_redirect accepted anything the effective allow-list admits, so an OAUTH_ALLOWED_REDIRECT_PREFIXES entry on a vendor's domain (another path on claude.ai, say) was shown as that vendor, contrary to the docs. Branding now requires a redirect admitted by a compiled-in DEFAULT_ALLOWED_REDIRECTS entry, under validation's own rules (redirect_uri_on_default_allow_list, sharing hosted_redirect_admitted with redirect_uri_permitted), so a vendor's name only ever rests on the reviewed callback paths in this repo. The branding 404 is now no-store like the 200 it stands in for, and the logo carries a sandboxing CSP: opened top-level it is an SVG document on the issuer origin, where <img> isolation does not apply. A test keeps every bundled logo free of script, event handlers, embedded HTML, and external references, for when licensed vendor marks replace the placeholder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tighten the bundled-logo guard to internal references only The logo test banned a handful of literal substrings, so a spaced `href = "https://…"` or a quoted `url('https://…')` got through. It now strips whitespace first and requires every href, src, and url() target to be an internal `#…` reference, and also rejects XHTML, animation, and DTDs. It checks itself against a set of unsafe SVGs, spaced and quoted spellings included, so the guard can't quietly weaken when licensed vendor marks replace the placeholder. The module doc now says what branding vouches for: where the authorization code is delivered, not who started the connect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the over-cap derivation-origin case on multidex.ai, not oisy.com oisy.com now serves a real /.well-known/ii-derivation-origin ("https://oisy.com"), so it resolves as Declared: the app's own declaration wins over the built-in registry, as intended. That broke resolve_app_identity_skips_probe_for_known_apps, which assumed oisy.com still served its app shell there, and with it CI for every PR. multidex.ai is a known app that still answers that path with its app shell (far over MAX_DERIVATION_ORIGIN_BYTES), so the test keeps pinning both things it was for: a known app is answered by the registry without an IC probe, and an over-cap body reads as "declares nothing" rather than a failed check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tighten branding tests and docs after the merge with main - branding_is_bound_to_the_session now checks both sides of the expiry boundary on the paused clock: branding answers one second before CONNECT_TTL and stops at it. The read-only check compares `created` after time has moved, so a lookup that re-stamped the connect would fail. - Restore the hosted_redirect_allow_list doc comment, which the new operator-entries test had been inserted beneath and taken over. - Cover ChatGPT's Exact-pinned callback (the redirect it actually registers) in resolution and vetting, and a descendant that the pin refuses. - Pin that a malformed /branding query (a repeated `state`) gets the same no-store 404 as the other misses, not axum's 400. - The CONNECTORS doc now says why loopback stays anonymous even with a CIMD client_id on a vetted origin; correct a stale description of what the DCR round-trip test registers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
A scoping doc,
docs/scoping-client-branding.md, for surfacing the MCP client's product identity to Internet Identity. Theid.aiconsent screen can then show which vetted product will receive the access a connect grants: a name and a logo. This PR is docs-only. The doc is the boundary contract for both sides; the server side is implemented in #200.Revised (2026-09-24). The first draft had the server put the product slug in the connect-link fragment (
&connector=<slug>) and serve an unboundGET {issuer}/branding/{slug}. Whoever drives the browser writes the fragment, so that let a consent screen vouch for a product the server never tied to the connect. The contract now binds branding to the connect itself:Key points
redirect_uri, recorded atauthorize. It never trusts anything the link or the client asserts.404, as do unlisted hosts, operator-added allow-list entries, and unknown or expired states. All these404s are identical.{issuer}from the #4091-validated callback with a strict parsed check;200and treat everything else as "no branding";callbackand onestatefor the #4091 check, the lookup, and the return navigation;<img>-only logo from the callback's origin, CSP, wording about where the access goes;DEFAULT_ALLOWED_REDIRECTS) are branded. Nothing keys onclient_id(DCR ids churn; CIMD ids are the client's own claim). v1 covers redirect-identified web connectors; native apps stay anonymous until a signedsoftware_statementpath exists.Related
registration_key): the connect-contract extensions this builds on.Testing
Docs-only; there is no build or test impact. The server-side behavior it describes is covered by #200's tests, listed in section 9 of the doc.
🤖 Generated with Claude Code