Auto-discover Codex/Gemini OAuth files across common homes

This commit is contained in:
ilya-bov
2026-03-06 23:20:33 +03:00
parent 14588cd9bd
commit a7261c2fbe
3 changed files with 125 additions and 13 deletions

View File

@@ -38,6 +38,7 @@ APP_PORT=3000
# XDG_CACHE_HOME=/app/data/.cache # XDG_CACHE_HOME=/app/data/.cache
# Optional CLI OAuth credential file overrides (for codex-cli / gemini-cli provider auth checks). # Optional CLI OAuth credential file overrides (for codex-cli / gemini-cli provider auth checks).
# If unset, Eggent auto-discovers files in common home directories.
# Useful when Eggent runs under a different user than the CLI login user. # Useful when Eggent runs under a different user than the CLI login user.
# CODEX_AUTH_FILE=/home/node/.codex/auth.json # CODEX_AUTH_FILE=/home/node/.codex/auth.json
# GEMINI_OAUTH_CREDS_FILE=/home/node/.gemini/oauth_creds.json # GEMINI_OAUTH_CREDS_FILE=/home/node/.gemini/oauth_creds.json

View File

@@ -216,9 +216,9 @@ Main environment variables:
| `PLAYWRIGHT_BROWSERS_PATH` | No | Browser install/cache path for Playwright (default: `/app/data/ms-playwright`) | | `PLAYWRIGHT_BROWSERS_PATH` | No | Browser install/cache path for Playwright (default: `/app/data/ms-playwright`) |
| `NPM_CONFIG_CACHE` | No | npm cache directory for runtime installs (default: `/app/data/npm-cache`) | | `NPM_CONFIG_CACHE` | No | npm cache directory for runtime installs (default: `/app/data/npm-cache`) |
| `XDG_CACHE_HOME` | No | Generic CLI cache directory (default: `/app/data/.cache`) | | `XDG_CACHE_HOME` | No | Generic CLI cache directory (default: `/app/data/.cache`) |
| `CODEX_AUTH_FILE` | No | Explicit path to Codex OAuth file (default: `$HOME/.codex/auth.json`) | | `CODEX_AUTH_FILE` | No | Explicit path to Codex OAuth file (if unset, Eggent auto-discovers `.codex/auth.json` in common home dirs) |
| `GEMINI_OAUTH_CREDS_FILE` | No | Explicit path to Gemini OAuth creds file (default: `$HOME/.gemini/oauth_creds.json`) | | `GEMINI_OAUTH_CREDS_FILE` | No | Explicit path to Gemini OAuth creds file (if unset, Eggent auto-discovers `.gemini/oauth_creds.json` in common home dirs) |
| `GEMINI_SETTINGS_FILE` | No | Explicit path to Gemini settings file (default: `$HOME/.gemini/settings.json`) | | `GEMINI_SETTINGS_FILE` | No | Explicit path to Gemini settings file (if unset, Eggent auto-discovers `.gemini/settings.json` in common home dirs) |
## Data Persistence ## Data Persistence

View File

@@ -83,11 +83,88 @@ function resolveAuthPath(envName: string, defaultPath: string): string {
return trimmed ? trimmed : defaultPath; return trimmed ? trimmed : defaultPath;
} }
function isReadableFile(filePath: string): boolean {
try {
return fs.statSync(filePath).isFile();
} catch {
return false;
}
}
function listChildDirs(baseDir: string): string[] {
try {
return fs
.readdirSync(baseDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
.map((entry) => path.join(baseDir, entry.name));
} catch {
return [];
}
}
function collectHomeCandidates(): string[] {
const candidates = new Set<string>();
const addCandidate = (value: string | undefined | null) => {
if (!value) return;
const trimmed = value.trim();
if (!trimmed) return;
candidates.add(trimmed);
};
addCandidate(os.homedir());
addCandidate(process.env.HOME);
addCandidate("/home/node");
addCandidate("/root");
addCandidate(path.join(process.cwd(), "data"));
for (const dir of listChildDirs("/home")) {
addCandidate(dir);
}
for (const dir of listChildDirs("/Users")) {
addCandidate(dir);
}
return Array.from(candidates);
}
function firstExistingFile(paths: string[]): string | null {
for (const filePath of paths) {
if (isReadableFile(filePath)) {
return filePath;
}
}
return null;
}
function discoverPath(defaultPath: string, relativePath: string): string {
if (isReadableFile(defaultPath)) {
return defaultPath;
}
const discovered =
firstExistingFile(
collectHomeCandidates().map((homeDir) => path.join(homeDir, relativePath))
) || null;
return discovered || defaultPath;
}
function deriveGeminiSettingsPathFromCreds(credsPath: string): string | null {
const parsed = path.parse(credsPath);
if (parsed.base !== "oauth_creds.json") return null;
if (path.basename(parsed.dir) !== ".gemini") return null;
return path.join(parsed.dir, "settings.json");
}
function readCodexAuth(): { path: string; parsed: CodexAuthFile | null } { function readCodexAuth(): { path: string; parsed: CodexAuthFile | null } {
const authPath = resolveAuthPath( const defaultPath = path.join(os.homedir(), ".codex", "auth.json");
const configuredPath = resolveAuthPath(
"CODEX_AUTH_FILE", "CODEX_AUTH_FILE",
path.join(os.homedir(), ".codex", "auth.json") defaultPath
); );
const authPath =
configuredPath === defaultPath
? discoverPath(defaultPath, path.join(".codex", "auth.json"))
: configuredPath;
const parsed = readJsonObject(authPath) as CodexAuthFile | null; const parsed = readJsonObject(authPath) as CodexAuthFile | null;
return { path: authPath, parsed }; return { path: authPath, parsed };
} }
@@ -173,25 +250,47 @@ function checkCodexOauthStatus(): ProviderAuthStatus {
} }
function readGeminiSettings(): { path: string; parsed: Record<string, unknown> | null } { function readGeminiSettings(): { path: string; parsed: Record<string, unknown> | null } {
const defaultPath = path.join(os.homedir(), ".gemini", "settings.json");
const settingsPath = resolveAuthPath( const settingsPath = resolveAuthPath(
"GEMINI_SETTINGS_FILE", "GEMINI_SETTINGS_FILE",
path.join(os.homedir(), ".gemini", "settings.json") defaultPath
); );
return { path: settingsPath, parsed: readJsonObject(settingsPath) }; const resolvedSettingsPath =
settingsPath === defaultPath
? discoverPath(defaultPath, path.join(".gemini", "settings.json"))
: settingsPath;
return { path: resolvedSettingsPath, parsed: readJsonObject(resolvedSettingsPath) };
} }
function readGeminiOauthCreds(): { path: string; parsed: GeminiOauthCreds | null } { function readGeminiOauthCreds(): { path: string; parsed: GeminiOauthCreds | null } {
const defaultPath = path.join(os.homedir(), ".gemini", "oauth_creds.json");
const credsPath = resolveAuthPath( const credsPath = resolveAuthPath(
"GEMINI_OAUTH_CREDS_FILE", "GEMINI_OAUTH_CREDS_FILE",
path.join(os.homedir(), ".gemini", "oauth_creds.json") defaultPath
); );
const parsed = readJsonObject(credsPath) as GeminiOauthCreds | null; const resolvedCredsPath =
return { path: credsPath, parsed }; credsPath === defaultPath
? discoverPath(defaultPath, path.join(".gemini", "oauth_creds.json"))
: credsPath;
const parsed = readJsonObject(resolvedCredsPath) as GeminiOauthCreds | null;
return { path: resolvedCredsPath, parsed };
} }
function resolveGeminiCredential(): ResolvedCliOAuthCredential { function resolveGeminiCredential(): ResolvedCliOAuthCredential {
const { parsed: creds } = readGeminiOauthCreds(); const { path: credsPath, parsed: creds } = readGeminiOauthCreds();
const { path: settingsPath, parsed: settings } = readGeminiSettings(); const settingsFromCreds = deriveGeminiSettingsPathFromCreds(credsPath);
const settingsConfigured = process.env.GEMINI_SETTINGS_FILE?.trim();
const { path: discoveredSettingsPath, parsed: discoveredSettings } = readGeminiSettings();
const settingsPath =
!settingsConfigured &&
settingsFromCreds &&
isReadableFile(settingsFromCreds)
? settingsFromCreds
: discoveredSettingsPath;
const settings =
settingsPath === discoveredSettingsPath
? discoveredSettings
: readJsonObject(settingsPath);
if (!creds) { if (!creds) {
throw new Error("Gemini OAuth file is missing. Run `gemini` and login with Google."); throw new Error("Gemini OAuth file is missing. Run `gemini` and login with Google.");
} }
@@ -238,7 +337,19 @@ function resolveGeminiCredential(): ResolvedCliOAuthCredential {
function checkGeminiOauthStatus(): ProviderAuthStatus { function checkGeminiOauthStatus(): ProviderAuthStatus {
const { path: credsPath, parsed: creds } = readGeminiOauthCreds(); const { path: credsPath, parsed: creds } = readGeminiOauthCreds();
const { path: settingsPath, parsed: settings } = readGeminiSettings(); const settingsFromCreds = deriveGeminiSettingsPathFromCreds(credsPath);
const settingsConfigured = process.env.GEMINI_SETTINGS_FILE?.trim();
const { path: discoveredSettingsPath, parsed: discoveredSettings } = readGeminiSettings();
const settingsPath =
!settingsConfigured &&
settingsFromCreds &&
isReadableFile(settingsFromCreds)
? settingsFromCreds
: discoveredSettingsPath;
const settings =
settingsPath === discoveredSettingsPath
? discoveredSettings
: readJsonObject(settingsPath);
if (!creds) { if (!creds) {
return { return {