From a727e2084399fc97504dd38706b2ec149f48aac5 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 24 Jul 2026 22:38:15 -0700 Subject: [PATCH] fix(auth): read the Anthropic credential preference from the resolved global dir Global settings live in settings.json under the resolved global dir, which falls back to the pre-rename ~/.pi/fusion and ~/.pi/kb dirs for installs that never migrated. The reader hardcoded ~/.fusion, so on those installs the operator's preference was silently ignored and resolution fell back to raw-key precedence -- the same silent fallback the preference exists to remove. Mirror the legacy-aware lookup getModelRegistryModelsPath already does for models.json rather than importing core's resolver, which throws under VITEST when called without an explicit dir. Co-Authored-By: Claude Opus 5 (1M context) --- .../auth-storage-anthropic-preference.test.ts | 21 +++++++++++++++++++ packages/engine/src/auth-storage.ts | 21 +++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/__tests__/auth-storage-anthropic-preference.test.ts b/packages/engine/src/__tests__/auth-storage-anthropic-preference.test.ts index 18954b3ed8..d431735583 100644 --- a/packages/engine/src/__tests__/auth-storage-anthropic-preference.test.ts +++ b/packages/engine/src/__tests__/auth-storage-anthropic-preference.test.ts @@ -131,6 +131,27 @@ describe("Anthropic credential precedence (anthropicAuthPreference)", () => { await expect(storage.getApiKey("anthropic")).resolves.toBe(SUBSCRIPTION_ACCESS_TOKEN); }); + it("honors the preference stored in a legacy global settings dir", async () => { + writeAuth(homeDir, { + anthropic: { type: "api_key", key: RAW_API_KEY }, + "anthropic-subscription": liveSubscriptionCredential(), + }); + /* + Operators who never migrated off the pre-rename global dir keep settings.json under + ~/.pi/fusion. Reading only ~/.fusion would silently ignore their choice and fall back to + raw-key precedence — the exact silent fallback this preference exists to remove. + */ + mkdirSync(join(homeDir, ".pi", "fusion"), { recursive: true }); + writeFileSync( + join(homeDir, ".pi", "fusion", "settings.json"), + JSON.stringify({ anthropicAuthPreference: "subscription" }), + ); + + const storage = createFusionAuthStorage(); + + await expect(storage.getApiKey("anthropic")).resolves.toBe(SUBSCRIPTION_ACCESS_TOKEN); + }); + it("falls back to the historical precedence when the settings file is unreadable", async () => { writeAuth(homeDir, { anthropic: { type: "api_key", key: RAW_API_KEY }, diff --git a/packages/engine/src/auth-storage.ts b/packages/engine/src/auth-storage.ts index bd48bdf00f..44b89cf0f0 100644 --- a/packages/engine/src/auth-storage.ts +++ b/packages/engine/src/auth-storage.ts @@ -421,9 +421,26 @@ function resolveStoredCredentialApiKey(providerId: string, credential: StoredCre * can return them as a fallback when neither Fusion auth nor legacy auth.json * contains a key for the provider. */ -/** Global settings file that carries the operator's Anthropic credential preference. */ +/* +FNXC:ProviderAuth 2026-07-24-18:20: +Global settings live in `settings.json` under the resolved GLOBAL dir — not in Postgres +(Postgres holds project settings, which are merged over the global ones by +`settings-ops.ts`, and the immutable configuration-revision journal). The global dir is +`~/.fusion` for current installs but falls back to the pre-rename `~/.pi/fusion` and +`~/.pi/kb` dirs for operators who never migrated (see core `resolveGlobalDirForHome`). +Hardcoding `~/.fusion` would silently ignore the operator's preference on those installs and +fall back to raw-key precedence — the same silent-fallback failure this preference exists to +remove. Mirror the legacy-aware lookup `getModelRegistryModelsPath` already does for +models.json rather than importing core's resolver, which throws under VITEST when called +without an explicit dir. +*/ export function getFusionGlobalSettingsPath(home = getHomeDir()): string { - return join(home, ".fusion", "settings.json"); + const candidates = [ + join(home, ".fusion", "settings.json"), + join(home, ".pi", "fusion", "settings.json"), + join(home, ".pi", "kb", "settings.json"), + ]; + return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0]; } /*