feat(FN-3434): add Claude OAuth credential interop with verification and st

This merge lands four major features: the desktop app gains shell onboarding with remote mode support via a new `DesktopModeChooser` and `shell-settings` module (FN-3399); the dashboard gains full archived insights support with the `InsightsView` redesign and `useInsights` hook overhaul (FN-3315); C

Fusion-Task-Id: FN-3434
This commit is contained in:
Fusion
2026-05-04 23:23:50 -07:00
committed by gsxdsm
parent 20fc4f8420
commit d91780d171
18 changed files with 179 additions and 28 deletions

View File

@@ -148,8 +148,8 @@ describe("wrapAuthStorageWithApiKeyProviders", () => {
expect(await storage.getApiKey("openai-codex")).toBe("legacy-access-token");
});
describe("Anthropic reclassification from OAuth to API key", () => {
it("filters anthropic out of getOAuthProviders even when upstream reports it as OAuth", () => {
describe("Anthropic provider classification", () => {
it("keeps anthropic in getOAuthProviders when upstream reports it as OAuth", () => {
const fusionAuth = makeAuthStorage();
fusionAuth.getOAuthProviders = vi.fn(() => [
{ id: "anthropic", name: "Anthropic" },
@@ -161,11 +161,11 @@ describe("wrapAuthStorageWithApiKeyProviders", () => {
const oauthProviders = wrapped.getOAuthProviders();
const oauthIds = oauthProviders.map((p) => p.id);
expect(oauthIds).not.toContain("anthropic");
expect(oauthIds).toContain("anthropic");
expect(oauthIds).toContain("github-copilot");
});
it("includes anthropic in getApiKeyProviders with correct display name", () => {
it("does not duplicate anthropic in getApiKeyProviders when OAuth-backed", () => {
const fusionAuth = makeAuthStorage();
fusionAuth.getOAuthProviders = vi.fn(() => [
{ id: "anthropic", name: "Anthropic" },
@@ -176,8 +176,7 @@ describe("wrapAuthStorageWithApiKeyProviders", () => {
const apiKeyProviders = wrapped.getApiKeyProviders();
const anthropic = apiKeyProviders.find((p) => p.id === "anthropic");
expect(anthropic).toBeDefined();
expect(anthropic!.name).toBe("Anthropic");
expect(anthropic).toBeUndefined();
});
it("stores anthropic credentials as api_key type", () => {

View File

@@ -18,6 +18,13 @@ export function getCodexCliAuthPath(home = process.env.HOME || process.env.USERP
return join(home, ".codex", "auth.json");
}
export function getClaudeCodeCredentialPaths(home = process.env.HOME || process.env.USERPROFILE || homedir()): string[] {
return [
join(home, ".claude", ".credentials.json"),
join(home, ".config", "claude", ".credentials.json"),
];
}
export function getLegacyAuthPaths(home = process.env.HOME || process.env.USERPROFILE || homedir()): string[] {
return [
join(home, ".pi", "agent", "auth.json"),

View File

@@ -61,7 +61,7 @@ import {
} from "./llama-cpp-extension.js";
import { resolveSelfExtension } from "./self-extension.js";
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { getClaudeCodeCredentialPaths, getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { resolveProject } from "../project-context.js";
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
import { syncStartupModels } from "./startup-model-sync.js";
@@ -424,6 +424,7 @@ export async function runDaemon(opts: DaemonOptions = {}) {
const supplementalAuthStorage = createReadOnlyAuthFileStorage([
...getLegacyAuthPaths(),
getCodexCliAuthPath(),
...getClaudeCodeCredentialPaths(),
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());

View File

@@ -37,7 +37,7 @@ import {
import { promptForPort } from "./port-prompt.js";
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { getClaudeCodeCredentialPaths, getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { resolveProject } from "../project-context.js";
import {
ensureClaudeSkillsForAllProjectsOnStartup,
@@ -1220,6 +1220,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
const supplementalAuthStorage = createReadOnlyAuthFileStorage([
...getLegacyAuthPaths(),
getCodexCliAuthPath(),
...getClaudeCodeCredentialPaths(),
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());

View File

@@ -41,18 +41,7 @@ interface ReadFallbackAuthStorage {
type StoredCredential = StoredAuthCredential;
/**
* Provider IDs that should be treated as OAuth-backed by the upstream
* pi-coding-agent AuthStorage but which Fusion reclassifies as API-key
* providers. These IDs are stripped from getOAuthProviders() results so
* the dashboard never offers a browser-based OAuth login for them.
*/
const OAUTH_TO_API_KEY_RECLASSIFICATIONS: ReadonlySet<string> = new Set([
"anthropic",
]);
const BUILT_IN_API_KEY_PROVIDERS: Array<{ id: string; name: string }> = [
{ id: "anthropic", name: "Anthropic" },
{ id: "brave", name: "Brave Search" },
{ id: "kimi-coding", name: "Kimi" },
{ id: "minimax", name: "Minimax" },
@@ -90,7 +79,6 @@ export function wrapAuthStorageWithApiKeyProviders(
getOAuthProviders: () =>
mergedAuthStorage
.getOAuthProviders()
.filter((provider) => !OAUTH_TO_API_KEY_RECLASSIFICATIONS.has(provider.id))
.map((provider) => ({ id: provider.id, name: provider.name })),
hasAuth: (provider) => mergedAuthStorage.hasAuth(provider),
login: (providerId, callbacks) =>
@@ -100,12 +88,9 @@ export function wrapAuthStorageWithApiKeyProviders(
),
logout: (provider) => mergedAuthStorage.logout(provider),
getApiKeyProviders: () => {
// Use the reclassified (filtered) OAuth provider list so that providers
// moved to API-key (e.g. anthropic) are not skipped by the OAuth dedup.
const oauthProviderIds = new Set(
mergedAuthStorage
.getOAuthProviders()
.filter((provider) => !OAUTH_TO_API_KEY_RECLASSIFICATIONS.has(provider.id))
.map((provider) => provider.id),
);
const providers = new Map<string, string>();

View File

@@ -41,7 +41,7 @@ import {
import { promptForPort } from "./port-prompt.js";
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { getClaudeCodeCredentialPaths, getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { resolveProject } from "../project-context.js";
import {
ensureClaudeSkillsForAllProjectsOnStartup,
@@ -487,6 +487,7 @@ export async function runServe(
const supplementalAuthStorage = createReadOnlyAuthFileStorage([
...getLegacyAuthPaths(),
getCodexCliAuthPath(),
...getClaudeCodeCredentialPaths(),
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());