Description
Sub-issue 4b of #5145. With sub-issue 4a (PR #5198) the new pkg/auth/dcr package is in place and EmbeddedAuthServer consumes it. The CLI OAuth flow at pkg/auth/discovery/discovery.go::PerformOAuthFlow still calls oauthproto.RegisterClientDynamically directly, so it does not yet inherit the review-property behaviours added during #5042 (S256 PKCE gating, RFC 7591 §3.2.1 expiry-driven refetch, bearer-token transport with redirect refusal, panic recovery, singleflight deduplication). This sub-issue migrates that call site to the shared resolver and locks in the invariant with a CI grep guard.
After 4b lands, all four acceptance criteria of the parent issue are met and #5145 closes.
Context
pkg/auth/dcr.ResolveCredentials today takes embedded-authserver-shaped types — *authserver.OAuth2UpstreamRunConfig and *upstream.OAuth2Config — because in 4a we deliberately kept the move-only scope rather than designing a profile-neutral input with only one consumer in hand. 4b is when the second consumer arrives and the right shape becomes obvious.
The CLI side speaks OAuthFlowConfig (pkg/auth/discovery/discovery.go:501) and persists DCR results into OAuthFlowResult.ClientID / ClientSecret (pkg/auth/discovery/discovery.go:517). The downstream remote handler reads those back via pkg/auth/remote/handler.go's CachedClientID / CachedClientSecretRef / CachedRegTokenRef fields. That is the CLI's existing persistence model and the migration must preserve it.
Dependencies: sub-issue 4a (PR #5198) — provides the pkg/auth/dcr package and the EmbeddedAuthServer consumer.
Blocks: closure of #5145.
Acceptance Criteria
Profile-neutral resolver input — pkg/auth/dcr
CLI flow migration — pkg/auth/discovery
Credential persistence — pkg/auth/remote/handler.go
Inherited behaviours — verified by tests
Invariant guard
Cross-cutting
Technical Approach
Recommended implementation
-
Design the neutral input. Walk both call sites (embedded authserver and CLI) and identify the minimal field set the resolver actually reads: registration endpoint (or discovery URL), requested scopes, redirect URI, optional initial access token, optional explicit endpoint overrides, and an HTTP client. Promote that into a dcr.Request (or similar) struct. Convert the existing OAuth2UpstreamRunConfig / upstream.OAuth2Config consumers to construct a dcr.Request at the call site; the resolver no longer imports either domain type.
-
Write the CLI adapter. pkg/auth/discovery/discovery.go constructs a dcr.Request from OAuthFlowConfig (issuer, scopes, callback port → loopback redirect URI per RFC 8252 §7.3) and either:
- (a) passes a
dcr.CredentialStore backed by OAuthFlowResult / pkg/auth/remote/handler.go's Cached* fields. This is the cleaner long-term shape because cache hits stay inside the resolver. It does require designing a small Get / Put adapter over the secretProvider interface used by the remote handler.
- (b) passes
dcr.NewInMemoryStore() and continues to populate OAuthFlowResult from the returned dcr.Resolution. Simpler, ships faster, but forfeits cross-invocation cache hits at the resolver layer (the remote handler still reads CachedClientID directly, so cross-invocation reuse still works in practice — just outside the resolver).
Recommend (a) if the secret-provider plumbing is straightforward; otherwise (b) is acceptable provided the commit message documents the trade-off.
-
Replace the discovery.go DCR call. Delete the direct oauthproto.RegisterClientDynamically call at pkg/auth/discovery/discovery.go:747. Translate the existing CLI-flag fallback error message ("this provider does not support Dynamic Client Registration (DCR). Please configure ...") into a check on the resolver's returned error, since the resolver also synthesises a registration endpoint per RFC 8414 in some cases.
-
Add the CI grep guard. Either a new task check-dcr-isolation Taskfile target invoked from the existing CI lint/check job, or a one-line grep in the existing job. The check fails if RegisterClientDynamically matches anywhere outside pkg/auth/dcr/ and pkg/oauthproto/.
-
Drop the 4a doc caveat. With a real second consumer in hand, edit pkg/auth/dcr/resolver.go's package doc comment to remove the "target state" framing introduced in 4a's review-feedback commit.
Code pointers
pkg/auth/discovery/discovery.go:500-535 — OAuthFlowConfig, OAuthFlowResult, shouldDynamicallyRegisterClient. The migration site is centred here.
pkg/auth/discovery/discovery.go:589-613 — handleDynamicRegistration. This is the function that currently calls oauthproto.RegisterClientDynamically (line 747 is inside its helper).
pkg/auth/discovery/discovery.go:744-754 — the direct oauthproto.RegisterClientDynamically call to remove.
pkg/auth/dcr/resolver.go:386 — ResolveCredentials signature. Refactor target.
pkg/auth/dcr/resolver.go:140 — Resolution struct. CLI consumer reads ClientID / ClientSecret off this.
pkg/auth/dcr/resolver.go:208,253,287 — NeedsDCR / ConsumeResolution / ApplyResolutionToOAuth2Config. Profile-coupled API to either neutralise or split.
pkg/auth/remote/handler.go:252-258 — where CachedClientID / CachedClientSecretRef are read back. The persistence contract this PR must preserve.
Patterns & frameworks
- Plain
testing + github.com/stretchr/testify per .claude/rules/testing.md — NOT Ginkgo.
- Always drive builds/tests/lint through
task commands.
- Commit-message style: imperative mood, capitalized subject, no trailing period, 50-char limit, no Conventional Commits prefixes.
- PR-size cap: 400 LOC / 10 files (excluding tests/docs/generated). If 4b exceeds the cap after the input refactor, split into a (4b-i: neutralise resolver inputs) + (4b-ii: migrate CLI + add guard) pair.
Testing Strategy
Unit tests
Integration tests
Edge cases
Out of Scope
References
Description
Sub-issue 4b of #5145. With sub-issue 4a (PR #5198) the new
pkg/auth/dcrpackage is in place andEmbeddedAuthServerconsumes it. The CLI OAuth flow atpkg/auth/discovery/discovery.go::PerformOAuthFlowstill callsoauthproto.RegisterClientDynamicallydirectly, so it does not yet inherit the review-property behaviours added during #5042 (S256 PKCE gating, RFC 7591 §3.2.1 expiry-driven refetch, bearer-token transport with redirect refusal, panic recovery, singleflight deduplication). This sub-issue migrates that call site to the shared resolver and locks in the invariant with a CI grep guard.After 4b lands, all four acceptance criteria of the parent issue are met and #5145 closes.
Context
pkg/auth/dcr.ResolveCredentialstoday takes embedded-authserver-shaped types —*authserver.OAuth2UpstreamRunConfigand*upstream.OAuth2Config— because in 4a we deliberately kept the move-only scope rather than designing a profile-neutral input with only one consumer in hand. 4b is when the second consumer arrives and the right shape becomes obvious.The CLI side speaks
OAuthFlowConfig(pkg/auth/discovery/discovery.go:501) and persists DCR results intoOAuthFlowResult.ClientID/ClientSecret(pkg/auth/discovery/discovery.go:517). The downstream remote handler reads those back viapkg/auth/remote/handler.go'sCachedClientID/CachedClientSecretRef/CachedRegTokenReffields. That is the CLI's existing persistence model and the migration must preserve it.Dependencies: sub-issue 4a (PR #5198) — provides the
pkg/auth/dcrpackage and theEmbeddedAuthServerconsumer.Blocks: closure of #5145.
Acceptance Criteria
Profile-neutral resolver input —
pkg/auth/dcrpkg/auth/dcrexposes a profile-neutral input type (or a small adapter pair) so bothEmbeddedAuthServerand the CLI flow can callResolveCredentialswithout coupling the package to either consumer's domain types. The current*authserver.OAuth2UpstreamRunConfig/*upstream.OAuth2Configparameters are either replaced or wrapped.pkg/auth/dcr/resolver.gois updated to drop the "target state" caveat added in 4a — the package is now genuinely profile-agnostic.EmbeddedAuthServercall sites continue to work (either unchanged via an adapter, or migrated to the new shape in the same PR).CLI flow migration —
pkg/auth/discoverypkg/auth/discovery/discovery.go::PerformOAuthFlowno longer callsoauthproto.RegisterClientDynamicallydirectly. The DCR step routes throughdcr.ResolveCredentials.handleDynamicRegistration(or its replacement) populatesOAuthFlowResult.ClientID/ClientSecretfrom thedcr.Resolutionreturned by the resolver.NewDynamicClientRegistrationRequest(config.Scopes, config.CallbackPort)shape), and the existing CLI-flag fallback error message ("configure with--remote-auth-client-id/--remote-auth-client-secret") when the provider does not advertise a registration endpoint.Credential persistence —
pkg/auth/remote/handler.goCachedClientID/CachedClientSecretRef/CachedRegTokenRefcontinue to work as the CLI's persistent credential store across runs. Either: (a) wrap them as adcr.CredentialStoreadapter so the resolver short-circuits on cache hit, or (b) treat them as a read-only fallback afterdcr.ResolveCredentialsreturns, with the resolver running againstdcr.NewInMemoryStore()for the duration of a single CLI invocation.Inherited behaviours — verified by tests
plainPKCE, the CLI surfaces a clear error from the resolver rather than silently downgrading.PerformOAuthFlowcall re-registers.PerformOAuthFlowinvocations with the same(issuer, scopes, redirectURI)tuple result in exactly one upstream/registercall.Invariant guard
oauthproto.RegisterClientDynamicallyis referenced outsidepkg/auth/dcr/orpkg/oauthproto/itself. Implementation: a grep step in the existing lint/check pipeline (or a Taskfile target invoked from CI), not a new linter. The parent issue calls this out: "No direct calls tooauthproto.RegisterClientDynamicallyoutsidepkg/auth/dcr(search-grep enforces this)."Cross-cutting
task build,task test,task lint-fix, andtask license-checkall pass.Technical Approach
Recommended implementation
Design the neutral input. Walk both call sites (embedded authserver and CLI) and identify the minimal field set the resolver actually reads: registration endpoint (or discovery URL), requested scopes, redirect URI, optional initial access token, optional explicit endpoint overrides, and an HTTP client. Promote that into a
dcr.Request(or similar) struct. Convert the existingOAuth2UpstreamRunConfig/upstream.OAuth2Configconsumers to construct adcr.Requestat the call site; the resolver no longer imports either domain type.Write the CLI adapter.
pkg/auth/discovery/discovery.goconstructs adcr.RequestfromOAuthFlowConfig(issuer, scopes, callback port → loopback redirect URI per RFC 8252 §7.3) and either:dcr.CredentialStorebacked byOAuthFlowResult/pkg/auth/remote/handler.go'sCached*fields. This is the cleaner long-term shape because cache hits stay inside the resolver. It does require designing a smallGet/Putadapter over thesecretProviderinterface used by the remote handler.dcr.NewInMemoryStore()and continues to populateOAuthFlowResultfrom the returneddcr.Resolution. Simpler, ships faster, but forfeits cross-invocation cache hits at the resolver layer (the remote handler still readsCachedClientIDdirectly, so cross-invocation reuse still works in practice — just outside the resolver).Recommend (a) if the secret-provider plumbing is straightforward; otherwise (b) is acceptable provided the commit message documents the trade-off.
Replace the discovery.go DCR call. Delete the direct
oauthproto.RegisterClientDynamicallycall atpkg/auth/discovery/discovery.go:747. Translate the existing CLI-flag fallback error message ("this provider does not support Dynamic Client Registration (DCR). Please configure ...") into a check on the resolver's returned error, since the resolver also synthesises a registration endpoint per RFC 8414 in some cases.Add the CI grep guard. Either a new
task check-dcr-isolationTaskfile target invoked from the existing CI lint/check job, or a one-line grep in the existing job. The check fails ifRegisterClientDynamicallymatches anywhere outsidepkg/auth/dcr/andpkg/oauthproto/.Drop the 4a doc caveat. With a real second consumer in hand, edit
pkg/auth/dcr/resolver.go's package doc comment to remove the "target state" framing introduced in 4a's review-feedback commit.Code pointers
pkg/auth/discovery/discovery.go:500-535—OAuthFlowConfig,OAuthFlowResult,shouldDynamicallyRegisterClient. The migration site is centred here.pkg/auth/discovery/discovery.go:589-613—handleDynamicRegistration. This is the function that currently callsoauthproto.RegisterClientDynamically(line 747 is inside its helper).pkg/auth/discovery/discovery.go:744-754— the directoauthproto.RegisterClientDynamicallycall to remove.pkg/auth/dcr/resolver.go:386—ResolveCredentialssignature. Refactor target.pkg/auth/dcr/resolver.go:140—Resolutionstruct. CLI consumer readsClientID/ClientSecretoff this.pkg/auth/dcr/resolver.go:208,253,287—NeedsDCR/ConsumeResolution/ApplyResolutionToOAuth2Config. Profile-coupled API to either neutralise or split.pkg/auth/remote/handler.go:252-258— whereCachedClientID/CachedClientSecretRefare read back. The persistence contract this PR must preserve.Patterns & frameworks
testing+github.com/stretchr/testifyper.claude/rules/testing.md— NOT Ginkgo.taskcommands.Testing Strategy
Unit tests
pkg/auth/dcr— resolver behaviour tests under the new neutral input type. Existing test cases carry over with minimal surface change.pkg/auth/discovery—PerformOAuthFlowagainst a mock authorization server that advertises/register. Assert that oneOAuthFlowResult.ClientIDis populated and that the resolver was the code path that produced it (e.g. via a fakedcr.CredentialStorewhosePutcall is observed).Integration tests
plainPKCE;PerformOAuthFlowreturns a resolver-shaped error.PerformOAuthFlowinvocation re-registers./registerwith 302 to an evil host; resolver refuses and the CLI surfaces the error.PerformOAuthFlowconcurrently with the same (issuer, scopes, redirectURI); the mock AS records exactly one/registerrequest.PerformOAuthFlowagainst the sameCached*fields; assert the second call short-circuits (zero/registerrequests).Edge cases
registration_endpointand synthesis is unavailable: CLI surfaces the existing flag-fallback message verbatim.pkg/oauthproto.handleHTTPResponse; the resolver surfaces it through).Out of Scope
Notify When DCR Is Unsupported for Remote MCP ServerUX work (Notify When DCR Is Unsupported for Remote MCP Server #2680).pkg/oauthprotowire-shape types.pkg/auth/remote/handler.go's caching beyond what is needed to satisfy the persistence-preservation acceptance criterion.References
pkg/auth/dcrand migratedEmbeddedAuthServer..claude/rules/go-style.md,.claude/rules/testing.md,.claude/rules/pr-creation.md,CLAUDE.md.