Clarify saved and effective provider selection - #725
Conversation
|
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. |
|
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.
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] Keep every provider mutator on the exact-name identity rule
internal/config/writer.go:234
This change makesProviderPersistedandproviders usecase-sensitive, and the resolver/list now intentionally exposeworkandWORKas distinct profiles. However,RemoveProvider(andRenameProvider/SetProviderModel) still selects the firstEqualFoldmatch. With saved profiles ordered aswork,WORK,zero providers remove WORKfirst passes the new exact persisted check, then deleteswork; the CLI subsequently deletes the case-normalized stored credential too, leaving the requestedWORKrow behind without its key. Use the same exact identity for every mutator, or reject case-distinct profiles globally, and cover the remove/rename paths.
UpsertProvider already merges by exact name, so config.json can hold two rows differing only by case (e.g. "work" saved once, "WORK" saved later). SetActiveProvider/ProviderPersisted were already switched to exact-name matching, but RemoveProvider, RenameProvider, and SetProviderModel still picked the first case-insensitive match: with rows ordered [work, WORK], `providers remove WORK` deleted "work" instead, and the ActiveProvider hand-off/follow logic in Remove/RenameProvider had the same case-folding bug when checking whether the mutated row was the active one. Switch all three to the same exact-identity rule (RenameProvider's newName collision check stays case-insensitive, since the credential store normalizes names and a case-variant rename would silently share/corrupt another row's stored key). Added regression tests for all three functions covering the case-distinct-duplicate scenario, and fixed two existing tests that relied on the old case-folding convenience. Addresses review feedback from jatmn on PR Gitlawb#725. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Split into 4 focused PRs per the review feedback above, in dependency order — each stacked on and reviewable against its predecessor:
This PR stays open as the tracking/reference point until all four land. |
|
closing this pr now, new pr's have been updated accordingly. |
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expose exact user-config selectability in provider list/current output, explain ZERO_PROVIDER resolution outcomes without executing provider commands, and keep case-only TUI edits synchronized only with the exact live profile row. PR 4 of the provider identity split from Gitlawb#725. Amp-Thread-ID: https://ampcode.com/threads/T-019ff5b2-d268-76f7-abe8-36f318aced49 Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expose exact user-config selectability in provider list/current output, explain ZERO_PROVIDER resolution outcomes without executing provider commands, and keep case-only TUI edits synchronized only with the exact live profile row. PR 4 of the provider identity split from Gitlawb#725. Amp-Thread-ID: https://ampcode.com/threads/T-019ff5b2-d268-76f7-abe8-36f318aced49 Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
…alidation Persisted provider rows and credential-store entries answer two different questions, and mixing them let one profile's mutation reach another's row and secret. This introduces the single identity rule and splits the two: - credstore.NormalizeProvider is now exported as the store's own provider-name equivalence rule (trim + ToLower). Callers deciding whether two spellings share one stored secret must use it rather than strings.EqualFold: Unicode case folding equates "s" and "ſ" while strings.ToLower does not, so an EqualFold comparison can promise a survivor access to a key it can never look up. - config.ValidatePersistedProviderNames rejects persisted rows that repeat a folded identity, whether the spellings are identical or only case variants; writeConfigFile guards every write with it, and Resolve() validates user config before merging. - config.SameProviderIdentity / sameProviderIdentity expose that rule to config mutators and future UI/CLI callers. Operations that address a persisted ROW now match its exact spelling: MarkProviderAPIKeyStored, SetActiveProvider, ProviderPersisted, SetProviderModel, ClearProviderKeyStored, RemoveProvider's index lookup, and the oldName lookups in RenameProvider/EditProvider. Operations that reason about a shared CREDENTIAL use identity: new-name collision checks, active-provider handoff, migrateStoredProviderKey's case-only-rename early return, and the new ClearProviderKeyStoredCaseVariants. normalizeProvidersWithOptions selects the active row before normalizing anything: an exact name always wins, credential identity is a fallback only when it identifies exactly one row, and an ambiguous fallback is an error instead of an arbitrary pick. PreflightUserConfig, PreflightProviderWrite, PersistedProviderNames and ClearProviderKeyStoredCaseVariants have no callers yet; the follow-up PRs in this split wire them into the CLI and TUI. This is PR1 of a 4-PR split of Gitlawb#725, addressing review feedback that the combined branch was too large to review. Refs Gitlawb#721. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expose exact user-config selectability in provider list/current output, explain ZERO_PROVIDER resolution outcomes without executing provider commands, and keep case-only TUI edits synchronized only with the exact live profile row. PR 4 of the provider identity split from Gitlawb#725. Amp-Thread-ID: https://ampcode.com/threads/T-019ff5b2-d268-76f7-abe8-36f318aced49 Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
Build on PR1's provider-identity primitives (credstore.NormalizeProvider,
config.SameProviderIdentity, PreflightUserConfig/PreflightProviderWrite,
ClearProviderKeyStoredCaseVariants) with positive catalog ownership,
ambiguous catalog-id rejection, and one shared read-only resolver for
credential-store candidates.
Positive ownership: a persisted row owns a catalog provider only when its
non-empty catalogId matches the requested descriptor. A matching display
name is not ownership — a custom profile may legitimately be called
"OpenRouter" while pointing at an unrelated endpoint — so
EnsureCatalogProvider, the OAuth login preflight, the provider wizard's
stored-key lookup, and the aimlapi discovery path now all require the
catalogId to prove it. Reusing a name-only row would have handed a foreign
profile to a catalog write that overwrites its endpoint, model, and
transport while preserving its stored-key marker.
Ambiguous catalog ids are refused rather than guessed at. Catalog ids are
shared by design ({name:"work-xai"} and {name:"personal-xai"} both carrying
catalogId "xai"), so a catalog-addressed login, status, refresh, or logout
that cannot name one row now errors instead of picking the file-order
winner. Identity resolution also prefers names over catalog ids and an
exact name over a case variant, so `auth logout xai` no longer retargets an
earlier {name:"work-xai", catalogId:"xai"} row.
ProviderCredentialCandidates is the one read-only resolver: it returns the
requested spelling, the canonical persisted name, and the catalog id only
when no sibling row can own credentials under it. OAuth status, refresh
(including --watch), logout, and the wizard's API-key removal are migrated
onto it together, so each command addresses the same stored login. Logout
expands over both the OAuth token store and the API-key store, clears
markers via ClearProviderKeyStoredCaseVariants, and still deletes
credentials when an unrelated part of config.json is ambiguous — reporting
the marker-write failure truthfully instead of exiting 0.
Interactive logins gained a BeforeSave hook (oauth.ManagerOptions.BeforeSave,
threaded through newAuthManager and the TUI OAuth/device commands) so the
config is revalidated immediately before token save, closing the window
where the file changes while a browser or device flow is pending.
`zero auth openrouter` now preflights before the browser flow and exits
non-zero when the minted key cannot be saved (still printing the key).
PR2 of a 4-PR split of Gitlawb#725 (refs Gitlawb#721). Locking, CommitProviderProfile,
marker transfer, and provider-selection presentation are deliberately left
to PR3/PR4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
Expose exact user-config selectability in provider list/current output, explain ZERO_PROVIDER resolution outcomes without executing provider commands, and keep case-only TUI edits synchronized only with the exact live profile row. PR 4 of the provider identity split from Gitlawb#725. Amp-Thread-ID: https://ampcode.com/threads/T-019ff5b2-d268-76f7-abe8-36f318aced49 Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
Expose exact user-config selectability in provider list/current output, explain ZERO_PROVIDER resolution outcomes without executing provider commands, and keep case-only TUI edits synchronized only with the exact live profile row. PR 4 of the provider identity split from Gitlawb#725. Amp-Thread-ID: https://ampcode.com/threads/T-019ff5b2-d268-76f7-abe8-36f318aced49 Co-authored-by: Pierre Bruno <pierrebruno@hotmail.ch>
Summary
ZERO_PROVIDERoverrides a provider saved byproviders use, including resolved, deferred, unresolvable, and unrelated config-error outcomescatalogIdownership for catalog setup/login/persistence and reject ambiguous shared catalog IDs instead of adopting by name or file orderZERO_PROVIDERsynchronized for case-only renames without retargeting case-variant project providersValidation
make fmt-checkgo vet ./...go test ./...go run ./cmd/zero-release buildgo run ./cmd/zero-release smokemake lint-static(0 issues)make vulncheck(no vulnerabilities found)git diff HEAD --checkFixes #721