fix(FN-XXX): unify codex auth and chat fallback

This commit is contained in:
gsxdsm
2026-05-03 23:08:05 -07:00
parent 6666a90b6e
commit a3ce8e1f8e
41 changed files with 2340 additions and 325 deletions

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { buildDevNodeArgs } from "../../../../scripts/dev-with-memory-lib.mjs";
describe("buildDevNodeArgs", () => {
it("enables source-condition resolution before loading the tsx runtime", () => {
const args = buildDevNodeArgs({
inspectFlags: ["--inspect=9230"],
preload: "/tmp/preflight.cjs",
loader: "/tmp/loader.mjs",
entry: "/tmp/bin.ts",
args: ["dashboard", "--host", "0.0.0.0"],
});
expect(args).toEqual([
"--inspect=9230",
"--conditions=source",
"--require",
"/tmp/preflight.cjs",
"--import",
"file:///tmp/loader.mjs",
"/tmp/bin.ts",
"dashboard",
"--host",
"0.0.0.0",
]);
});
});

View File

@@ -14,6 +14,10 @@ export function getFusionAuthPath(home = process.env.HOME || process.env.USERPRO
return join(getFusionAgentDir(home), "auth.json");
}
export function getCodexCliAuthPath(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
return join(home, ".codex", "auth.json");
}
export function getLegacyAuthPaths(home = process.env.HOME || process.env.USERPROFILE || homedir()): string[] {
return [
join(home, ".pi", "agent", "auth.json"),

View File

@@ -56,7 +56,7 @@ import {
} from "./droid-cli-extension.js";
import { resolveSelfExtension } from "./self-extension.js";
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { resolveProject } from "../project-context.js";
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
@@ -415,8 +415,11 @@ export async function runDaemon(opts: DaemonOptions = {}) {
const automationStore = cwdEngine.getAutomationStore();
const authStorage = AuthStorage.create(getFusionAuthPath());
const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths());
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
const supplementalAuthStorage = createReadOnlyAuthFileStorage([
...getLegacyAuthPaths(),
getCodexCliAuthPath(),
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);

View File

@@ -36,7 +36,7 @@ import {
import { promptForPort } from "./port-prompt.js";
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { resolveProject } from "../project-context.js";
import {
ensureClaudeSkillsForAllProjectsOnStartup,
@@ -1195,8 +1195,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
// Passing these to createServer enables the dashboard's Authentication
// tab (login/logout) and Model selector.
const authStorage = AuthStorage.create(getFusionAuthPath());
const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths());
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
const supplementalAuthStorage = createReadOnlyAuthFileStorage([
...getLegacyAuthPaths(),
getCodexCliAuthPath(),
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);

View File

@@ -1,12 +1,20 @@
import { existsSync, readFileSync } from "node:fs";
import type {
AuthStorage,
ModelRegistry,
AuthCredential,
} from "@mariozechner/pi-coding-agent";
import {
choosePreferredStoredCredential,
readStoredCredentialsFromAuthFile,
shouldHydrateStoredCredential,
type StoredAuthCredential,
} from "@fusion/core";
import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
import type { OAuthCredentials } from "@mariozechner/pi-ai/oauth";
export type LoginCallbacks = Parameters<AuthStorage["login"]>[1];
export type LoginCallbacks = Parameters<AuthStorage["login"]>[1] & {
onManualCodeInput?: () => Promise<string>;
};
export interface DashboardAuthStorage {
reload(): void;
@@ -31,14 +39,7 @@ interface ReadFallbackAuthStorage {
list(): string[];
}
type StoredCredential = {
type?: string;
key?: string;
access?: string;
refresh?: string;
expires?: number;
[key: string]: unknown;
};
type StoredCredential = StoredAuthCredential;
/**
* Provider IDs that should be treated as OAuth-backed by the upstream
@@ -93,7 +94,10 @@ export function wrapAuthStorageWithApiKeyProviders(
.map((provider) => ({ id: provider.id, name: provider.name })),
hasAuth: (provider) => mergedAuthStorage.hasAuth(provider),
login: (providerId, callbacks) =>
mergedAuthStorage.login(providerId as Parameters<AuthStorage["login"]>[0], callbacks),
mergedAuthStorage.login(
providerId as Parameters<AuthStorage["login"]>[0],
callbacks as Parameters<AuthStorage["login"]>[1],
),
logout: (provider) => mergedAuthStorage.logout(provider),
getApiKeyProviders: () => {
// Use the reclassified (filtered) OAuth provider list so that providers
@@ -149,14 +153,35 @@ export function mergeAuthStorageReads(
readFallbackAuthStorages: ReadFallbackAuthStorage[] = [],
): AuthStorage {
const readAuthStorages = [authStorage, ...readFallbackAuthStorages];
const getCredential = (providerId: string) => {
for (const storage of readAuthStorages) {
const credential = storage.get(providerId);
if (credential) return credential;
const selectCredential = (
providerId: string,
storages: Array<Pick<ReadFallbackAuthStorage, "get">>,
): StoredCredential | undefined => {
let best: StoredCredential | undefined;
for (const storage of storages) {
best = choosePreferredStoredCredential(best, storage.get(providerId));
}
return undefined;
return best;
};
const getCredential = (providerId: string) => selectCredential(providerId, readAuthStorages);
const syncFallbackOauthCredentials = () => {
const providerIds = new Set(readFallbackAuthStorages.flatMap((storage) => storage.list()));
for (const providerId of providerIds) {
const current = authStorage.get(providerId) as StoredCredential | undefined;
const candidate = selectCredential(providerId, readFallbackAuthStorages);
if (!shouldHydrateStoredCredential(current, candidate)) {
continue;
}
if (candidate && (candidate.type === "oauth" || candidate.type === "api_key")) {
authStorage.set(providerId, candidate as AuthCredential);
}
}
};
syncFallbackOauthCredentials();
return new Proxy(authStorage, {
get(target, prop, receiver) {
if (prop === "reload") {
@@ -164,6 +189,7 @@ export function mergeAuthStorageReads(
for (const storage of readAuthStorages) {
storage.reload();
}
syncFallbackOauthCredentials();
};
}
@@ -180,13 +206,17 @@ export function mergeAuthStorageReads(
}
if (prop === "getAll") {
return () => ({
...readFallbackAuthStorages.reduce(
(merged, storage) => ({ ...merged, ...storage.getAll() }),
{} as Record<string, { type?: string; key?: string }>,
),
...target.getAll(),
});
return () => {
const providerIds = new Set(readAuthStorages.flatMap((storage) => storage.list()));
const merged: Record<string, StoredCredential> = {};
for (const providerId of providerIds) {
const credential = getCredential(providerId);
if (credential) {
merged[providerId] = credential;
}
}
return merged;
};
}
if (prop === "list") {
@@ -243,16 +273,9 @@ export function createReadOnlyAuthFileStorage(authPaths: string[]): ReadFallback
const reload = () => {
const nextCredentials: Record<string, StoredCredential> = {};
for (const authPath of authPaths) {
if (!existsSync(authPath)) {
continue;
}
try {
const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as Record<string, StoredCredential>;
for (const [provider, credential] of Object.entries(parsed)) {
nextCredentials[provider] ??= credential;
}
} catch {
// Ignore unreadable legacy auth files and continue with other candidates.
const parsed = readStoredCredentialsFromAuthFile(authPath);
for (const [provider, credential] of Object.entries(parsed)) {
nextCredentials[provider] = choosePreferredStoredCredential(nextCredentials[provider], credential) ?? credential;
}
}
credentials = nextCredentials;

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 { getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { getCodexCliAuthPath, getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
import { resolveProject } from "../project-context.js";
import {
ensureClaudeSkillsForAllProjectsOnStartup,
@@ -478,8 +478,11 @@ export async function runServe(
const automationStore = cwdEngine.getAutomationStore();
const authStorage = AuthStorage.create(getFusionAuthPath());
const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths());
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
const supplementalAuthStorage = createReadOnlyAuthFileStorage([
...getLegacyAuthPaths(),
getCodexCliAuthPath(),
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);