Skip to content

Clarify saved and effective provider selection - #725

Open
PierrunoYT wants to merge 12 commits into
Gitlawb:mainfrom
PierrunoYT:fix/721-provider-selection-overrides
Open

Clarify saved and effective provider selection#725
PierrunoYT wants to merge 12 commits into
Gitlawb:mainfrom
PierrunoYT:fix/721-provider-selection-overrides

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • report when ZERO_PROVIDER overrides a provider saved by providers use
  • include the effective provider and override source in JSON output
  • mark provider list entries as user-config/selectable or runtime-only
  • explain why runtime-only providers cannot be selected with providers use

Validation

  • go test ./...
  • go vet ./...
  • govulncheck ./...
  • golangci-lint against new changes: 0 issues

Repository-wide golangci-lint still reports existing findings unrelated to this change.

Fixes #721

Summary by CodeRabbit

  • New Features
    • providers list now labels profiles with case-sensitive selectable status and source (user-config vs resolved) in both text and --json.
  • Bug Fixes
    • Provider identity and credential/key operations are now case-sensitive (with trimmed matching), preventing ambiguous case-only duplicates.
    • OAuth/provider flows now re-validate configuration immediately before saving tokens, improving correctness when config is changed during login.
    • Env-override behavior now reports selectable/deferred/resolved outcomes more accurately.
  • Tests
    • Added/expanded coverage for selectability/source labeling, case-sensitivity, env-override labeling, and OAuth revalidation/preflight.

Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
Copilot AI review requested due to automatic review settings July 18, 2026 09:58
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Provider 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.

Changes

Provider configuration and selection

Layer / File(s) Summary
Provider identity validation and mutation
internal/config/writer.go, internal/config/credentials.go, internal/config/*_test.go
Persisted provider names are validated for case-insensitive duplicates, while mutation targets use trimmed exact matching.
Active provider resolution
internal/config/resolver.go, internal/config/resolver_test.go
Active-provider resolution prefers exact matches, supports unique case-insensitive fallback, and rejects ambiguity.

Provider CLI behavior

Layer / File(s) Summary
Provider listing metadata
internal/cli/command_center.go, internal/cli/command_center_test.go
Provider listings expose selectable and source metadata in JSON and text output.
Provider selection override handling
internal/cli/provider_onboarding.go, internal/cli/provider_onboarding_test.go, internal/config/paths.go
providers use reports resolved, unresolved, and deferred environment overrides and rejects non-selectable or mismatched identities.

Authentication and persistence preflight

Layer / File(s) Summary
OAuth save validation
internal/cli/app.go, internal/cli/auth.go, internal/cli/auth_test.go, internal/oauth/manager.go
OAuth flows support before-save configuration validation, preventing token writes when configuration becomes invalid.
Provider write and TUI flow preflights
internal/cli/provider_setup.go, internal/cli/setup.go, internal/tui/*
Provider setup, wizard, and device-login flows pass config paths and preflight configuration before persistence.

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
Loading

Possibly related PRs

  • Gitlawb/zero#560: Overlaps provider listing rendering changes in internal/cli/command_center.go.

Suggested reviewers: gnanam1990, vasanthdev2004

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: clarifying saved versus effective provider selection.
Linked Issues check ✅ Passed The PR adds source/selectability metadata, visual non-selectable markers, and better handling of override and identity cases required by #721.
Out of Scope Changes check ✅ Passed The broader auth, setup, config, and TUI edits support the same provider-identity and selection behavior, so no clear unrelated scope stands out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/cli/command_center_test.go (1)

194-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reset stderr alongside stdout for 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 confusing t.Fatalf error 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

📥 Commits

Reviewing files that changed from the base of the PR and between 60dc84e and 4b26069.

📒 Files selected for processing (4)
  • internal/cli/command_center.go
  • internal/cli/command_center_test.go
  • internal/cli/provider_onboarding.go
  • internal/cli/provider_onboarding_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 use to report when ZERO_PROVIDER overrides the newly saved active provider, and include effective/override metadata in JSON output.
  • Enhance providers list/current output (human + JSON) with selectable and source metadata 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.

Comment thread internal/cli/command_center.go
Comment thread internal/cli/command_center_test.go Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 18, 2026
@kevincodex1

Copy link
Copy Markdown
Contributor

please rebase. to main and fix conflicts

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 from main and 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 current main, retain the existing fixes, and submit the resolved diff for review rather than merging this head.

  • [P2] Do not report an unresolvable ZERO_PROVIDER value as the effective provider.
    internal/cli/provider_onboarding.go:64
    A nonempty ZERO_PROVIDER is reported as the effective provider without checking that it resolves. For example, a stale ZERO_PROVIDER=runtime when no runtime profile exists prints Effective provider: runtime and suggests zero 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.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

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.
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Merged current main (dcf1a40) — the PR is MERGEABLE again — and addressed both findings plus the two bot comments.

[P2] Rebase this stale, superseded branch — done, and resolved away from this branch where main already had the behaviour. main's #767 covers the providers use override reporting with a cleaner design (the env read is injected through deps.getenv rather than a direct os.Getenv, so tests stay hermetic against an ambient ZERO_PROVIDER), and #716/#707 cover runtime-only provider handling. I kept all of that and dropped this branch's duplicate implementation and its two duplicate override tests, so the branch now adds only what main lacks:

  • providers list / providers current: per-entry selectable + source in JSON, and a (not selectable via providers use) marker in the human output.
  • providers use: an actionable error when the requested name is resolvable at runtime but cannot be saved (only providers saved in user config are selectable …).

Net diff against main is now 4 files / ~220 lines instead of the 227-file base-to-head comparison.

[P2] Do not report an unresolvable ZERO_PROVIDER value as the effective provider — fixed. New activeProviderEnvOverrideResolves checks, in order, the saved profile in the config file the command just wrote (so the check does not depend on ambient env) and then the resolver for env-synthesized profiles. When neither matches:

  • the text note now reads "… but no provider named X can be resolved, so Zero cannot start until you unset ZERO_PROVIDER or point it at a saved provider" instead of calling it effective and suggesting zero providers check X;
  • the JSON payload omits effectiveProvider entirely and reports overriddenByEnv, envProvider, and envProviderResolves: false.

TestRunProvidersUseFlagsUnresolvableEnvOverride covers both paths; main's resolvable-override tests still pass unchanged (their ZERO_PROVIDER=work is a saved profile, so it resolves and effectiveProvider is still reported).

@copilot: "runtime-only; not selectable/saved" over-claims for other persisted sources — valid, and it applies to the source value too, not just the prose. Project config and provider commands both contribute persisted profiles (mergeProjectConfig merges src.Providers), so those are not "runtime". The human marker is now (not selectable via providers use) — the only guaranteed claim — and the JSON source enum is user-config | resolved, documented next to the constants. The companion test comment about hard-coded wording is handled by the same rename.

@coderabbitai: reset stderr alongside stdout — already applied in fef6c5d; the second runWithDeps in TestRunProvidersListMarksUserAndRuntimeProfiles resets both buffers, and the new override test does the same.

Validation (Windows host, Go 1.26.5): go build ./..., go vet ./..., go test ./... -count=1 all pass; GOOS=linux|darwin|windows builds clean; gofmt clean on every changed file.

@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

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

@PierrunoYT: Thanks for the detailed rebase and resolution notes. I’ll review the updated, reduced diff with particular attention to provider-source semantics, environment-override UX, and the new test coverage.


✏️ Learnings added
Learnt from: PierrunoYT
URL: https://github.com/Gitlawb/zero/pull/725

Timestamp: 2026-07-25T11:35:15.664Z
Learning: In this Go CLI, provider profiles may come from user config, project config, or provider commands; profiles resolved at runtime are not necessarily transient. For provider list/current output, `selectable` indicates whether `providers use` can persist the profile, and the documented JSON `source` values are `user-config` and `resolved`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
internal/cli/provider_onboarding_test.go (1)

163-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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_PROVIDER names a provider that is not persisted but is resolvable via resolveCommandCenterConfig (e.g. set an ambient OPENAI_API_KEY and point the override at the env-derived provider name while switching to a different saved profile). That exercises the second branch of activeProviderEnvOverrideResolves (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 win

Resolver-fallback branch of activeProviderEnvOverrideResolves is 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 where ZERO_PROVIDER names an env-derived-but-unpersisted provider (e.g. an ambient OPENAI_API_KEY-derived profile) is not tested, leaving one of the two logical branches unverified.

See companion comment on internal/cli/provider_onboarding_test.go for 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

📥 Commits

Reviewing files that changed from the base of the PR and between fef6c5d and dcf1a40.

📒 Files selected for processing (4)
  • internal/cli/command_center.go
  • internal/cli/command_center_test.go
  • internal/cli/provider_onboarding.go
  • internal/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

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 25, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 in resolved.Providers, is not proof that ZERO_PROVIDER is the provider a subsequent invocation will use. For example, a saved OpenAI-compatible profile without a model passes the persisted fast path, so providers use fast --json reports envProviderResolves: true and effectiveProvider: "broken"; the next resolution instead fails because the active broken profile 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 resolved ActiveProvider to 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 collapses work and WORK. If user config has work and project config (or a provider command) contributes WORK, both resolved entries are labeled selectable: true, source: "user-config". providers use WORK only updates the user-config work row, so it cannot select the displayed WORK entry. 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between dcf1a40 and 9fd27b8.

📒 Files selected for processing (4)
  • internal/cli/command_center.go
  • internal/cli/command_center_test.go
  • internal/cli/provider_onboarding.go
  • internal/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

Comment thread internal/cli/command_center_test.go Outdated

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
    activeProviderEnvOverrideResolves now performs a full config resolution after providers use has already written config.json. When ZERO_PROVIDER_COMMAND is 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 unresolved ZERO_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 resolved WORK profile as distinct from persisted work, but ProviderPersisted and SetActiveProvider still match case-insensitively. Copying the displayed WORK into zero providers use WORK therefore exits successfully while silently activating the different persisted work profile. 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.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 26, 2026
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Final follow-up on current head dd353bc:

  • EditProvider now uses exact identity for target lookup and active-provider handoff
  • case-insensitive matching remains only for new-name collision detection
  • adversarial case-variant regression coverage was added
  • all review threads are resolved; full validation and all GitHub checks pass

The PR is mergeable. PierrunoYT requests fresh reviewer approval to supersede the previous CHANGES_REQUESTED state.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Merged current upstream/main (81a5e6c) normally into the branch in e1d1e42 (no rebase or force-push) and re-inspected every review/request thread.

The still-valid EditProvider identity request is covered on the updated branch:

  • target lookup compares trimmed names with exact, case-sensitive equality;
  • active-provider handoff also uses exact, case-sensitive identity;
  • case-insensitive matching remains only for new-name collision detection because credential-store names are normalized;
  • TestEditProviderRequiresExactProviderIdentityAmongCaseVariants puts WORK beside active work, verifies only WORK is edited/renamed, and verifies active work is unchanged;
  • the collision regression still rejects a rename to BETA when beta exists.

All four inline review threads are resolved; the latest CodeRabbit review approved the implementation.

Validation in the Linux orb with Go 1.26.5:

  • go test ./internal/config/... -count=1
  • make fmt-check
  • go vet ./...
  • go test ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke
  • make lint-static ✅ (0 issues)
  • make vulncheck ✅ (No vulnerabilities found)
  • git diff HEAD --check and PR diff check ✅

Skipped: none. Post-push GitHub checks are expected to rerun on e1d1e42.

@jatmn @Vasanthdev2004 — please re-review the current head when convenient so the stale CHANGES_REQUESTED decision can be superseded.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Post-push update for e1d1e42: all GitHub checks are now green — CodeRabbit, Performance Smoke, Security & code health, Smoke on Linux/macOS/Windows, and Zero Review. The only remaining gate is the stale human CHANGES_REQUESTED review decision; there are no unresolved review threads.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

The prior review round's rebase/supersession, unresolvable ZERO_PROVIDER reporting, resolver-backed override proof, provider-command deferral, list selectability case folding, and exact-identity edits for SetActiveProvider/ProviderPersisted/RemoveProvider/RenameProvider/SetProviderModel/EditProvider are all addressed on e1d1e42. The remaining problems are new providers use error routing introduced here, a regression in env-derived name matching, and an incomplete credential-identity migration that this branch makes user-visible.

Findings

  • [P2] Do not label every providers use not-found as a runtime-only profile
    internal/cli/provider_onboarding.go:74
    This PR adds a suffix to any SetActiveProvider error whose text contains provider and not found. On main, a typo such as providers use fastt when fast is saved simply returned provider "fastt" not found; here it also says to run providers setup or providers add. That guidance is wrong when the name does not exist in the resolved provider list at all. Only append the selectability hint when the name is absent from user config but present in the resolved list (the same condition reportUnpersistedProviderUse already uses), and leave plain unknown-name failures as a simple not-found error.

  • [P3] Wrong-case env-synthesized names regressed away from the #707 path
    internal/cli/provider_onboarding.go:595
    providerResolvedByName changed from case-insensitive to exact match in this PR. With only OPENAI_API_KEY set, providers use openai still reaches reportUnpersistedProviderUse (exit 0, persisted:false), but providers use OpenAI now skips that path and falls through to the misleading not-found wrapper above even though openai is resolvable for the session. Keep exact identity for persisted config.json rows, but restore case-insensitive matching for env/catalog-derived profile names before deciding whether a provider is resolvable-but-unpersisted.

  • [P2] Finish the exact-identity migration in the credential marker helpers
    internal/config/writer.go:137, internal/config/credentials.go:102
    The main config mutators now use exact, case-sensitive identity, and this branch adds regression tests for coexisting work/WORK rows, but MarkProviderAPIKeyStored and ClearProviderKeyStored still use EqualFold (they were already case-folding before this PR; the gap is now inconsistent and user-visible). After zero auth or key removal targets "WORK", the work row can get the apiKeyStored marker set or cleared instead. Apply the same exact-name rule here and add the same case-variant regression coverage used for the other writer mutators.

  • [P2] Case-variant config rows still share one credential-store entry
    internal/credstore/credstore.go:267, internal/cli/provider_onboarding.go:493
    Lowercasing store keys predates this PR, but this branch now treats distinct work and WORK config rows as a supported layout. providers remove WORK deletes only the WORK row yet removeStoredProviderKeyAt deletes the shared work key, leaving the surviving work profile without its stored secret. Either reject ambiguous case-variant rows up front or make stored-key read/write/delete use the same exact identity as the config writers.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

PierrunoYT addressed the remaining provider identity findings in d41bd16.

  • providers use now gives runtime-only guidance only for genuinely resolved, unpersisted providers; typos remain plain not-found.
  • Resolved selection is exact-first with a unique case-insensitive fallback and ambiguity errors.
  • API-key markers use exact persisted identity.
  • Persisted case-ambiguous configs are rejected before every reviewed CLI/TUI credential mutation.
  • OAuth browser/device flows revalidate immediately before saving; OpenRouter preserves a sole existing case-variant profile.
  • Added config, CLI, TUI, OAuth timing, and cross-layer resolution regressions.

Validation: affected config, CLI, and TUI tests pass with -race; formatting and diff checks pass. Ready for re-review.

@PierrunoYT
PierrunoYT requested a review from jatmn July 28, 2026 14:06

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

The prior review round's rebase/supersession work, unresolvable ZERO_PROVIDER reporting, resolver-backed override proof, provider-command deferral, list selectability case folding, exact-identity config mutators, credential-marker exact matching, and the providers use typo-vs-runtime-only routing fix from d41bd16 all look addressed on the current head. The items below are verified against d41bd163; they are new regressions or incomplete follow-through from that commit, not stale CI or pre-existing drift.

Findings

  • [P2] zero auth login / zero auth chatgpt now block OAuth for mixed-case persisted profile names
    internal/cli/auth.go:405, internal/cli/auth.go:424, internal/cli/auth.go:187
    d41bd16 adds both strings.ToLower on the login target and PreflightProviderWrite against that lowercased spelling. On main, login neither lowercased nor preflighted. A config with a sole legitimate case-variant row such as "OpenRouter" or "ChatGPT" (TestResolvePreservesSoleOpenRouterCaseVariant) now fails before any browser/device flow with provider "openrouter" already exists as "OpenRouter". TUI OAuth still only calls PreflightUserConfig, so the same on-disk config can complete OAuth in the wizard while the CLI path is blocked. Resolve the login target against the persisted row (or catalog id) before preflighting, or allow PreflightProviderWrite to accept a case-variant re-login of the same credential identity.

  • [P2] ZERO_PROVIDER override resolution false-negatives when env casing differs from the resolved profile name
    internal/cli/provider_onboarding.go:182
    activeProviderEnvOverrideResolution runs config.Resolve to prove the override works, but compares resolved.ActiveProvider to the raw env string with exact equality. Resolution selects the active row case-insensitively and returns the canonical persisted name (normalizeProvidersWithOptions in internal/config/resolver.go), so a valid override such as ZERO_PROVIDER=openrouter with a saved "OpenRouter" profile resolves successfully at runtime yet is reported as envProviderResolves: false, omits effectiveProvider, and prints the “cannot be resolved / Zero cannot start” note. Compare case-insensitively (or compare against the env-applied active name) and add a regression test for this mismatch.

  • [P2] providers use treats catalog-id addressing of a saved profile as an env-only provider
    internal/cli/provider_onboarding.go:595, internal/cli/provider_onboarding.go:628
    This PR adds an exact CatalogID match to providerResolvedByName's first loop; main only case-folded Name. A persisted user-config row such as {name:"my-router", catalogId:"openrouter"} appears in the resolved list, but zero providers use openrouter skips SetActiveProvider, exits 0, and prints the runtime-only/env guidance because ProviderPersisted("openrouter") is false while providerResolvedByName matches the saved row by catalog id. Only genuinely env-synthesized names should take the unpersisted path. Restrict the catalog-id branch to env-derived rows, or require that no persisted row already owns that catalog identity.

  • [P2] providers remove / providers rename did not get the same case-variant guard as providers use
    internal/cli/provider_onboarding.go:657, internal/cli/provider_onboarding.go:688
    reportUnpersistedProviderUse now checks ProviderPersistedCaseInsensitive and falls through to the real not-found path when a case-variant persisted row exists (provider_onboarding.go:617), but reportUnpersistedProviderRemove and reportUnpersistedProviderRename do not. zero providers remove SAVED / rename SAVED … against a saved "saved" row therefore exit 0 with the env-only message instead of the same plain not-found behavior providers use now has. Apply the same persisted-case guard on remove/rename.

Needs maintainer decision @Vasanthdev2004 @gnanam1990

  • Rejecting ambiguous case-duplicate user config is intentional, but there is no in-app repair path
    internal/config/resolver.go:77, internal/config/writer.go:336, internal/config/writer.go:859
    ValidatePersistedProviderNames failing Resolve() and every mutator — including RemoveProvider — for work/WORK duplicates is deliberate (TestRemoveProviderRequiresExactProviderIdentityAmongCaseVariants expects the mutation to be blocked). That is not a mistaken finding. The remaining problem is operational: the error tells users to “rename or remove one row,” but no CLI/TUI command can delete one duplicate while the file is still invalid, so pre-existing ambiguous configs require manual JSON surgery before Zero starts. Please either accept that as a one-time breaking migration, or add a repair path (for example, allow exact-name removal of one duplicate without requiring the file to already pass validation, or ship a one-shot migration/command) and adjust the error text to match whichever policy you choose.

Findings (lower priority)

  • [P3] Case-mismatch guidance is inconsistent across commands
    internal/cli/provider_onboarding.go:617, internal/cli/auth.go:481
    zero providers use SAVED against a persisted "saved" row correctly avoids the runtime-only suffix (per TestRunProvidersUseRejectsCaseVariantOfPersistedProvider) but still returns only provider "SAVED" not found, while zero auth logout SAVED explains that a differently cased row exists. If exact casing is required everywhere, consider returning the same capitalization hint on providers use and on the remove/rename case-variant paths above. Separately, auth logout could print the canonical persisted spelling (for example "saved") instead of only saying the exact name is required.

Four places drew the identity line in the wrong spot.

A login is not a new provider write. Preflighting it as one rejected the very
row it was logging into: a config whose sole ChatGPT profile is spelled
"ChatGPT" failed `zero auth chatgpt` before the browser flow with "provider
\"chatgpt\" already exists as \"ChatGPT\"", while the TUI completed the same
login. EnsureCatalogProvider reuses whatever row owns the identity, so
PreflightCatalogProviderLogin now checks that instead, and keeps the collision
check for the case where a row would actually be created.

The ZERO_PROVIDER override check ran the resolver to prove the override works,
then compared its result to the raw env string exactly. Resolution matches the
active row case-insensitively and reports the row's persisted spelling, so
ZERO_PROVIDER=openrouter against a saved "OpenRouter" resolved fine yet was
reported as unresolvable — the opposite of true.

`providers use <catalog-id>` against a row saved under a different name took the
runtime-only path and exited 0 with an environment-variable explanation that was
simply false for a provider the config owns. Every runtime-only report now asks
config first, and remove/rename get the same persisted-case guard use already
had rather than the env-only message on a case variant of a saved row.

Falling through to a bare "not found" leaves the user to guess what they got
wrong, so the provider mutators now name the saved profile — as `auth logout`
already did for capitalization, and one better by naming the spelling that works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Addressed all four P2s and the P3 in 7896ff3. The maintainer-decision item is untouched and still needs @Vasanthdev2004 / @gnanam1990 — see the end.

All four turned out to be the same mistake in different places: treating one provider identity as two because the spelling or the addressing differed.

[P2] auth login / auth chatgpt case-variant lockout. A login mints no new spelling — EnsureCatalogProvider reuses whatever row already owns the identity, matching on catalog id or folded name — so preflighting it as a new provider write rejected the row it was logging into. New config.PreflightCatalogProviderLogin asks that question instead: file valid, plus "does a row already own this identity?". It keeps the collision check for the case where a row would actually be created (unreachable today given EnsureCatalogProvider's matching, kept so a future narrowing fails fast before the browser flow rather than after it). This also closes the CLI-vs-TUI split you noted, in the direction the TUI already had.

[P2] ZERO_PROVIDER case-fold false negative. activeProviderEnvOverrideResolution now compares EqualFold. Worth noting the fold can't produce a false success: if the resolved active row folds to the override, the override is what selected it.

[P2] Catalog-id addressing of a saved row. Went with your second option — require that no persisted row owns the identity. New config.PersistedProviderIdentity answers "does the config own this, as a name under any capitalization or as a catalog id?", and all three runtime-only reports consult it first. A saved {name: "my-router", catalogId: "openrouter"} addressed as openrouter now falls through to the real not-found instead of exiting 0 with an environment-variable story that was false.

[P2] remove/rename guard. Same helper, same guard, now on all three commands rather than just use.

[P3] Case-mismatch guidance. providerMutationError decorates a not-found from SetActiveProvider/RemoveProvider/RenameProvider with the saved profile's actual name:

provider "SAVED" not found; the saved profile is named "saved" and provider names are matched exactly

That covers the catalog-id case too (… is named "my-router" …), so it names the spelling that works rather than only stating that exact casing is required. TestRunProvidersUseRejectsCaseVariantOfPersistedProvider still passes — it asserts Contains, and the plain not-found text is unchanged.

Regressions — four new tests, each verified to fail with the source changes stashed and the tests kept, reproducing exactly the symptoms you described:

  • TestRunAuthChatGPTAllowsCaseVariantPersistedProfile — fails with provider "chatgpt" already exists as "ChatGPT"; also asserts the ambiguous-config guard is unchanged (two case-duplicate rows still refuse the login).
  • TestRunProvidersUseResolvesCaseVariantEnvOverride — fails with envProviderResolves = false; checks both JSON and the text note.
  • TestRunProvidersUseRejectsCatalogIDOfSavedProfile — fails with exit 0 and the env-derived message.
  • TestRunProvidersRemoveRenameRejectCaseVariantOfPersistedProvider — both subtests fail with exit 0 and the env-only message; also asserts the config is not mutated.

Not addressed — maintainer decision. I deliberately left the case-duplicate repair path alone: whether pre-existing ambiguous configs get a one-time breaking migration or an in-app repair command is a product call, and the error text should follow whichever is chosen rather than the other way round. Happy to implement either once @Vasanthdev2004 / @gnanam1990 decide. Nothing in this commit changes that behavior.

Validation

  • go build ./..., go vet ./..., go test ./...
  • go test ./internal/cli ./internal/config -count=1
  • Each new test confirmed failing against d41bd16's source
  • git diff --check clean; gofmt -l clean for every touched file

@PierrunoYT
PierrunoYT requested a review from jatmn July 28, 2026 20:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/cli/auth.go (1)

465-485: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Logout's capitalization-mismatch error doesn't tell the user what to type.

When a case-variant is detected, the error only echoes the user's own input back at them — it never surfaces the actual persisted spelling, so the user has no easy way to discover the exact string required. A one-line addition pointing them at providers list (or embedding the actual persisted name, if cheaply available) would make this actionable rather than just a dead end.

🛠️ Proposed fix
 		if folded {
-			return writeAppError(stderr, fmt.Sprintf("provider %q exists with different capitalization; logout requires the exact persisted provider name", provider), exitCrash)
+			return writeAppError(stderr, fmt.Sprintf("provider %q exists with different capitalization; logout requires the exact persisted provider name (run \"zero providers list\" to see it)", provider), exitCrash)
 		}
🤖 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/auth.go` around lines 465 - 485, Update the case-insensitive
provider mismatch error in the logout flow around
ProviderPersistedCaseInsensitive to tell users how to discover the exact
persisted spelling, preferably by directing them to run “providers list”; retain
the existing exact-name requirement and error behavior.
🤖 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/auth.go`:
- Around line 465-485: Update the case-insensitive provider mismatch error in
the logout flow around ProviderPersistedCaseInsensitive to tell users how to
discover the exact persisted spelling, preferably by directing them to run
“providers list”; retain the existing exact-name requirement and error behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6658f0cb-5c9d-4cbb-a41c-d2fd219fa4d6

📥 Commits

Reviewing files that changed from the base of the PR and between dd353bc and 7896ff3.

📒 Files selected for processing (19)
  • internal/cli/app.go
  • internal/cli/auth.go
  • internal/cli/auth_test.go
  • internal/cli/provider_onboarding.go
  • internal/cli/provider_onboarding_test.go
  • internal/cli/provider_setup.go
  • internal/cli/setup.go
  • internal/config/credentials.go
  • internal/config/credentials_test.go
  • internal/config/resolver.go
  • internal/config/resolver_test.go
  • internal/config/writer.go
  • internal/config/writer_test.go
  • internal/oauth/manager.go
  • internal/tui/oauth_device.go
  • internal/tui/onboarding.go
  • internal/tui/provider_manager.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_discovery.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/config/writer_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

The prior review round's rebase/supersession work, unresolvable ZERO_PROVIDER reporting, resolver-backed override proof, provider-command deferral, list selectability case folding, exact-identity config mutators, credential-marker exact matching, providers use typo-vs-runtime-only routing, catalog-id addressing of saved profiles, case-variant env-override resolution, and PreflightCatalogProviderLogin for CLI OAuth login all look addressed on 7896ff32fa7f16044d9c22bb9a40b9f5a418e6c6. The items below were re-verified against that head (not drift from an earlier SHA or merge-base mismatch).

Findings

  • [P2] zero auth logout blocks the logout path the TUI documents for case-variant profile names
    internal/cli/auth.go:472
    Login now accepts catalog-id / case-variant addressing via PreflightCatalogProviderLogin and stores OAuth tokens under the catalog id (e.g. chatgpt). oauthLoginName reports that stored key back to users (zero auth logout chatgpt in provider_manager.go), but runAuthLogout still hard-stops when ProviderPersistedCaseInsensitive matches and the exact name does not (internal/cli/auth.go:476-482), so manager.Logout, ForgetProviderKey, and ClearProviderKeyStored never run.

    Repro: config with a sole ChatGPT row saved as "ChatGPT" (any setup that persisted a capitalized name). zero auth chatgpt succeeds; zero auth logout chatgpt (the string the TUI emits) fails with the capitalization error and leaves the OAuth token and any stored API key in place. zero auth logout ChatGPT would pass the guard but is not what the UI documents when the token lives under the catalog id.

    Please resolve the logout target the same way login does — PersistedProviderIdentity and/or the stored OAuth key — instead of treating a catalog-id or case-variant invocation as a hard error.

  • [P2] zero auth openrouter still runs the browser flow before config preflight
    internal/cli/auth.go:102
    runAuthChatGPT and runAuthLogin call PreflightCatalogProviderLogin before opening the browser. The TUI OpenRouter wizard at least runs PreflightUserConfig via preflightOAuthUserConfig before OpenRouterLogin (internal/tui/provider_wizard.go:164). runAuthOpenRouter is the outlier: it calls deps.openRouterLogin first and only reaches PreflightUserConfig inside saveOpenRouterProviderKey after the key is minted (internal/cli/auth.go:134).

    Repro: invalid or ambiguous config.json (anything PreflightUserConfig rejects). The user completes the browser/PKCE flow, then save fails. Worse, a save failure still returns exit 0 with a manual export OPENROUTER_API_KEY=... fallback (internal/cli/auth.go:112-117), so the command can look successful while nothing was persisted. Preflight before starting PKCE, matching the other auth entry points, and consider a non-zero exit when persistence fails.

  • [P2] Setup and wizard finalize still use write-time collision checks for catalog re-login
    internal/tui/provider_wizard.go:1298, internal/cli/setup.go:267, internal/tui/onboarding.go:876
    CLI catalog OAuth reuses existing rows through EnsureCatalogProvider / PreflightCatalogProviderLogin, but finalize paths still call PreflightProviderWrite on the catalog-derived profile name (typically lowercased, e.g. openrouter from providerWizardDisplayName / setupProfileName). When a legitimately saved row already owns that identity under different casing (e.g. "OpenRouter"), finalize fails with provider "openrouter" already exists as "OpenRouter" after a successful OpenRouter OAuth/key capture, while zero auth openrouter succeeds via EnsureCatalogProvider.

    This is narrower than the logout gap (re-setup or wizard re-finalize with a case-variant row already on disk) but it is the same partial migration: identity-aware preflight on login, collision preflight on persist. Use PreflightCatalogProviderLogin / EnsureCatalogProvider (or derive the persisted profile name before writing) on these finalize paths.

Findings (lower priority)

  • [P3] ZERO_PROVIDER override warnings fire when only the spelling differs
    internal/cli/provider_onboarding.go:155
    This PR correctly fixed resolution (activeProviderEnvOverrideResolution uses EqualFold; see TestRunProvidersUseResolvesCaseVariantEnvOverride), but the warning trigger still compares raw strings in activeProviderEnvOverride (TestActiveProviderEnvOverrideTreatsCaseVariantAsDistinct locks that in). zero providers use work with ZERO_PROVIDER=WORK therefore prints an override note and JSON override fields even though resolution proves the env value selects the same saved row. Runtime behavior is correct; the warning is misleading. Compare normalized identity, or suppress the note when resolution proves the override names the same provider.

  • [P3] auth logout case-mismatch errors do not name the persisted spelling
    internal/cli/auth.go:482
    If exact casing remains required on logout, the error should name the saved profile the way providerMutationError now does for providers use / remove / rename (internal/cli/provider_onboarding.go:622). Today it only says capitalization differs. Include the canonical persisted name (via PersistedProviderIdentity) or direct the user to providers list. This becomes moot if the P2 logout fix resolves identity instead of rejecting.

Needs maintainer decision @Vasanthdev2004 @gnanam1990

  • Rejecting ambiguous case-duplicate user config is intentional, but there is still no in-app repair path
    internal/config/resolver.go:77, internal/config/writer.go:396
    ValidatePersistedProviderNames correctly blocks work/WORK duplicates at resolution time, and every mutator including RemoveProvider validates before acting, so the error text "rename or remove one row" cannot be followed through shipped commands. Pre-existing ambiguous configs require manual JSON surgery before Zero starts. Please either accept that as a one-time breaking migration, or add a repair path (for example, allow exact-name removal of one duplicate without requiring the file to already pass validation) and align the error text with whichever policy you choose.

Making login identity-aware left the surrounding paths on write-time collision
checks, so one provider identity was still two in three places.

Logout hard-stopped whenever a persisted row matched case-insensitively without
matching exactly. Login accepts a catalog id and stores its token under that
key, and the TUI tells users to run `zero auth logout <catalog id>` — so the
documented command failed and left the OAuth token and any stored API key in
place. Logout now resolves the target the way login does: credential keys stay
on the spelling the user typed, where login put them, while the config mutation
uses the persisted row's name. Both spellings are cleared from the key store
when they differ, since a setup-captured key and a login-captured one live under
different names.

`zero auth openrouter` ran the browser flow before checking the config, so a
user could complete a PKCE round trip only to have the save fail — and it then
exited 0, reporting success for a command that persisted nothing. It now
preflights first like every other auth entry point, and a failed save is a
failure. The minted key is still printed, because it is real and the user paid
a browser round trip for it.

Setup and the wizard collided with the row that catalog login reuses. When a
caller takes the catalog's own default name, it now yields to the row already
holding that catalog identity. A user-chosen name is deliberately left alone:
there a case collision is a real collision, and silently overwriting the other
row would be worse than the error.

Also fold case in the ZERO_PROVIDER override warning. Resolution already selects
the active row case-insensitively, so ZERO_PROVIDER=WORK against a saved "work"
lands on exactly the row the user just selected; warning that the switch stays
overridden described a conflict that does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

All three P2s and both P3s addressed in 54c1d2e. You framed this exactly right — I made login identity-aware and left everything around it on write-time collision checks, so the migration was half-done.

[P2] auth logout resolves identity instead of rejecting. The guard now goes: credential keys stay on the spelling the user typed (that is where login put them), and only the config mutation uses the persisted row's name, because those mutators match a row exactly. zero auth logout chatgpt against a "ChatGPT" row now clears the token and the stored key.

One thing that fell out while writing it: the key store is asked for both spellings when they differ. A key captured by provider setup lives under the persisted row's name while one captured by a catalog login lives under the catalog id, so clearing only what the user typed would have left the other behind.

[P3, folded in] auth logout capitalization error — moot, as you predicted. There is no rejection path left to name a spelling in.

[P2] auth openrouter preflights before the browser flow. Moved to PreflightCatalogProviderLogin before openRouterLogin, matching the other entry points.

I also took the "consider" on the exit code, because I think the current behavior is worse than it looks: a failed save returned 0, so a script would carry on believing the provider was configured. It now exits non-zero. The minted key is still printed — it is real and the user paid a browser round trip for it — but printing a key is not the same as persisting one, and only the exit code says which happened.

[P2] Setup and wizard finalize adopt the persisted row. New config.AdoptPersistedCatalogProviderName, applied at all three finalize paths (provider_setup.go, setup.go, provider_wizard.go). It is deliberately narrow: it only retargets when the caller took the catalog's own default name (profile name == catalog id), which is where the two spellings are the same provider by construction. A user-chosen name is left alone — there a case collision with an existing row is a genuine collision, and silently overwriting someone's other profile would be worse than the error they get today. TestProviderSetupAdoptsPersistedCatalogRowCasing asserts both halves.

internal/tui/onboarding.go:876 needed no change: it routes through setupSavesetup.go, which is one of the three.

[P3] ZERO_PROVIDER override warning folds case. activeProviderEnvOverride now compares with EqualFold. I replaced TestActiveProviderEnvOverrideTreatsCaseVariantAsDistinct rather than editing it — it locked in the behavior you flagged as misleading, so keeping the name would have been confusing. The replacement asserts both directions: a case variant of the selection is not an override, a genuinely different provider still is.

Not addressed, still yours: the case-duplicate repair path. Unchanged from last round — whether that is a one-time breaking migration or an in-app repair command is a product call, and the error text should follow the decision.

Tests: four new, plus one replaced. TestRunAuthLogoutResolvesCatalogIdentity and TestRunAuthOpenRouterPreflightsBeforeTheBrowserFlow were both verified to fail against the previous implementation.

Validation: go build ./..., go vet ./..., go test ./..., git diff --check and gofmt -l clean for every touched file.

@PierrunoYT
PierrunoYT requested a review from jatmn July 29, 2026 12:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/auth_test.go (1)

229-245: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the rejected ChatGPT attempt neither logs in nor overwrites the token.

This branch only checks stderr. Track callback invocation and verify the existing fresh token remains stored after rejection.

Proposed test assertions
 	stdout.Reset()
 	stderr.Reset()
+	loginCalled := false
 	code = runWithDeps([]string{"auth", "chatgpt"}, &stdout, &stderr, appDeps{
 		userConfigPath: func() (string, error) { return configPath, nil },
 		chatGPTLogin: func(context.Context, provideroauth.ChatGPTOptions) (oauth.Token, error) {
+			loginCalled = true
 			return oauth.Token{AccessToken: "should-not-save"}, nil
 		},
 	})
 	if code == exitSuccess || !strings.Contains(stderr.String(), "ambiguous persisted provider names") {
 		t.Fatalf("exit = %d, stderr = %q; want the ambiguous-config failure preserved", code, stderr.String())
 	}
+	if loginCalled {
+		t.Fatal("login ran despite ambiguous persisted provider names")
+	}
+	token, ok, err = store.Load(oauth.ProviderKey("chatgpt"))
+	if err != nil || !ok || token.AccessToken != "fresh" {
+		t.Fatalf("stored token = %+v, %v, %v; want fresh token unchanged", token, ok, err)
+	}

As per coding guidelines, “**/*_test.go: … add a regression test for behavior changes.”

🤖 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/auth_test.go` around lines 229 - 245, The ambiguous-provider
rejection test around runWithDeps must verify that the rejected ChatGPT request
neither invokes chatGPTLogin nor overwrites the existing fresh token. Track
callback invocation, seed the configuration with a fresh token, then assert the
callback was not called and the persisted token remains unchanged after the
command fails.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@internal/cli/auth_test.go`:
- Around line 229-245: The ambiguous-provider rejection test around runWithDeps
must verify that the rejected ChatGPT request neither invokes chatGPTLogin nor
overwrites the existing fresh token. Track callback invocation, seed the
configuration with a fresh token, then assert the callback was not called and
the persisted token remains unchanged after the command fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e51aa94-a48d-4675-8885-9737fc02816c

📥 Commits

Reviewing files that changed from the base of the PR and between 7896ff3 and 54c1d2e.

📒 Files selected for processing (8)
  • internal/cli/auth.go
  • internal/cli/auth_test.go
  • internal/cli/provider_onboarding.go
  • internal/cli/provider_onboarding_test.go
  • internal/cli/provider_setup.go
  • internal/cli/setup.go
  • internal/config/writer.go
  • internal/tui/provider_wizard.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/cli/auth.go
  • internal/config/writer.go
  • internal/cli/provider_onboarding.go
  • internal/tui/provider_wizard.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 29, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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. Please dismiss or supersede that review after the items below are fixed.

The prior review items on exact mutators, runtime-only routing, env-override resolution, auth login/logout/openrouter identity, and setup/wizard catalog adoption look addressed on 54c1d2e. What remains is one verified regression from the exact-identity migration and two policy gaps this branch introduces alongside the new ambiguous-config validation.

Findings

  • [P2] Keep activeProvider in sync when the removed or renamed row is the active profile
    internal/config/writer.go:440, internal/config/writer.go:512, internal/config/writer.go:607
    This is a real regression introduced here, not drift from main. RemoveProvider, RenameProvider, and EditProvider now update activeProvider only on case-sensitive equality with the mutated row, but main used EqualFold for that hand-off and resolution still selects the active row case-insensitively. You do not need ambiguous work/WORK duplicates to hit this: a single saved "work" row with project config setting activeProvider: "WORK" is enough. Resolve() works, zero providers remove work deletes the profile, and activeProvider stays "WORK" pointing at nothing. Please clear or repoint activeProvider whenever the mutated row is the active profile under case-insensitive identity, and add regression coverage for remove/rename/edit with mismatched activeProvider casing.

Findings (lower priority)

  • [P3] Decide whether zero auth logout should clear credentials when config.json fails validation
    internal/cli/auth.go:483
    PreflightUserConfig on logout is also new in this branch (d41bd16); main did not gate logout on config validity. With the new ValidatePersistedProviderNames checks, an ambiguous work/WORK file now blocks logout before manager.Logout or ForgetProviderKey run, so users cannot clear OAuth/API keys and then hand-edit JSON the way they could before. That is a deliberate extension of “reject bad config everywhere” unless you want logout to stay a recovery escape hatch. If recovery matters, run OAuth and credential-store cleanup without requiring the file to pass validation, and only gate the ClearProviderKeyStored write on a readable config; if not, say so in the error text and fold this into the maintainer decision below.

Needs maintainer decision @Vasanthdev2004 @gnanam1990

  • Rejecting ambiguous case-duplicate user config is intentional, but there is still no in-app repair path
    internal/config/resolver.go:77, internal/config/writer.go:396
    ValidatePersistedProviderNames is new in this pull request. Blocking work/WORK duplicates at Resolve() and in every mutator is deliberate, and the error text tells users to rename or remove one row. No CLI/TUI command can delete one duplicate while the file is still invalid, so pre-existing ambiguous configs require manual JSON surgery before Zero starts. Please either accept that as a one-time breaking migration, or add a repair path (for example, allow exact-name removal of one duplicate without requiring the file to already pass validation) and align the error text with whichever policy you choose.

  • Whether credential cleanup should work on an otherwise-invalid config
    internal/cli/auth.go:483
    This overlaps the P3 item above. If the answer to the repair-path question is “manual JSON only,” please confirm that blocking zero auth logout on invalid config is intentional too, so users are not told to fix config while every shipped command that could help is refused.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

PierrunoYT addressed the remaining provider-identity and recovery findings in a6bbf74.

  • Active-provider handoff now preserves exact-row precedence while still following a sole case-variant active name during remove/rename/edit.
  • Exact removal can repair an ambiguous case-duplicate config, but only when the resulting config validates; non-exact and still-ambiguous repairs remain rejected.
  • Removing one case variant preserves the surviving profile’s shared credential in both CLI and TUI flows.
  • zero auth logout now clears OAuth/API credentials even when invalid config blocks the marker update, and returns the config error truthfully.
  • Added regressions for handoff, safe repair, shared-key retention, and invalid-config credential cleanup.

Validation passed: make fmt-check, focused race tests (internal/config, internal/cli, internal/tui), go vet ./..., go test ./..., release build, smoke, make lint-static (0 issues), make vulncheck (no vulnerabilities), and git diff HEAD --check.

@jatmn @Vasanthdev2004 — PierrunoYT requests a fresh review of the current head. The account does not have permission to use GitHub’s reviewer-request button.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/auth.go (1)

483-527: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Recovery logout leaves API keys under alternate case-variant spellings. An ambiguous persisted config prevents canonical identity resolution, so cleanup only deletes the typed spelling even though provider-setup keys may be stored under a persisted spelling.

  • internal/cli/auth.go#L483-L527: for an ambiguity error, enumerate and deduplicate all case-insensitive persisted provider names, delete each corresponding key, then return the preflight error.
  • internal/cli/auth_test.go#L328-L350: seed API keys under alternate persisted spellings such as Demo and DEMO, and assert recovery logout removes them.
🤖 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/auth.go` around lines 483 - 527, Update the logout cleanup in
internal/cli/auth.go lines 483-527 to handle ambiguous PersistedProviderIdentity
errors by enumerating all persisted provider names matching provider
case-insensitively, deduplicating them, deleting each corresponding key, and
then returning the original preflight error; preserve existing cleanup for
unambiguous identities. Extend the recovery logout test in
internal/cli/auth_test.go lines 328-350 to seed keys under alternate spellings
such as Demo and DEMO and assert both are removed.
🤖 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.

Outside diff comments:
In `@internal/cli/auth.go`:
- Around line 483-527: Update the logout cleanup in internal/cli/auth.go lines
483-527 to handle ambiguous PersistedProviderIdentity errors by enumerating all
persisted provider names matching provider case-insensitively, deduplicating
them, deleting each corresponding key, and then returning the original preflight
error; preserve existing cleanup for unambiguous identities. Extend the recovery
logout test in internal/cli/auth_test.go lines 328-350 to seed keys under
alternate spellings such as Demo and DEMO and assert both are removed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a74457e6-0225-4d1a-8e42-eda4a774a66a

📥 Commits

Reviewing files that changed from the base of the PR and between 54c1d2e and a6bbf74.

📒 Files selected for processing (7)
  • internal/cli/auth.go
  • internal/cli/auth_test.go
  • internal/cli/provider_onboarding.go
  • internal/cli/provider_onboarding_test.go
  • internal/config/writer.go
  • internal/config/writer_test.go
  • internal/tui/provider_manager.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/cli/provider_onboarding_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

providers use reports success when ZERO_PROVIDER keeps another provider active

6 participants