Clarify saved and effective provider selection - #725
Conversation
Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughProvider listings now distinguish persisted and runtime-resolved profiles. Provider selection reports environment override resolution, while configuration mutations require exact persisted names and reject ambiguous duplicates. Authentication, setup, and TUI flows preflight configuration before persistence. ChangesProvider configuration and selection
Provider CLI behavior
Authentication and persistence preflight
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ProvidersUse
participant UserConfig
participant Resolver
ProvidersUse->>UserConfig: save requested provider
ProvidersUse->>Resolver: resolve environment override
Resolver-->>ProvidersUse: return resolved, unresolved, or deferred state
ProvidersUse-->>ProvidersUse: report effectiveProvider and envProviderResolves
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/cli/command_center_test.go (1)
194-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReset
stderralongsidestdoutfor clean failure messages.While this test passes correctly, it is a good practice to reset both output buffers before reusing them. This ensures that if the second command fails,
stderr.String()only contains the relevant error from the second execution, preventing confusingt.Fatalferror messages containing leftover artifacts.♻️ Proposed refactor
} stdout.Reset() + stderr.Reset() if code := runWithDeps([]string{"providers", "list"}, &stdout, &stderr, deps); code != exitSuccess { t.Fatalf("code=%d stderr=%s", code, stderr.String())🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/command_center_test.go` around lines 194 - 229, Reset stderr alongside stdout before the second runWithDeps invocation in TestRunProvidersListMarksUserAndRuntimeProfiles, so failure reporting reflects only the second command’s output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/cli/command_center_test.go`:
- Around line 194-229: Reset stderr alongside stdout before the second
runWithDeps invocation in TestRunProvidersListMarksUserAndRuntimeProfiles, so
failure reporting reflects only the second command’s output.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 86a322da-ff12-4cef-bf1f-5f77bcde8327
📒 Files selected for processing (4)
internal/cli/command_center.gointernal/cli/command_center_test.gointernal/cli/provider_onboarding.gointernal/cli/provider_onboarding_test.go
There was a problem hiding this comment.
Pull request overview
This PR improves clarity around provider selection by distinguishing the provider saved in user config from the provider that is actually effective at runtime (especially when ZERO_PROVIDER is set), and by enriching provider listing output so UIs can tell which entries are user-selectable vs runtime-derived.
Changes:
- Update
providers useto report whenZERO_PROVIDERoverrides the newly saved active provider, and include effective/override metadata in JSON output. - Enhance
providers list/currentoutput (human + JSON) withselectableandsourcemetadata to distinguish user-config providers from runtime-only entries. - Add tests covering the override messaging/JSON payload and the selectable/source metadata in provider lists.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| internal/cli/provider_onboarding.go | Adds override detection for ZERO_PROVIDER to clarify saved vs effective provider; enriches JSON output and error messaging for non-selectable providers. |
| internal/cli/provider_onboarding_test.go | Adds coverage for override messaging/JSON and for improved error text when selecting runtime-only providers. |
| internal/cli/command_center.go | Adds provider list metadata (selectable, source) and updates formatting to mark runtime-only/non-selectable entries. |
| internal/cli/command_center_test.go | Adds coverage for new selectable/source JSON fields and for the runtime-only marker in human output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
please rebase. to main and fix conflicts |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P2] Rebase this stale, superseded branch before merging.
internal/cli/provider_onboarding.go
The current head is not descended frommainand has content conflicts in both this file and its test. The resulting base-to-head comparison spans 227 files rather than the PR's four-file merge-base diff. Main already contains #716's runtime-only-provider handling and #767's fix for #721; resolving these conflicts by taking this branch would remove both. Please rebase onto currentmain, retain the existing fixes, and submit the resolved diff for review rather than merging this head. -
[P2] Do not report an unresolvable
ZERO_PROVIDERvalue as the effective provider.
internal/cli/provider_onboarding.go:64
A nonemptyZERO_PROVIDERis reported as the effective provider without checking that it resolves. For example, a staleZERO_PROVIDER=runtimewhen noruntimeprofile exists printsEffective provider: runtimeand suggestszero providers check runtime, but the next resolution fails before that check can run. Validate the override against the resolved provider list, or report it as invalid and direct the user to fix or unset it.
|
Heads-up @PierrunoYT: #767 merged and covers the core of this (the warning when ZERO_PROVIDER overrides a providers-use selection, #721). Your PR goes further though, and the extra parts are genuinely useful: the JSON effective-provider/override-source fields and the user-config-vs-runtime-only list marking are not in #767, so this is not fully superseded. To carry it forward it would need a rebase onto main (post-#767) and a trim down to just the net-new parts (the JSON output, the list marking, and the runtime-only explanation), since the warning itself is now landed. Happy to review that, or if you would rather, fold those into a fresh focused PR. Let us know which you prefer. |
…lection-overrides Keeps main's Gitlawb#767 override reporting and drops this branch's duplicate of it, so the branch reduces to what main does not have yet: - providers list/current: per-entry selectable + source metadata (JSON) and a "(not selectable via providers use)" marker in the human output. - providers use: an actionable error when the requested name is only resolvable at runtime and cannot be saved. The override plumbing is main's: activeProviderEnvOverride reading the injected deps.getenv, not a direct os.Getenv, so tests stay hermetic. The branch's duplicate `providers use` override tests are dropped in favour of main's; its runtime-only-selection test is kept. Also addresses the two open review points: - An unresolvable ZERO_PROVIDER is no longer reported as the effective provider. activeProviderEnvOverrideResolves checks the saved profile in the config file just written, then the resolver for env-derived profiles; when neither matches, the note says the override resolves to nothing and the JSON payload omits effectiveProvider (adding envProvider/envProviderResolves). - Non-user-config entries are labelled source "resolved" rather than "runtime": project config and provider commands persist profiles too, just not where `providers use` writes, so the only claim the label makes is that the entry cannot be selected with that command.
|
Merged current [P2] Rebase this stale, superseded branch — done, and resolved away from this branch where
Net diff against [P2] Do not report an unresolvable
@copilot: "runtime-only; not selectable/saved" over-claims for other persisted sources — valid, and it applies to the @coderabbitai: reset Validation (Windows host, Go 1.26.5): @jatmn @Vasanthdev2004 — the resolved diff is ready for another look; I can't use the reviewer-request button on this repo, hence the mention. @coderabbitai review |
|
✏️ Learnings added
✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
internal/cli/provider_onboarding_test.go (1)
163-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a companion test for the resolver-fallback-resolves-true case.
This test nicely covers the fully-unresolvable override. Consider adding a sibling test where
ZERO_PROVIDERnames a provider that is not persisted but is resolvable viaresolveCommandCenterConfig(e.g. set an ambientOPENAI_API_KEYand point the override at the env-derived provider name while switching to a different saved profile). That exercises the second branch ofactiveProviderEnvOverrideResolves(lines 148-156 in provider_onboarding.go), which currently only sees coverage via the persisted-check branch and the fully-unresolvable branch.As per path instructions, "
**/*_test.go: ... add regression tests for behavior changes."🧪 Suggested additional test sketch
func TestRunProvidersUseFlagsResolvableEnvOverrideViaResolver(t *testing.T) { t.Setenv("OPENAI_API_KEY", "sk-env") configPath := providersUseOverrideConfig(t) deps := providerSetupDeps(configPath) deps.getenv = func(key string) string { switch key { case config.ActiveProviderEnv: return "openai" case "OPENAI_API_KEY": return "sk-env" default: return "" } } var stdout, stderr bytes.Buffer if code := runWithDeps([]string{"providers", "use", "fast", "--json"}, &stdout, &stderr, deps); code != exitSuccess { t.Fatalf("exit = %d, want %d: %s", code, exitSuccess, stderr.String()) } var payload map[string]any if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { t.Fatalf("decode JSON: %v\n%s", err, stdout.String()) } if payload["effectiveProvider"] != "openai" { t.Fatalf("expected env-derived override to resolve via resolver fallback, got %#v", payload) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/provider_onboarding_test.go` around lines 163 - 213, Add a sibling regression test next to TestRunProvidersUseFlagsUnresolvableEnvOverride that covers an environment override naming a provider absent from persisted profiles but resolvable by resolveCommandCenterConfig, such as “openai” with an injected OPENAI_API_KEY. Invoke providers use with --json and assert successful execution reports that provider as effectiveProvider, exercising the resolver-fallback branch of activeProviderEnvOverrideResolves while retaining the existing unresolvable test.Source: Path instructions
internal/cli/provider_onboarding.go (1)
137-158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winResolver-fallback branch of
activeProviderEnvOverrideResolvesis untested.This function has two independent resolution paths: the persisted-config check (line 149) and the resolver-fallback via
resolveCommandCenterConfig/providerResolvedByName(lines 152-156). Only the persisted-true case ("work") and the fully-unresolvable case ("removed-profile") are covered by tests; the case whereZERO_PROVIDERnames an env-derived-but-unpersisted provider (e.g. an ambientOPENAI_API_KEY-derived profile) is not tested, leaving one of the two logical branches unverified.See companion comment on
internal/cli/provider_onboarding_test.gofor the suggested regression test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/provider_onboarding.go` around lines 137 - 158, Add a regression test for activeProviderEnvOverrideResolves covering an env-derived provider that is not persisted, such as one synthesized from an ambient OPENAI_API_KEY. Configure the test so the persisted-profile check does not match, resolveCommandCenterConfig succeeds, and providerResolvedByName finds the override, asserting the function returns true while preserving existing persisted and unresolvable cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/cli/provider_onboarding_test.go`:
- Around line 163-213: Add a sibling regression test next to
TestRunProvidersUseFlagsUnresolvableEnvOverride that covers an environment
override naming a provider absent from persisted profiles but resolvable by
resolveCommandCenterConfig, such as “openai” with an injected OPENAI_API_KEY.
Invoke providers use with --json and assert successful execution reports that
provider as effectiveProvider, exercising the resolver-fallback branch of
activeProviderEnvOverrideResolves while retaining the existing unresolvable
test.
In `@internal/cli/provider_onboarding.go`:
- Around line 137-158: Add a regression test for
activeProviderEnvOverrideResolves covering an env-derived provider that is not
persisted, such as one synthesized from an ambient OPENAI_API_KEY. Configure the
test so the persisted-profile check does not match, resolveCommandCenterConfig
succeeds, and providerResolvedByName finds the override, asserting the function
returns true while preserving existing persisted and unresolvable cases.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f848d4aa-1f56-4478-ba84-ea66efece4ee
📒 Files selected for processing (4)
internal/cli/command_center.gointernal/cli/command_center_test.gointernal/cli/provider_onboarding.gointernal/cli/provider_onboarding_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/cli/command_center_test.go
- internal/cli/command_center.go
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P2] Determine the effective override from the resolved active profile
internal/cli/provider_onboarding.go:149
A persisted row, or merely a matching entry inresolved.Providers, is not proof thatZERO_PROVIDERis the provider a subsequent invocation will use. For example, a saved OpenAI-compatible profile without a model passes the persisted fast path, soproviders use fast --jsonreportsenvProviderResolves: trueandeffectiveProvider: "broken"; the next resolution instead fails because the activebrokenprofile requires a model. Likewise, a provider command can leave the env-named profile in the list while setting a different active provider. Run the resolver for every override and require the successfully resolvedActiveProviderto match the override before emitting the effective-provider fields/note. -
[P3] Do not case-fold provider identities when labeling individual list entries selectable
internal/cli/command_center.go:154
Resolution merges provider names case-sensitively, but this map collapsesworkandWORK. If user config hasworkand project config (or a provider command) contributesWORK, both resolved entries are labeledselectable: true, source: "user-config".providers use WORKonly updates the user-configworkrow, so it cannot select the displayedWORKentry. Preserve the concrete persisted identity when deriving the metadata (or explicitly reject/handle case-only collisions) and add a regression test.
activeProviderEnvOverrideResolves no longer treats a config.json row or a resolved-list match as proof ZERO_PROVIDER is effective. It now always runs the resolver and requires the resolved ActiveProvider to match the override, so a persisted-but-broken profile (e.g. missing a required model) is reported as unresolvable instead of falsely "effective". providers list/current also stopped case-folding provider names when deriving selectable/source metadata. Resolution merges providers case-sensitively, so a project config or provider command can add a "WORK" entry alongside a persisted "work"; `providers use` can only ever select the exact persisted casing, so the case-variant entry must not be labeled selectable too. Addresses review feedback from jatmn on PR Gitlawb#725. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/cli/command_center_test.go`:
- Around line 271-276: Update the assertions in the provider cases of the test
to compare Source directly against the documented JSON strings "user-config" and
"resolved" instead of production constants, while preserving the existing
Selectable checks and failure messages.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: aa142389-f1f1-48db-8ae1-a5138ccf7608
📒 Files selected for processing (4)
internal/cli/command_center.gointernal/cli/command_center_test.gointernal/cli/provider_onboarding.gointernal/cli/provider_onboarding_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
- internal/cli/provider_onboarding.go
- internal/cli/command_center.go
- internal/cli/provider_onboarding_test.go
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P2] Avoid executing the provider command just to render the override note
internal/cli/provider_onboarding.go:150
activeProviderEnvOverrideResolvesnow performs a full config resolution afterproviders usehas already writtenconfig.json. WhenZERO_PROVIDER_COMMANDis configured, that resolution runs the configured shell command (and can wait up to five seconds); its side effects or failure are then discarded and reported as an unresolvedZERO_PROVIDER. This makes a previously config-only selection unexpectedly execute an external command after committing the change. Determine the override status without loading the provider command, or surface and handle that resolution failure before claiming a successful selection. -
[P2] Resolve the case-variant provider identity ambiguity in list selectability
internal/cli/command_center.go:160
The new exact-case lookup correctly treats a resolvedWORKprofile as distinct from persistedwork, butProviderPersistedandSetActiveProviderstill match case-insensitively. Copying the displayedWORKintozero providers use WORKtherefore exits successfully while silently activating the different persistedworkprofile. Case-only persisted duplicates have the inverse problem: both are reported selectable although the mutator always selects the first match. Align the mutation and listing identity rules (or reject ambiguous case variants) so a selected list entry cannot resolve to a different provider.
|
Addressed the Windows CI failure in commit 4749a7b. Root cause: Validation:
The pinned standalone lint/vulnerability tools could not be rerun locally because this Windows Go launcher uses a Go 1.24.3 base toolchain for |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
The activeProvider hand-off regression from the prior round is fixed on a6bbf74 (TestProviderMutatorsHandOffCaseVariantActiveProvider). Logout also clears OAuth/API secrets before returning on invalid config, which is an improvement over main. The items below are verified on 4749a7b7; regressions introduced by this PR's case-identity and credential changes are separated from pre-existing gaps the branch touches but did not create.
Findings
-
[P2] Delete the shared credential when removing the keyed case-duplicate row
internal/cli/provider_onboarding.go:503,internal/tui/provider_manager.go:367
PR regression.configContainsCaseVariantProvideris new here. It correctly keeps the key forremove WORKwhileworksurvives (TestRunProvidersRemoveCaseDuplicateKeepsSurvivorKey), but it also skips deletion when the keyed row is removed. Onmain,providers removealways calledremoveStoredProviderKeyAt; this PR added the skip. With{"providers":[{"name":"work","apiKeyStored":true},{"name":"WORK"}]},zero providers remove workdrops the keyed profile, leavesWORKwithoutapiKeyStored, and keeps the encrypted key in the credstore. The survivor cannot load it (ApplyStoredAPIKeyonly runs whenAPIKeyStoredis true). Please delete the key when the removed row ownedapiKeyStored, or transfer the marker to the surviving case-variant before skipping deletion; mirror the same rule in the TUI cleanup path. -
[P2] Finish OAuth logout while you are hardening credential cleanup
internal/cli/auth.go:503
Pre-existing gap, PR-incomplete.manager.Logouthas always deleted only the spelling the user typed. This PR correctly added dualkeyStore.Deletefor profile vs config row names, but OAuth still runs once with the raw argument. A profile saved as{name:"my-xai", catalogId:"xai"}withzero auth login xaistores the token underprovider:xai;zero auth logout my-xaican delete API keys yet leave the OAuth token behind and still report success. The runtime already resolves candidates viaProviderProfile.OAuthLoginCandidates()/oauth.FirstStored(internal/tui/command_center.go:653). Please delete every stored OAuth candidate for the resolved profile, matching the API-key fix you just added.
Findings (lower priority)
-
[P3] Clear
apiKeyStoredon every folded row during recovery logout
internal/cli/auth.go:530
PR regression. Onmain, logout had no config preflight andClearProviderKeyStoredmatched case-insensitively, so recovery on ambiguous configs was less broken. This PR addsPreflightUserConfig, deletes secrets, then errors beforeClearProviderKeyStored— which is now exact-match only.TestRunAuthLogoutCleansCredentialsWhenConfigIsAmbiguousdocuments the result: credentials gone,DEMOstill hasapiKeyStored:true, config unchanged. Either clear the marker on every case-variant row whose key was deleted, or say explicitly in the error that the marker must be edited by hand. -
[P3] Clear
apiKeyStoredon every folded row when removing a stored key in the TUI
internal/tui/provider_wizard.go:1382
PR regression.applyManageKeyChoicedeletes the folded credstore entry but clears the marker only for the exact spelling chosen. Onmain,ClearProviderKeyStoredwas case-insensitive, so this path stayed consistent. After this PR's exact-marker rule, removing the key forWORKwhileworkownsapiKeyStored:truedeletes the shared secret yet leaves the marker onwork. Apply the same case-variant awareness you added toproviders removeand provider delete. -
[P3] Match in-memory provider list updates to exact-case deletes
internal/tui/provider_manager.go:379,internal/tui/provider_manager.go:398
Pre-existing drift; more visible after this PR.removeSavedProviderstill usesEqualFoldand was not changed here.deleteManagerSelectionnow calls exact-caseRemoveProvider, so deleting a resolved-onlyWORKrow can evict persistedworkfrom the manager UI while it remains inconfig.json. Worth fixing for consistency with the exact-identity work in this PR, but not a regression from it.
Needs maintainer decision @Vasanthdev2004 @gnanam1990
- Ambiguous case-duplicate user config remains a hard startup failure with only CLI exact-name removal as repair
internal/config/resolver.go:77,internal/config/writer.go:423
Intentional new policy in this PR, not a defect. Blockingwork/WORKatResolve()is deliberate, andRemoveProvidercan delete one exact duplicate without pre-validation. That is enough forzero providers remove WORKfrom a shell, but not from the TUI while Zero cannot resolve config. Please confirm this one-time manual-JSON migration is acceptable, or add a documented repair path that does not require a successful resolve first.
…/remove - providers remove / TUI delete: transfer the apiKeyStored marker to a surviving case-variant row instead of orphaning the still-shared credential-store secret when the row that owned the marker is removed - auth logout: delete every OAuth login candidate (profile name, canonical persisted name, catalog id), not just the typed spelling, so a catalog-id-addressed login is fully cleared - auth logout / TUI remove-key: clear the apiKeyStored marker on every case-variant row sharing the deleted credential-store secret, and say explicitly when an ambiguous config blocks clearing a stale marker - TUI provider manager: align removeSavedProvider with RemoveProvider's exact-name identity so deleting one case-variant row can no longer evict its surviving sibling from the in-memory list Addresses jatmn's Gitlawb#725 review round on 4749a7b.
|
Addressed the review round on 4749a7b in 0479e9f:
Left the "needs maintainer decision" repair-path item as-is — that's a policy call, not something to resolve unilaterally. Validation: Requesting re-review. |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
The 0479e9f follow-up addressed the prior round's keyed case-variant removal, multi-candidate OAuth logout (when config validation passes), and removeSavedProvider exact-identity alignment. The items below are verified on 0479e9f667fa4aff4cb3a7e002e2b7e65ebc6423 against d37de9214b1ba86542f4f5ef3b060bb96849763b. Regressions introduced or left incomplete by this branch are separated from pre-existing gaps and polish.
Findings
-
[P2] Sync in-memory
savedProvidersafter transferringapiKeyStoredon case-variant delete
internal/tui/provider_manager.go:373
deleteManagerSelectioncallsTransferProviderAPIKeyStoredMarkeron disk when removing the keyed case-variant row, but only runsremoveSavedProvideron the in-memory list.reloadProviderManagerRowsrebuilds fromsavedProviders, so the survivor keepsAPIKeyStored: falsein memory whileconfig.jsonhas the marker.providerManagerCredStategates store lookups onprofile.APIKeyStored, so the manager showsno credentialuntil restart even though disk and the credstore are correct.TestProviderManagerDeleteTransfersKeyMarkerToSurvivingCaseVariantchecks persisted config only, not the manager UI. Please patch the survivor insavedProviders(or reload that row from config) after a successful marker transfer. -
[P2] Delete API keys under every OAuth login candidate during logout
internal/cli/auth.go:547
This PR's logout hardening deletes OAuth tokens for profile name, canonical persisted name, and catalog id, but API-key deletion still only callskeyStore.Deletefor the user-typed spelling andconfigProvider. A key stored under the catalog id (for examplexaiwhile the saved row ismy-xai) surviveszero auth logout my-xai.mainonly deleted the typed spelling once; this branch improved OAuth coverage but left the same asymmetry for API keys. Please delete credstore entries for the same candidate set used for OAuth logout. -
[P3] Resolve OAuth logout candidates even when unrelated config validation fails
internal/cli/auth.go:496
runAuthLogoutgatesPersistedProviderIdentity,ProviderRow, and catalog-id candidate expansion onconfigErr == nilfromPreflightUserConfig, even though those reads do not require a valid config — onlywriteConfigFiledoes. With an unrelated ambiguous file (for exampledemo/DEMO) and a profile{name:"my-xai", catalogId:"xai"}whose token was stored underxai,zero auth logout my-xaideletes onlyprovider:my-xaiand leaves the catalog-id token behind. This is a narrow recovery edge case in the new logout path (credentials are deleted before the marker write fails), but the candidate list should not depend on preflight passing. Please resolve identity and expand OAuth candidates regardless of validation, and gate only the marker-clearing write onconfigErr.
Findings (lower priority)
-
[P3] Apply OAuth candidate resolution to
auth refreshandauth status
internal/cli/auth.go:652,internal/cli/auth.go:687
runAuthRefresh/runAuthRefreshWatchandfilterAuthStatusesstill look up onlyoauth.ProviderKey(provider)with the raw argument. Afterzero auth login xaion{name:"codex", catalogId:"chatgpt"},zero auth refresh codexandzero auth status codexreport no token even thoughoauthLoginForProfileand the TUI gate would find the catalog-id login. This gap predates this branch (mainbehaved the same); this PR hardened logout but did not extend the same semantics here. Worth fixing for auth-CLI parity, but not a regression from this diff. -
[P3] Finish the exact-identity migration for in-memory provider list updates
internal/tui/provider_manager.go:704,internal/tui/provider_wizard.go:736
This PR movedremoveSavedProviderto exact-name matching, butapplySavedProviderEditandupsertSavedProviderProfilestill useEqualFoldand update the first fold-equal entry — the same pre-existing drift noted in the prior review round. It only bites whensavedProviderscontains case-variant siblings (uncommon). Align these helpers with the exact-identity rule when convenient; not introduced by this PR. -
[P3] Include
sourcein textproviders listoutput, or narrow the claim
internal/cli/command_center.go:441
JSON list output andproviders currenttext exposesource(user-configvsresolved), butproviders listtext only appends(not selectable via providers use). This is claims/output drift, not a runtime failure. Please printsourcein list text as well, or update the PR summary to say JSON/providers currentonly.
Needs maintainer decision @Vasanthdev2004 @gnanam1990
- Ambiguous case-duplicate user config remains a hard startup failure with only CLI exact-name removal as repair
internal/config/resolver.go:77,internal/config/writer.go:423
Blockingwork/WORKduplicates atResolve()is deliberate in this PR, andRemoveProvidercan delete one exact duplicate without pre-validation. That is enough forzero providers remove WORKfrom a shell, but not from the TUI while Zero cannot resolve config. Please confirm this one-time manual-JSON migration is acceptable, or add a documented repair path that does not require a successful resolve first.
- auth logout: delete API keys under every OAuth login candidate (typed name, canonical persisted name, catalog id), not just the first two — a key stored under the catalog id previously survived logout - auth logout: resolve identity and expand OAuth/API-key candidates regardless of PreflightUserConfig's result, since PersistedProviderIdentity and ProviderRow only read+parse raw JSON and never validate case-duplicate names; only the final apiKeyStored marker write still requires a valid config - TUI provider manager: mirror the on-disk apiKeyStored marker transfer into the in-memory savedProviders list on case-variant delete, so the manager UI doesn't show "no credential" for a survivor whose key is actually reachable until the process restarts - providers list (text): include the source (user-config/resolved) next to the not-selectable marker, matching the JSON output and `providers current` text Addresses jatmn's Gitlawb#725 review round on 0479e9f.
|
Addressed the review round on 0479e9f in 780a219:
Left the other two lower-priority P3s as-is since they're called out as pre-existing, not introduced by this branch: Validation: Requesting re-review. |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
The branch is mergeable and all required GitHub checks are green, but GitHub still shows the pull request as blocked on my earlier CHANGES_REQUESTED review from 0479e9f. Please dismiss or supersede that review after the items below are fixed.
The 780a219 follow-up addressed the prior round's in-memory apiKeyStored sync after case-variant delete, multi-candidate OAuth/API-key deletion on auth logout (including catalog-id keys), and logout candidate expansion when unrelated config validation fails. The items below are verified on 780a21968ca0ed9b5c5dc92e3390fa37e5661bad against merge-base 81a5e6cb716ba4b7ecf7ff3ff261dfa51fd01098.
Findings
-
[P3] Fix the case-variant
providers removewarning when marker transfer fails
internal/cli/provider_onboarding.go:544
When a case-variant survivor intentionally retains the shared credstore secret,keyErrfromTransferProviderAPIKeyStoredMarkeris reported as “could not be deleted and remains in the credential store.” That describes the wrong operation — deletion was skipped on purpose. Please distinguish marker-transfer failure from delete failure so users know the shared key may be unreachable on the survivor. -
[P3] Surface first-run setup failures from
persistOAuthLoginProvider
internal/tui/onboarding.go:665
After token OAuth during setup,_ = persistOAuthLoginProvider(...)is ignored while the wizard path stops withoauthErrwhen the same call fails. This pattern predates the branch, but the newValidatePersistedProviderNamesguards make profile creation fail more often after the oauth store already holds a token. Please matchapplyProviderWizardOAuth: setsetup.oauthErr, stay on the provider step, and do not advance when persist fails. -
[P3] Give TUI OpenRouter the same save-failure recovery as CLI OpenRouter
internal/tui/onboarding.go(setupOAuthCmd),internal/tui/provider_wizard.go(providerWizardOAuthCmdFor)
runAuthOpenRouternow prints the minted key and exits non-zero when persistence fails (TestRunAuthOpenRouterFailsWhenTheKeyCannotBeSaved). TUI OpenRouter still holds the key only in memory through later persist with no echo on failure. This is a real parity gap within the same PR — please print the minted key (or equivalent recovery instructions) before returning a save error, same as CLI. -
[P3] Preflight config before device-code OAuth phase 1 in the TUI
internal/tui/onboarding.go:578,internal/tui/provider_wizard.go:330
This PR added config preflight before device OAuth phase 2 and before CLI interactive auth, but phase 1 still requests a device code with no check. With ambiguous case-duplicateconfig.json, the user can complete external authorization and only then fail on poll/BeforeSave. Please preflight in the prepare commands beforePrepareDeviceLogin.
Findings (lower priority)
-
[P3] Extend OAuth candidate resolution to
auth refreshandauth status
internal/cli/auth.go:652,internal/cli/auth.go:622
runAuthLogoutnow mirrorsOAuthLoginCandidates, but refresh/status still look up onlyoauth.ProviderKey(provider)from the raw argument. This gap predates the branch; logout hardening makes the asymmetry more visible. Worth aligning when convenient, not a regression from this diff. -
[P3] Expose
selectable/sourceonzero configoutputs, or narrow the PR claim
internal/cli/command_center.go(formatConfigSummary,runConfigJSON)
The PR implements the distinction onzero providers list|current, notzero config. If the summary is meant to cover all provider listings, please extendzero config; otherwise narrow the PR description.
Needs maintainer decision @Vasanthdev2004 @gnanam1990
- Ambiguous case-duplicate user config is now a hard
Resolve()failure with CLI-only repair
internal/config/resolver.go:77,internal/config/writer.go:423
Blockingwork/WORKduplicates atResolve()is deliberate, andRemoveProvidercan delete one exact duplicate without pre-validation. That is enough forzero providers remove WORKfrom a shell, but not from the TUI while Zero cannot resolve config. Please confirm this one-time manual-JSON migration is acceptable, or add a documented repair path that does not require a successful resolve first.
Amp-Thread-ID: https://ampcode.com/threads/T-019fb8cf-a980-7568-80f1-fe34faaaecae Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
|
PierrunoYT addressed the four current review findings in
Added focused regressions for each TUI behavior. Validation passed:
The explicitly pre-existing refresh/status and @jatmn @Vasanthdev2004 — PierrunoYT requests a fresh review of the current head. |
jatmn
left a comment
There was a problem hiding this comment.
Findings
[P1] Rebase this branch onto the current PR base before merging
.release-please-manifest.json:1
The supplied base is 8e26679, but its merge-base with this head is 81a5e6c, so this head does not contain the current mainline changes. Consequently the actual PR diff rolls the release metadata and package version back from 0.6.0 to 0.5.0, deletes the 0.6.0 changelog entry, and reverts 7,104 lines across 131 files, including ACP cancellation, sandbox, provider-retry, and swarm fixes that are unrelated to #721. Please rebase (or merge and resolve) the current base while preserving its changes, then submit the resulting focused diff for a fresh review.
[P2] Do not hide a case-distinct environment override
internal/cli/provider_onboarding.go:161
activeProviderEnvOverride treats ZERO_PROVIDER=WORK as identical to a newly saved user provider named work. That is not generally true: a workspace config (or provider-command result) can supply a separate exact-case WORK profile, and resolution selects that exact row. providers use work then omits both the warning and the JSON override fields even though subsequent runs use WORK and potentially different credentials or endpoint. Compare the resolved identity/source (or at least require exact equality before suppressing the override) so the command reports the provider that will actually be used.
[P1] Scope logout cleanup to the selected profile's credential identities
internal/cli/auth.go:513
PersistedProviderIdentity can select the first row with a matching catalog ID instead of a later exact-name row, and logout then uses that row's catalog ID as both an OAuth and API-key delete alias. Catalog IDs are shared by valid profiles: a stored-key work-xai, stored-key xai, and keyless personal-xai can all use catalogId:"xai". Logging out work-xai consequently deletes the shared xai OAuth token and the xai profile's API key, while only work-xai's marker is cleared. Resolve the exact target before catalog fallback, derive OAuth candidates only when that target can use them, and delete API keys only by proven profile ownership.
[P2] Reject exact duplicate persisted provider names
internal/config/writer.go:24
The new validator only rejects a repeated folded name when its spelling differs, so two rows named work (including names that trim to the same value) pass validation. That violates the stated one-credential-per-folded-name invariant: resolver merging silently coalesces the rows, while plaintext-key migration writes both values to the same normalized credential-store entry and can overwrite the first key. Reject any repeated folded identity before resolving or migrating.
[P2] Fail providers remove when its credential-marker handoff fails
internal/cli/provider_onboarding.go:503
In the case-variant path the selected row is removed before TransferProviderAPIKeyStoredMarker runs. If that write fails, the command reports a warning (or keyError in JSON) but still returns success, even though the surviving profile is left unmarked and cannot reach the shared stored key. Return a nonzero result for this partial failure, or make the remove/marker transfer transactional so scripts do not treat a broken credential handoff as complete.
[P1] Do not adopt an arbitrary profile sharing the catalog ID
internal/config/writer.go:89
AdoptPersistedCatalogProviderName calls PersistedProviderIdentity(catalogID), which returns the first row sharing that catalog ID. Multiple profiles may legitimately use one catalog provider: with an existing {name:"work-xai", catalogId:"xai"}, adding the default xai profile is silently retargeted and upserts into work-xai, replacing its endpoint/model and potentially its stored key. Restrict adoption to a uniquely proven case-variant default-name row; preserving an explicitly supplied default spelling is necessary too.
[P2] Use credential-store normalization to detect a key-sharing survivor
internal/cli/provider_onboarding.go:589
Survivor detection uses strings.EqualFold, whereas the credential store normalizes names with trimmed strings.ToLower. Those equivalence relations differ: s and long-s ſ pass persisted-name validation as distinct credential-store identities but compare equal here. Removing keyed s then retains its orphaned key and transfers the marker to ſ, whose credential-store lookup uses a different key. Compare the same trimmed lowercase representation used by credstore.normalizeProvider in both CLI and TUI removal paths.
…lection-overrides
Addresses jatmn's Gitlawb#725 review round on 64f186f. - auth logout resolves its target by exact name before catalog id, and expands to the catalog id only when no other row claims it. Catalog ids are shared by design, so logging out one profile was deleting a sibling's OAuth token and API key. - AdoptPersistedCatalogProviderName follows only a case variant of the catalog's own default NAME. Following an arbitrary row that listed the same catalogId silently retargeted the default profile onto it, overwriting its endpoint, model, and stored key. - ValidatePersistedProviderNames rejects any repeated folded identity, not just case-distinct spellings, so two rows literally named "work" can no longer coalesce at resolve time or overwrite one another's migrated key. - providers use no longer hides a case-distinct ZERO_PROVIDER. The suppression now requires resolution to prove the env value lands on the row just written, so a separate exact-case project profile is reported. - providers remove exits nonzero when the credential-marker handoff to a surviving case variant fails, and says which operation failed. The handoff cannot precede the removal: while both rows exist the config is ambiguous and every write is rejected. - Survivor detection in the CLI and TUI removal paths compares with credstore.NormalizeProvider instead of strings.EqualFold, matching the credential store's own trimmed-lowercase keying. Also merges the current base so the diff no longer reverts unrelated mainline changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks @jatmn — all seven findings are addressed on [P1] Rebase onto the current base — merged [P1] Scope logout cleanup to the selected profile's credential identities — [P1] Do not adopt an arbitrary profile sharing the catalog ID — [P2] Reject exact duplicate persisted provider names — [P2] Do not hide a case-distinct environment override — [P2] Fail providers remove when its credential-marker handoff fails — the command now returns nonzero in both text and [P2] Use credential-store normalization to detect a key-sharing survivor — exported Validation: On the maintainer question about ambiguous case-duplicate configs being a hard |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Use the credential-store identity when clearing stored-key markers
internal/config/credentials.go:139
This newly added helper compares names withstrings.EqualFold, while the store itself usescredstore.NormalizeProvider(TrimSpace+ToLower). Those relations differ:sand Unicode long-s (ſ) are valid distinct provider/store identities, butEqualFoldconsiders them equal. Logging out ofsdeletes only its store entry and then clearsapiKeyStoredon both profiles, leaving the still-presentſkey unreachable; the TUI's Remove Key flow takes the same path. Compare normalized store identities here and add a regression that preserves the unrelated marker. -
[P2] Keep the TUI's edit mirror exact-name aligned with the persisted mutation
internal/tui/provider_manager.go:701
This PR changesEditProviderto update the selected persisted row by exact name, but the follow-up in-memory updates still useEqualFold. Since the new validator permits distinct store identities such assandſ, editing the latter when the resolved list is ordereds,ſmutates the former in memory; if the live session is ons, the line at 701 also rewrites its provider name and exportedZERO_PROVIDERfor an edit to the other profile. The manager then disagrees with config.json until restart and can point spawned children at the wrong provider. Use the same exact identity for these UI mirror/session comparisons (and cover the distinct-Unicode case).
Amp-Thread-ID: https://ampcode.com/threads/T-019fbec7-b9dc-73a7-9676-675cf9b4d2c7 Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
|
Addressed the latest requested changes in
Validation run with Go 1.26.5:
@jatmn please re-review the latest head when you have a chance. |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Do not adopt an unrelated custom profile by case-folded name
internal/config/writer.go:121
AdoptPersistedCatalogProviderNameinterprets a case-folded name match as proof that the existing row represents the requested catalog provider, but it never verifies the row'scatalogId. A valid custom profile such as{name:"OpenRouter", catalogId:"custom-openai-compatible", baseURL:"https://corp.example/v1", model:"corp-model"}is therefore adopted byzero providers add openrouter. The caller then passes the adopted name toUpsertProvider, whose merge replaces the existing row's catalog ID, endpoint, model, transport/auth fields, and custom headers with OpenRouter's defaults; a pre-existing stored-key marker can survive that incompatible rewrite. The root cause is conflating display/name equivalence with provider ownership. Restrict adoption to a unique case-variant row whose catalog identity also matches the requested descriptor (or an equivalent explicitly supported identity); otherwise retain the requested default name and let the collision be reported rather than mutating a different profile. Please add a regression test with a customOpenRouter-named row and a non-OpenRouter catalog ID. -
[P1] Keep distinct Unicode names out of destructive logout candidate expansion
internal/config/writer.go:242
The new logout flow expands its deletion set from the profile returned byResolvePersistedProviderIdentity. That helper usesstrings.EqualFold, while the credential store deliberately defines identity with the narrowercredstore.NormalizeProviderrule:sand Unicode long-sſhave separate stored API-key entries. With a savedsprofile and its OAuth token/API key,zero auth logout ſfindssas a folded name match, adds it tocredentialCandidates, deletes its token and API key, and clears itsapiKeyStoredmarker—even thoughſis an unsaved, distinct provider identity. The root cause is using a broad human-facing case comparison as authority for destructive credential ownership. Make destructive candidate expansion require an exact persisted name or the same credential-store normalization used by the store; audit the catalog-ID ownership checks under that same rule, and add a deny test provinglogout ſcannot affect a savedscredential. -
[P2] Let normal provider writes use the same identity rule as the credential store
internal/config/writer.go:314
PreflightProviderWriteandUpsertProviderat line 343 still reject names withstrings.EqualFold, even though this PR's validator, credential-store helper, delete path, marker clearing, and edit path deliberately treatsand Unicode long-sſas separate identities. Starting with a validsprofile,providers add ... --name ſ(and the setup/wizard paths that call the same preflight) now fails with a case-insensitive collision despite using a separate credential-store entry. This leaves the newly introduced identity contract internally inconsistent and makes the supported distinct profile pair hand-edit-only. Centralize persisted-profile collision checks oncredstore.NormalizeProviderrather than mixingEqualFoldand store normalization, then cover add/setup/wizard creation of the distinct pair. -
[P2] Do not retain a shared key after a failed TUI marker transfer
internal/tui/provider_manager.go:370
When deleting a case-variant row, the manager setsretainStoredKeybefore attempting the post-removal marker transfer. IfTransferProviderAPIKeyStoredMarkerfails afterRemoveProviderhas succeeded—for example because the follow-up config write fails—the manager only adds a warning and still calls cleanup withretainStoredKey=true. The secret remains in the credential store, but the surviving row retainsapiKeyStored:false;ApplyStoredAPIKeyintentionally does not consult the store without that marker, so the key is now unreachable while the UI has already reportedDeleted <name>.The root cause is committing the retention decision before confirming the state that makes retention safe. Match the CLI's partial-failure behavior: only retain the key after a successful marker handoff, otherwise surface an incomplete delete and either preserve recoverable state or remove the unreachable credential deliberately. Add a failure-injection test for the post-removal marker write and assert that a completed-delete status is not shown with an inaccessible key.
The PR introduced a contract where the credential store's own rule —
credstore.NormalizeProvider — decides provider identity, which makes "s" and
Unicode long-s "s" separate profiles with separate secrets. Several paths were
still comparing with strings.EqualFold, which folds those two together, so the
two rules disagreed in both directions: destructive logout expansion adopted an
unrelated row's credentials, while ordinary adds rejected the supported pair as
a collision. sameProviderIdentity is now the single comparison behind identity
resolution, catalog-ownership checks, write preflight, upsert, rename, removal's
active-provider handoff, and the stored-key migration's same-entry check.
A folded NAME match is also no longer proof of ownership on its own.
AdoptPersistedCatalogProviderName required only that a case-variant row carry
the requested name, so `zero providers add openrouter` adopted a custom profile
saved as {name: "OpenRouter", catalogId: "custom-openai-compatible"} and handed
it to UpsertProvider, whose merge overwrites catalog id, endpoint, model,
transport and headers while a stored-key marker survives. The row's catalog
identity must now agree; a row declaring none is still adopted, since its name
is the only identity it has.
The TUI's delete also committed its retain-the-shared-key decision before the
marker handoff that makes retention safe. The handoff can only run after
RemoveProvider (an ambiguous two-row config rejects every write), so its failure
is reachable: the key stayed in the store with no row marked to read it while
the UI reported a completed "Deleted <name>." It now reports an incomplete
delete naming the survivor and the recovery step, and keeps the secret rather
than destroying it over a failed config write — matching `zero providers remove`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed [P1] Do not adopt an unrelated custom profile by case-folded name — [P1] Distinct Unicode names out of destructive expansion and [P2] normal writes use the store's rule are one fix, because they were the same inconsistency pointing in opposite directions. One correction worth flagging on the logout scenario, since it changes what a test there can prove: [P2] Do not retain a shared key after a failed TUI marker transfer — the manager no longer commits the retention decision before the handoff that makes retention safe. On failure it reports an incomplete delete naming the survivor and the recovery step ("the shared key is still in the credential store and WORK cannot reach it until you set a key for it again") instead of Mutation-checked: reverting Verification: |
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Do not adopt a name-only custom profile as a catalog provider
internal/config/writer.go:151
The new adoption helper treats anOpenRouterrow with an emptycatalogIdas proof that it is the catalogopenrouterprofile. That is not safe: a user can create a custom OpenAI-compatible profile namedOpenRouter, pointed at a private gateway, withapiKeyStored:trueand no catalog ID. Runningzero providers add openrouter,zero setup openrouter, or the provider wizard reaches this helper, changes the requested profile name toOpenRouter, and letsUpsertProvidermerge the OpenRouter defaults into that existing row. The base URL, model, transport/kind, and catalog ID are replaced, but the existing stored-key marker is preserved; on the next run the private-gateway secret is selected for the OpenRouter endpoint.The root cause is treating display-name equivalence as provider ownership. A missing catalog ID is absence of evidence, not evidence that the row belongs to the requested catalog. Restrict automatic adoption to a case variant whose catalog ID positively identifies the requested descriptor; otherwise leave the requested name unchanged and surface the existing collision for the user to resolve. Please cover CLI add, setup, and wizard persistence with a name-only custom
OpenRouterrow and a stored key, asserting that it is never rewritten and its key is never used for OpenRouter. -
[P2] Use credential-store identity for the active-provider fallback
internal/config/resolver.go:941
This PR deliberately makescredstore.NormalizeProviderthe persisted-profile identity rule and therefore permitssand Unicode long-sſas separate profiles with separate secrets. The active-row fallback introduced here still usesstrings.EqualFold, which is broader:strings.EqualFold("S", "s")andstrings.EqualFold("S", "ſ")are both true, while credential-store normalization maps onlyStos. With both valid rows present, a manually retained case-variantactiveProvider: "S"orZERO_PROVIDER=Sfinds no exact row, matches both in this fallback, and aborts as ambiguous instead of selecting the sole credential identitys; Zero cannot start.The root cause is a mixed identity model: mutations and credential ownership use normalized store identity, but runtime active-provider lookup still uses Unicode case folding. Centralize the persisted-provider lookup/equivalence rule and use it for the non-exact active-provider fallback as well, preserving exact-name precedence first. Add resolver coverage for
{name:"s"}plus{name:"ſ"}withactiveProviderandZERO_PROVIDERset toS, and assert that it consistently resolves thesprofile without reaching theſcredential.
Summary
ZERO_PROVIDERoverrides a provider saved byproviders useproviders useValidation
go test ./...go vet ./...govulncheck ./...Repository-wide golangci-lint still reports existing findings unrelated to this change.
Fixes #721
Summary by CodeRabbit
providers listnow labels profiles with case-sensitive selectable status and source (user-configvsresolved) in both text and--json.