fix(FN-XXX): unify codex auth and chat fallback
This commit is contained in:
27
packages/cli/src/__tests__/dev-with-memory-lib.test.ts
Normal file
27
packages/cli/src/__tests__/dev-with-memory-lib.test.ts
Normal 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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user