agentgui 1.0.752 → 1.0.753

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+ import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState, oauthResultPage, oauthRelayPage } from './oauth-common.js';
6
+
7
+ const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
8
+ const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
9
+ const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
10
+ const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
11
+ const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
12
+ const CODEX_OAUTH_PORT = 1455;
13
+
14
+ let codexOAuthState = { status: 'idle', error: null, email: null };
15
+ let codexOAuthPending = null;
16
+
17
+ function generatePkce() {
18
+ const verifierBytes = crypto.randomBytes(64);
19
+ const codeVerifier = verifierBytes.toString('base64url');
20
+ const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
21
+ const codeChallenge = challengeBytes.toString('base64url');
22
+ return { codeVerifier, codeChallenge };
23
+ }
24
+
25
+ function parseJwtEmail(jwt) {
26
+ try {
27
+ const parts = jwt.split('.');
28
+ if (parts.length < 2) return '';
29
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
30
+ return payload.email || payload['https://api.openai.com/profile']?.email || '';
31
+ } catch (_) { return ''; }
32
+ }
33
+
34
+ function saveCodexCredentials(tokens) {
35
+ if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
36
+ const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
37
+ fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
38
+ try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
39
+ }
40
+
41
+ export function getCodexOAuthStatus() {
42
+ try {
43
+ if (fs.existsSync(CODEX_AUTH_FILE)) {
44
+ const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
45
+ if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
46
+ const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
47
+ return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
48
+ }
49
+ }
50
+ } catch (_) {}
51
+ return null;
52
+ }
53
+
54
+ export async function startCodexOAuth(req, { PORT, BASE_URL }) {
55
+ const remote = isRemoteRequest(req);
56
+ const redirectUri = remote
57
+ ? `${buildBaseUrl(req, PORT)}${BASE_URL}/codex-oauth2callback`
58
+ : `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
59
+ const pkce = generatePkce();
60
+ const csrfToken = crypto.randomBytes(32).toString('hex');
61
+ const relayUrl = remote ? `${buildBaseUrl(req, PORT)}${BASE_URL}/api/codex-oauth/relay` : null;
62
+ const state = encodeOAuthState(csrfToken, relayUrl);
63
+ const params = new URLSearchParams({
64
+ response_type: 'code',
65
+ client_id: CODEX_CLIENT_ID,
66
+ redirect_uri: redirectUri,
67
+ scope: CODEX_SCOPES,
68
+ code_challenge: pkce.codeChallenge,
69
+ code_challenge_method: 'S256',
70
+ id_token_add_organizations: 'true',
71
+ codex_cli_simplified_flow: 'true',
72
+ state,
73
+ });
74
+ const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
75
+ const mode = remote ? 'remote' : 'local';
76
+ codexOAuthPending = { pkce, redirectUri, state: csrfToken };
77
+ codexOAuthState = { status: 'pending', error: null, email: null };
78
+ setTimeout(() => {
79
+ if (codexOAuthState.status === 'pending') {
80
+ codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
81
+ codexOAuthPending = null;
82
+ }
83
+ }, 5 * 60 * 1000);
84
+ return { authUrl, mode };
85
+ }
86
+
87
+ export async function exchangeCodexOAuthCode(code, stateParam) {
88
+ if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
89
+ const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
90
+ const { csrfToken } = decodeOAuthState(stateParam);
91
+ if (csrfToken !== expectedCsrf) {
92
+ codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
93
+ codexOAuthPending = null;
94
+ throw new Error('State mismatch - possible CSRF attack.');
95
+ }
96
+ if (!code) {
97
+ codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
98
+ codexOAuthPending = null;
99
+ throw new Error('No authorization code received.');
100
+ }
101
+ const body = new URLSearchParams({
102
+ grant_type: 'authorization_code',
103
+ code,
104
+ redirect_uri: redirectUri,
105
+ client_id: CODEX_CLIENT_ID,
106
+ code_verifier: pkce.codeVerifier,
107
+ });
108
+ const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
111
+ body: body.toString(),
112
+ });
113
+ if (!resp.ok) {
114
+ const text = await resp.text();
115
+ codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
116
+ codexOAuthPending = null;
117
+ throw new Error(`Token exchange failed (${resp.status}): ${text}`);
118
+ }
119
+ const tokens = await resp.json();
120
+ const email = parseJwtEmail(tokens.id_token || '');
121
+ saveCodexCredentials(tokens);
122
+ codexOAuthState = { status: 'success', error: null, email };
123
+ codexOAuthPending = null;
124
+ return email;
125
+ }
126
+
127
+ export async function handleCodexOAuthCallback(req, res, PORT) {
128
+ const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
129
+ const code = reqUrl.searchParams.get('code');
130
+ const state = reqUrl.searchParams.get('state');
131
+ const error = reqUrl.searchParams.get('error');
132
+ const errorDesc = reqUrl.searchParams.get('error_description');
133
+ if (error) {
134
+ const desc = errorDesc || error;
135
+ codexOAuthState = { status: 'error', error: desc, email: null };
136
+ codexOAuthPending = null;
137
+ }
138
+ const stateData = decodeOAuthState(state || '');
139
+ if (stateData.relayUrl) {
140
+ res.writeHead(200, { 'Content-Type': 'text/html' });
141
+ res.end(oauthRelayPage(code, state, errorDesc || error));
142
+ return;
143
+ }
144
+ if (!codexOAuthPending) {
145
+ res.writeHead(200, { 'Content-Type': 'text/html' });
146
+ res.end(oauthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
147
+ return;
148
+ }
149
+ try {
150
+ if (error) throw new Error(errorDesc || error);
151
+ const email = await exchangeCodexOAuthCode(code, state);
152
+ res.writeHead(200, { 'Content-Type': 'text/html' });
153
+ res.end(oauthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
154
+ } catch (e) {
155
+ res.writeHead(200, { 'Content-Type': 'text/html' });
156
+ res.end(oauthResultPage('Authentication Failed', e.message, false));
157
+ }
158
+ }
159
+
160
+ export function getCodexOAuthState() { return codexOAuthState; }
161
+ export function getCodexOAuthPending() { return codexOAuthPending; }
162
+ export { CODEX_HOME, CODEX_AUTH_FILE };
@@ -0,0 +1,92 @@
1
+ import crypto from 'crypto';
2
+
3
+ export function buildBaseUrl(req, PORT) {
4
+ const override = process.env.AGENTGUI_BASE_URL;
5
+ if (override) return override.replace(/\/+$/, '');
6
+ const fwdProto = req.headers['x-forwarded-proto'];
7
+ const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
8
+ if (fwdHost) {
9
+ const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
10
+ const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
11
+ return `${proto}://${cleanHost}`;
12
+ }
13
+ return `http://127.0.0.1:${PORT}`;
14
+ }
15
+
16
+ export function isRemoteRequest(req) {
17
+ return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
18
+ }
19
+
20
+ export function encodeOAuthState(csrfToken, relayUrl) {
21
+ const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
22
+ return Buffer.from(payload).toString('base64url');
23
+ }
24
+
25
+ export function decodeOAuthState(stateStr) {
26
+ try {
27
+ const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
28
+ return { csrfToken: payload.t, relayUrl: payload.r };
29
+ } catch (_) {
30
+ return { csrfToken: stateStr, relayUrl: null };
31
+ }
32
+ }
33
+
34
+ export function oauthResultPage(title, message, success) {
35
+ const color = success ? '#10b981' : '#ef4444';
36
+ const icon = success ? '&#10003;' : '&#10007;';
37
+ return `<!DOCTYPE html><html><head><title>${title}</title></head>
38
+ <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
39
+ <div style="text-align:center;max-width:400px;padding:2rem;">
40
+ <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
41
+ <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
42
+ <p style="color:#9ca3af;">${message}</p>
43
+ <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
44
+ </div></body></html>`;
45
+ }
46
+
47
+ export function oauthRelayPage(code, state, error) {
48
+ const stateData = decodeOAuthState(state || '');
49
+ const relayUrl = stateData.relayUrl || '';
50
+ const escapedCode = (code || '').replace(/['"\\]/g, '');
51
+ const escapedState = (state || '').replace(/['"\\]/g, '');
52
+ const escapedError = (error || '').replace(/['"\\]/g, '');
53
+ const escapedRelay = relayUrl.replace(/['"\\]/g, '');
54
+ return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
55
+ <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
56
+ <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
57
+ <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
58
+ <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
59
+ <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
60
+ </div>
61
+ <script>
62
+ (function() {
63
+ var code = '${escapedCode}';
64
+ var state = '${escapedState}';
65
+ var error = '${escapedError}';
66
+ var relayUrl = '${escapedRelay}';
67
+ function show(icon, title, msg, color) {
68
+ document.getElementById('spinner').textContent = icon;
69
+ document.getElementById('spinner').style.color = color;
70
+ document.getElementById('title').textContent = title;
71
+ document.getElementById('msg').textContent = msg;
72
+ }
73
+ if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
74
+ if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
75
+ if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
76
+ fetch(relayUrl, {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({ code: code, state: state })
80
+ }).then(function(r) { return r.json(); }).then(function(data) {
81
+ if (data.success) {
82
+ show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
83
+ } else {
84
+ show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
85
+ }
86
+ }).catch(function(e) {
87
+ show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
88
+ });
89
+ })();
90
+ </script>
91
+ </body></html>`;
92
+ }
@@ -0,0 +1,200 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+ import { execSync } from 'child_process';
6
+ import { OAuth2Client } from 'google-auth-library';
7
+ import { findCommand } from './agent-discovery.js';
8
+ import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState, oauthResultPage, oauthRelayPage } from './oauth-common.js';
9
+
10
+ const GEMINI_SCOPES = [
11
+ 'https://www.googleapis.com/auth/cloud-platform',
12
+ 'https://www.googleapis.com/auth/userinfo.email',
13
+ 'https://www.googleapis.com/auth/userinfo.profile',
14
+ ];
15
+ const GEMINI_DIR = path.join(os.homedir(), '.gemini');
16
+ const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
17
+ const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
18
+
19
+ let geminiOAuthState = { status: 'idle', error: null, email: null };
20
+ let geminiOAuthPending = null;
21
+
22
+ function extractOAuthFromFile(oauth2Path) {
23
+ try {
24
+ const src = fs.readFileSync(oauth2Path, 'utf8');
25
+ const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
26
+ const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
27
+ if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
28
+ } catch {}
29
+ return null;
30
+ }
31
+
32
+ export function getGeminiOAuthCreds(rootDir) {
33
+ if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
34
+ return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
35
+ }
36
+ const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
37
+ try {
38
+ const geminiPath = findCommand('gemini', rootDir);
39
+ if (geminiPath) {
40
+ const realPath = fs.realpathSync(geminiPath);
41
+ const pkgRoot = path.resolve(path.dirname(realPath), '..');
42
+ const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
43
+ if (result) return result;
44
+ }
45
+ } catch (e) {
46
+ console.error('[gemini-oauth] gemini lookup failed:', e.message);
47
+ }
48
+ try {
49
+ const npmCacheDirs = new Set();
50
+ const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
51
+ addDir(path.join(os.homedir(), '.npm'));
52
+ addDir(path.join(os.homedir(), '.cache', '.npm'));
53
+ if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
54
+ if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
55
+ try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
56
+ for (const cacheDir of npmCacheDirs) {
57
+ if (!fs.existsSync(cacheDir)) continue;
58
+ for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
59
+ const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
60
+ if (result) return result;
61
+ }
62
+ }
63
+ } catch (e) {
64
+ console.error('[gemini-oauth] npm cache scan failed:', e.message);
65
+ }
66
+ console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
67
+ return null;
68
+ }
69
+
70
+ function saveGeminiCredentials(tokens, email) {
71
+ if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
72
+ fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
73
+ try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
74
+ let accounts = { active: null, old: [] };
75
+ try {
76
+ if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
77
+ accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
78
+ }
79
+ } catch (_) {}
80
+ if (email) {
81
+ if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
82
+ accounts.old.push(accounts.active);
83
+ }
84
+ accounts.active = email;
85
+ }
86
+ fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
87
+ }
88
+
89
+ export async function startGeminiOAuth(req, { PORT, BASE_URL, rootDir }) {
90
+ const creds = getGeminiOAuthCreds(rootDir);
91
+ if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
92
+ const useCustomClient = !!creds.custom;
93
+ const remote = isRemoteRequest(req);
94
+ let redirectUri;
95
+ if (useCustomClient && req) {
96
+ redirectUri = `${buildBaseUrl(req, PORT)}${BASE_URL}/oauth2callback`;
97
+ } else {
98
+ redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
99
+ }
100
+ const csrfToken = crypto.randomBytes(32).toString('hex');
101
+ const relayUrl = req ? `${buildBaseUrl(req, PORT)}${BASE_URL}/api/gemini-oauth/relay` : null;
102
+ const state = encodeOAuthState(csrfToken, relayUrl);
103
+ const client = new OAuth2Client({ clientId: creds.clientId, clientSecret: creds.clientSecret });
104
+ const authUrl = client.generateAuthUrl({ redirect_uri: redirectUri, access_type: 'offline', scope: GEMINI_SCOPES, state });
105
+ const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
106
+ geminiOAuthPending = { client, redirectUri, state: csrfToken };
107
+ geminiOAuthState = { status: 'pending', error: null, email: null };
108
+ setTimeout(() => {
109
+ if (geminiOAuthState.status === 'pending') {
110
+ geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
111
+ geminiOAuthPending = null;
112
+ }
113
+ }, 5 * 60 * 1000);
114
+ return { authUrl, mode };
115
+ }
116
+
117
+ export async function exchangeGeminiOAuthCode(code, stateParam) {
118
+ if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
119
+ const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
120
+ const { csrfToken } = decodeOAuthState(stateParam);
121
+ if (csrfToken !== expectedCsrf) {
122
+ geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
123
+ geminiOAuthPending = null;
124
+ throw new Error('State mismatch - possible CSRF attack.');
125
+ }
126
+ if (!code) {
127
+ geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
128
+ geminiOAuthPending = null;
129
+ throw new Error('No authorization code received.');
130
+ }
131
+ const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
132
+ client.setCredentials(tokens);
133
+ let email = '';
134
+ try {
135
+ const { token } = await client.getAccessToken();
136
+ if (token) {
137
+ const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${token}` } });
138
+ if (resp.ok) { const info = await resp.json(); email = info.email || ''; }
139
+ }
140
+ } catch (_) {}
141
+ saveGeminiCredentials(tokens, email);
142
+ geminiOAuthState = { status: 'success', error: null, email };
143
+ geminiOAuthPending = null;
144
+ return email;
145
+ }
146
+
147
+ export async function handleGeminiOAuthCallback(req, res, PORT) {
148
+ const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
149
+ const code = reqUrl.searchParams.get('code');
150
+ const state = reqUrl.searchParams.get('state');
151
+ const error = reqUrl.searchParams.get('error');
152
+ const errorDesc = reqUrl.searchParams.get('error_description');
153
+ if (error) {
154
+ const desc = errorDesc || error;
155
+ geminiOAuthState = { status: 'error', error: desc, email: null };
156
+ geminiOAuthPending = null;
157
+ }
158
+ const stateData = decodeOAuthState(state || '');
159
+ if (stateData.relayUrl) {
160
+ res.writeHead(200, { 'Content-Type': 'text/html' });
161
+ res.end(oauthRelayPage(code, state, errorDesc || error));
162
+ return;
163
+ }
164
+ if (!geminiOAuthPending) {
165
+ res.writeHead(200, { 'Content-Type': 'text/html' });
166
+ res.end(oauthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
167
+ return;
168
+ }
169
+ try {
170
+ if (error) throw new Error(errorDesc || error);
171
+ const email = await exchangeGeminiOAuthCode(code, state);
172
+ res.writeHead(200, { 'Content-Type': 'text/html' });
173
+ res.end(oauthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
174
+ } catch (e) {
175
+ res.writeHead(200, { 'Content-Type': 'text/html' });
176
+ res.end(oauthResultPage('Authentication Failed', e.message, false));
177
+ }
178
+ }
179
+
180
+ export function getGeminiOAuthStatus() {
181
+ try {
182
+ if (fs.existsSync(GEMINI_OAUTH_FILE)) {
183
+ const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
184
+ if (creds.refresh_token || creds.access_token) {
185
+ let email = '';
186
+ try {
187
+ if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
188
+ const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
189
+ email = accts.active || '';
190
+ }
191
+ } catch (_) {}
192
+ return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
193
+ }
194
+ }
195
+ } catch (_) {}
196
+ return null;
197
+ }
198
+
199
+ export function getGeminiOAuthState() { return geminiOAuthState; }
200
+ export function getGeminiOAuthPending() { return geminiOAuthPending; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.752",
3
+ "version": "1.0.753",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/server.js CHANGED
@@ -10,7 +10,6 @@ import { execSync, spawn } from 'child_process';
10
10
  import { LRUCache } from 'lru-cache';
11
11
  import { createRequire } from 'module';
12
12
  const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
13
- import { OAuth2Client } from 'google-auth-library';
14
13
  import express from 'express';
15
14
  import Busboy from 'busboy';
16
15
  import fsbrowse from 'fsbrowse';
@@ -18,6 +17,8 @@ import { queries } from './database.js';
18
17
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
19
18
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
20
19
  import { findCommand, queryACPServerAgents, discoverAgents, discoverExternalACPServers, initializeAgentDiscovery } from './lib/agent-discovery.js';
20
+ import { getGeminiOAuthCreds, startGeminiOAuth, exchangeGeminiOAuthCode, handleGeminiOAuthCallback, getGeminiOAuthStatus, getGeminiOAuthState } from './lib/oauth-gemini.js';
21
+ import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
21
22
  import { WSOptimizer } from './lib/ws-optimizer.js';
22
23
  import { WsRouter } from './lib/ws-protocol.js';
23
24
  import { encode as wsEncode } from './lib/codec.js';
@@ -483,551 +484,6 @@ async function getModelsForAgent(agentId) {
483
484
  return models;
484
485
  }
485
486
 
486
- const GEMINI_SCOPES = [
487
- 'https://www.googleapis.com/auth/cloud-platform',
488
- 'https://www.googleapis.com/auth/userinfo.email',
489
- 'https://www.googleapis.com/auth/userinfo.profile',
490
- ];
491
-
492
- function extractOAuthFromFile(oauth2Path) {
493
- try {
494
- const src = fs.readFileSync(oauth2Path, 'utf8');
495
- const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
496
- const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
497
- if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
498
- } catch {}
499
- return null;
500
- }
501
-
502
- function getGeminiOAuthCreds() {
503
- if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
504
- return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
505
- }
506
- const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
507
- try {
508
- const geminiPath = findCommand('gemini', rootDir);
509
- if (geminiPath) {
510
- const realPath = fs.realpathSync(geminiPath);
511
- const pkgRoot = path.resolve(path.dirname(realPath), '..');
512
- const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
513
- if (result) return result;
514
- }
515
- } catch (e) {
516
- console.error('[gemini-oauth] gemini lookup failed:', e.message);
517
- }
518
- try {
519
- const npmCacheDirs = new Set();
520
- const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
521
- addDir(path.join(os.homedir(), '.npm'));
522
- addDir(path.join(os.homedir(), '.cache', '.npm'));
523
- if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
524
- if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
525
- try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
526
- for (const cacheDir of npmCacheDirs) {
527
- if (!fs.existsSync(cacheDir)) continue;
528
- for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
529
- const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
530
- if (result) return result;
531
- }
532
- }
533
- } catch (e) {
534
- console.error('[gemini-oauth] npm cache scan failed:', e.message);
535
- }
536
- console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
537
- return null;
538
- }
539
- const GEMINI_DIR = path.join(os.homedir(), '.gemini');
540
- const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
541
- const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
542
-
543
- let geminiOAuthState = { status: 'idle', error: null, email: null };
544
- let geminiOAuthPending = null;
545
-
546
- function buildBaseUrl(req) {
547
- const override = process.env.AGENTGUI_BASE_URL;
548
- if (override) return override.replace(/\/+$/, '');
549
- const fwdProto = req.headers['x-forwarded-proto'];
550
- const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
551
- if (fwdHost) {
552
- const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
553
- const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
554
- return `${proto}://${cleanHost}`;
555
- }
556
- return `http://127.0.0.1:${PORT}`;
557
- }
558
-
559
- function saveGeminiCredentials(tokens, email) {
560
- if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
561
- fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
562
- try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
563
-
564
- let accounts = { active: null, old: [] };
565
- try {
566
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
567
- accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
568
- }
569
- } catch (_) {}
570
-
571
- if (email) {
572
- if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
573
- accounts.old.push(accounts.active);
574
- }
575
- accounts.active = email;
576
- }
577
- fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
578
- }
579
-
580
- function geminiOAuthResultPage(title, message, success) {
581
- const color = success ? '#10b981' : '#ef4444';
582
- const icon = success ? '&#10003;' : '&#10007;';
583
- return `<!DOCTYPE html><html><head><title>${title}</title></head>
584
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
585
- <div style="text-align:center;max-width:400px;padding:2rem;">
586
- <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
587
- <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
588
- <p style="color:#9ca3af;">${message}</p>
589
- <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
590
- </div></body></html>`;
591
- }
592
-
593
- function encodeOAuthState(csrfToken, relayUrl) {
594
- const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
595
- return Buffer.from(payload).toString('base64url');
596
- }
597
-
598
- function decodeOAuthState(stateStr) {
599
- try {
600
- const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
601
- return { csrfToken: payload.t, relayUrl: payload.r };
602
- } catch (_) {
603
- return { csrfToken: stateStr, relayUrl: null };
604
- }
605
- }
606
-
607
- function geminiOAuthRelayPage(code, state, error) {
608
- const stateData = decodeOAuthState(state || '');
609
- const relayUrl = stateData.relayUrl || '';
610
- const escapedCode = (code || '').replace(/['"\\]/g, '');
611
- const escapedState = (state || '').replace(/['"\\]/g, '');
612
- const escapedError = (error || '').replace(/['"\\]/g, '');
613
- const escapedRelay = relayUrl.replace(/['"\\]/g, '');
614
- return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
615
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
616
- <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
617
- <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
618
- <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
619
- <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
620
- </div>
621
- <script>
622
- (function() {
623
- var code = '${escapedCode}';
624
- var state = '${escapedState}';
625
- var error = '${escapedError}';
626
- var relayUrl = '${escapedRelay}';
627
- function show(icon, title, msg, color) {
628
- document.getElementById('spinner').textContent = icon;
629
- document.getElementById('spinner').style.color = color;
630
- document.getElementById('title').textContent = title;
631
- document.getElementById('msg').textContent = msg;
632
- }
633
- if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
634
- if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
635
- if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
636
- fetch(relayUrl, {
637
- method: 'POST',
638
- headers: { 'Content-Type': 'application/json' },
639
- body: JSON.stringify({ code: code, state: state })
640
- }).then(function(r) { return r.json(); }).then(function(data) {
641
- if (data.success) {
642
- show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
643
- } else {
644
- show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
645
- }
646
- }).catch(function(e) {
647
- show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
648
- });
649
- })();
650
- </script>
651
- </body></html>`;
652
- }
653
-
654
- function isRemoteRequest(req) {
655
- return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
656
- }
657
-
658
- async function startGeminiOAuth(req) {
659
- const creds = getGeminiOAuthCreds();
660
- if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
661
-
662
- const useCustomClient = !!creds.custom;
663
- const remote = isRemoteRequest(req);
664
- let redirectUri;
665
- if (useCustomClient && req) {
666
- redirectUri = `${buildBaseUrl(req)}${BASE_URL}/oauth2callback`;
667
- } else {
668
- redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
669
- }
670
-
671
- const csrfToken = crypto.randomBytes(32).toString('hex');
672
- const relayUrl = req ? `${buildBaseUrl(req)}${BASE_URL}/api/gemini-oauth/relay` : null;
673
- const state = encodeOAuthState(csrfToken, relayUrl);
674
-
675
- const client = new OAuth2Client({
676
- clientId: creds.clientId,
677
- clientSecret: creds.clientSecret,
678
- });
679
-
680
- const authUrl = client.generateAuthUrl({
681
- redirect_uri: redirectUri,
682
- access_type: 'offline',
683
- scope: GEMINI_SCOPES,
684
- state,
685
- });
686
-
687
- const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
688
- geminiOAuthPending = { client, redirectUri, state: csrfToken };
689
- geminiOAuthState = { status: 'pending', error: null, email: null };
690
-
691
- setTimeout(() => {
692
- if (geminiOAuthState.status === 'pending') {
693
- geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
694
- geminiOAuthPending = null;
695
- }
696
- }, 5 * 60 * 1000);
697
-
698
- return { authUrl, mode };
699
- }
700
-
701
- async function exchangeGeminiOAuthCode(code, stateParam) {
702
- if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
703
-
704
- const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
705
- const { csrfToken } = decodeOAuthState(stateParam);
706
-
707
- if (csrfToken !== expectedCsrf) {
708
- geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
709
- geminiOAuthPending = null;
710
- throw new Error('State mismatch - possible CSRF attack.');
711
- }
712
-
713
- if (!code) {
714
- geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
715
- geminiOAuthPending = null;
716
- throw new Error('No authorization code received.');
717
- }
718
-
719
- const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
720
- client.setCredentials(tokens);
721
-
722
- let email = '';
723
- try {
724
- const { token } = await client.getAccessToken();
725
- if (token) {
726
- const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
727
- headers: { Authorization: `Bearer ${token}` }
728
- });
729
- if (resp.ok) {
730
- const info = await resp.json();
731
- email = info.email || '';
732
- }
733
- }
734
- } catch (_) {}
735
-
736
- saveGeminiCredentials(tokens, email);
737
- geminiOAuthState = { status: 'success', error: null, email };
738
- geminiOAuthPending = null;
739
-
740
- return email;
741
- }
742
-
743
- async function handleGeminiOAuthCallback(req, res) {
744
- const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
745
- const code = reqUrl.searchParams.get('code');
746
- const state = reqUrl.searchParams.get('state');
747
- const error = reqUrl.searchParams.get('error');
748
- const errorDesc = reqUrl.searchParams.get('error_description');
749
-
750
- if (error) {
751
- const desc = errorDesc || error;
752
- geminiOAuthState = { status: 'error', error: desc, email: null };
753
- geminiOAuthPending = null;
754
- }
755
-
756
- const stateData = decodeOAuthState(state || '');
757
- if (stateData.relayUrl) {
758
- res.writeHead(200, { 'Content-Type': 'text/html' });
759
- res.end(geminiOAuthRelayPage(code, state, errorDesc || error));
760
- return;
761
- }
762
-
763
- if (!geminiOAuthPending) {
764
- res.writeHead(200, { 'Content-Type': 'text/html' });
765
- res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
766
- return;
767
- }
768
-
769
- try {
770
- if (error) throw new Error(errorDesc || error);
771
- const email = await exchangeGeminiOAuthCode(code, state);
772
- res.writeHead(200, { 'Content-Type': 'text/html' });
773
- res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
774
- } catch (e) {
775
- res.writeHead(200, { 'Content-Type': 'text/html' });
776
- res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
777
- }
778
- }
779
-
780
- function getGeminiOAuthStatus() {
781
- try {
782
- if (fs.existsSync(GEMINI_OAUTH_FILE)) {
783
- const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
784
- if (creds.refresh_token || creds.access_token) {
785
- let email = '';
786
- try {
787
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
788
- const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
789
- email = accts.active || '';
790
- }
791
- } catch (_) {}
792
- return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
793
- }
794
- }
795
- } catch (_) {}
796
- return null;
797
- }
798
-
799
- const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
800
- const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
801
- const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
802
- const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
803
- const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
804
- const CODEX_OAUTH_PORT = 1455;
805
-
806
- let codexOAuthState = { status: 'idle', error: null, email: null };
807
- let codexOAuthPending = null;
808
-
809
- function generatePkce() {
810
- const verifierBytes = crypto.randomBytes(64);
811
- const codeVerifier = verifierBytes.toString('base64url');
812
- const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
813
- const codeChallenge = challengeBytes.toString('base64url');
814
- return { codeVerifier, codeChallenge };
815
- }
816
-
817
- function parseJwtEmail(jwt) {
818
- try {
819
- const parts = jwt.split('.');
820
- if (parts.length < 2) return '';
821
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
822
- return payload.email || payload['https://api.openai.com/profile']?.email || '';
823
- } catch (_) { return ''; }
824
- }
825
-
826
- function saveCodexCredentials(tokens) {
827
- if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
828
- const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
829
- fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
830
- try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
831
- }
832
-
833
- function getCodexOAuthStatus() {
834
- try {
835
- if (fs.existsSync(CODEX_AUTH_FILE)) {
836
- const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
837
- if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
838
- const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
839
- return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
840
- }
841
- }
842
- } catch (_) {}
843
- return null;
844
- }
845
-
846
- async function startCodexOAuth(req) {
847
- const remote = isRemoteRequest(req);
848
- const redirectUri = remote
849
- ? `${buildBaseUrl(req)}${BASE_URL}/codex-oauth2callback`
850
- : `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
851
-
852
- const pkce = generatePkce();
853
- const csrfToken = crypto.randomBytes(32).toString('hex');
854
- const relayUrl = remote ? `${buildBaseUrl(req)}${BASE_URL}/api/codex-oauth/relay` : null;
855
- const state = encodeOAuthState(csrfToken, relayUrl);
856
-
857
- const params = new URLSearchParams({
858
- response_type: 'code',
859
- client_id: CODEX_CLIENT_ID,
860
- redirect_uri: redirectUri,
861
- scope: CODEX_SCOPES,
862
- code_challenge: pkce.codeChallenge,
863
- code_challenge_method: 'S256',
864
- id_token_add_organizations: 'true',
865
- codex_cli_simplified_flow: 'true',
866
- state,
867
- });
868
-
869
- const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
870
- const mode = remote ? 'remote' : 'local';
871
-
872
- codexOAuthPending = { pkce, redirectUri, state: csrfToken };
873
- codexOAuthState = { status: 'pending', error: null, email: null };
874
-
875
- setTimeout(() => {
876
- if (codexOAuthState.status === 'pending') {
877
- codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
878
- codexOAuthPending = null;
879
- }
880
- }, 5 * 60 * 1000);
881
-
882
- return { authUrl, mode };
883
- }
884
-
885
- async function exchangeCodexOAuthCode(code, stateParam) {
886
- if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
887
-
888
- const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
889
- const { csrfToken } = decodeOAuthState(stateParam);
890
-
891
- if (csrfToken !== expectedCsrf) {
892
- codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
893
- codexOAuthPending = null;
894
- throw new Error('State mismatch - possible CSRF attack.');
895
- }
896
-
897
- if (!code) {
898
- codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
899
- codexOAuthPending = null;
900
- throw new Error('No authorization code received.');
901
- }
902
-
903
- const body = new URLSearchParams({
904
- grant_type: 'authorization_code',
905
- code,
906
- redirect_uri: redirectUri,
907
- client_id: CODEX_CLIENT_ID,
908
- code_verifier: pkce.codeVerifier,
909
- });
910
-
911
- const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
912
- method: 'POST',
913
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
914
- body: body.toString(),
915
- });
916
-
917
- if (!resp.ok) {
918
- const text = await resp.text();
919
- codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
920
- codexOAuthPending = null;
921
- throw new Error(`Token exchange failed (${resp.status}): ${text}`);
922
- }
923
-
924
- const tokens = await resp.json();
925
- const email = parseJwtEmail(tokens.id_token || '');
926
-
927
- saveCodexCredentials(tokens);
928
- codexOAuthState = { status: 'success', error: null, email };
929
- codexOAuthPending = null;
930
-
931
- return email;
932
- }
933
-
934
- function codexOAuthResultPage(title, message, success) {
935
- const color = success ? '#10b981' : '#ef4444';
936
- const icon = success ? '&#10003;' : '&#10007;';
937
- return `<!DOCTYPE html><html><head><title>${title}</title></head>
938
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
939
- <div style="text-align:center;max-width:400px;padding:2rem;">
940
- <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
941
- <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
942
- <p style="color:#9ca3af;">${message}</p>
943
- <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
944
- </div></body></html>`;
945
- }
946
-
947
- function codexOAuthRelayPage(code, state, error) {
948
- const stateData = decodeOAuthState(state || '');
949
- const relayUrl = stateData.relayUrl || '';
950
- const escapedCode = (code || '').replace(/['"\\]/g, '');
951
- const escapedState = (state || '').replace(/['"\\]/g, '');
952
- const escapedError = (error || '').replace(/['"\\]/g, '');
953
- const escapedRelay = relayUrl.replace(/['"\\]/g, '');
954
- return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
955
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
956
- <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
957
- <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
958
- <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
959
- <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
960
- </div>
961
- <script>
962
- (function() {
963
- var code = '${escapedCode}';
964
- var state = '${escapedState}';
965
- var error = '${escapedError}';
966
- var relayUrl = '${escapedRelay}';
967
- function show(icon, title, msg, color) {
968
- document.getElementById('spinner').textContent = icon;
969
- document.getElementById('spinner').style.color = color;
970
- document.getElementById('title').textContent = title;
971
- document.getElementById('msg').textContent = msg;
972
- }
973
- if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
974
- if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
975
- if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
976
- fetch(relayUrl, {
977
- method: 'POST',
978
- headers: { 'Content-Type': 'application/json' },
979
- body: JSON.stringify({ code: code, state: state })
980
- }).then(function(r) { return r.json(); }).then(function(data) {
981
- if (data.success) {
982
- show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
983
- } else {
984
- show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
985
- }
986
- }).catch(function(e) {
987
- show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
988
- });
989
- })();
990
- </script>
991
- </body></html>`;
992
- }
993
-
994
- async function handleCodexOAuthCallback(req, res) {
995
- const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
996
- const code = reqUrl.searchParams.get('code');
997
- const state = reqUrl.searchParams.get('state');
998
- const error = reqUrl.searchParams.get('error');
999
- const errorDesc = reqUrl.searchParams.get('error_description');
1000
-
1001
- if (error) {
1002
- const desc = errorDesc || error;
1003
- codexOAuthState = { status: 'error', error: desc, email: null };
1004
- codexOAuthPending = null;
1005
- }
1006
-
1007
- const stateData = decodeOAuthState(state || '');
1008
- if (stateData.relayUrl) {
1009
- res.writeHead(200, { 'Content-Type': 'text/html' });
1010
- res.end(codexOAuthRelayPage(code, state, errorDesc || error));
1011
- return;
1012
- }
1013
-
1014
- if (!codexOAuthPending) {
1015
- res.writeHead(200, { 'Content-Type': 'text/html' });
1016
- res.end(codexOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
1017
- return;
1018
- }
1019
-
1020
- try {
1021
- if (error) throw new Error(errorDesc || error);
1022
- const email = await exchangeCodexOAuthCode(code, state);
1023
- res.writeHead(200, { 'Content-Type': 'text/html' });
1024
- res.end(codexOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
1025
- } catch (e) {
1026
- res.writeHead(200, { 'Content-Type': 'text/html' });
1027
- res.end(codexOAuthResultPage('Authentication Failed', e.message, false));
1028
- }
1029
- }
1030
-
1031
487
  const PROVIDER_CONFIGS = {
1032
488
  'anthropic': {
1033
489
  name: 'Anthropic', configPaths: [
@@ -1281,12 +737,12 @@ const server = http.createServer(async (req, res) => {
1281
737
  const pathOnly = routePath.split('?')[0];
1282
738
 
1283
739
  if (pathOnly === '/oauth2callback' && req.method === 'GET') {
1284
- await handleGeminiOAuthCallback(req, res);
740
+ await handleGeminiOAuthCallback(req, res, PORT);
1285
741
  return;
1286
742
  }
1287
743
 
1288
744
  if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
1289
- await handleCodexOAuthCallback(req, res);
745
+ await handleCodexOAuthCallback(req, res, PORT);
1290
746
  return;
1291
747
  }
1292
748
 
@@ -2746,7 +2202,7 @@ const server = http.createServer(async (req, res) => {
2746
2202
 
2747
2203
  if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
2748
2204
  try {
2749
- const result = await startGeminiOAuth(req);
2205
+ const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
2750
2206
  sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
2751
2207
  } catch (e) {
2752
2208
  console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
@@ -2756,7 +2212,7 @@ const server = http.createServer(async (req, res) => {
2756
2212
  }
2757
2213
 
2758
2214
  if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
2759
- sendJSON(req, res, 200, geminiOAuthState);
2215
+ sendJSON(req, res, 200, getGeminiOAuthState());
2760
2216
  return;
2761
2217
  }
2762
2218
 
@@ -2771,8 +2227,6 @@ const server = http.createServer(async (req, res) => {
2771
2227
  const email = await exchangeGeminiOAuthCode(code, stateParam);
2772
2228
  sendJSON(req, res, 200, { success: true, email });
2773
2229
  } catch (e) {
2774
- geminiOAuthState = { status: 'error', error: e.message, email: null };
2775
- geminiOAuthPending = null;
2776
2230
  sendJSON(req, res, 400, { error: e.message });
2777
2231
  }
2778
2232
  return;
@@ -2796,8 +2250,6 @@ const server = http.createServer(async (req, res) => {
2796
2250
  const error = parsed.searchParams.get('error');
2797
2251
  if (error) {
2798
2252
  const desc = parsed.searchParams.get('error_description') || error;
2799
- geminiOAuthState = { status: 'error', error: desc, email: null };
2800
- geminiOAuthPending = null;
2801
2253
  sendJSON(req, res, 200, { error: desc });
2802
2254
  return;
2803
2255
  }
@@ -2807,8 +2259,6 @@ const server = http.createServer(async (req, res) => {
2807
2259
  const email = await exchangeGeminiOAuthCode(code, state);
2808
2260
  sendJSON(req, res, 200, { success: true, email });
2809
2261
  } catch (e) {
2810
- geminiOAuthState = { status: 'error', error: e.message, email: null };
2811
- geminiOAuthPending = null;
2812
2262
  sendJSON(req, res, 400, { error: e.message });
2813
2263
  }
2814
2264
  return;
@@ -2816,7 +2266,7 @@ const server = http.createServer(async (req, res) => {
2816
2266
 
2817
2267
  if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
2818
2268
  try {
2819
- const result = await startCodexOAuth(req);
2269
+ const result = await startCodexOAuth(req, { PORT, BASE_URL });
2820
2270
  sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
2821
2271
  } catch (e) {
2822
2272
  console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
@@ -2826,7 +2276,7 @@ const server = http.createServer(async (req, res) => {
2826
2276
  }
2827
2277
 
2828
2278
  if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
2829
- sendJSON(req, res, 200, codexOAuthState);
2279
+ sendJSON(req, res, 200, getCodexOAuthState());
2830
2280
  return;
2831
2281
  }
2832
2282
 
@@ -2841,8 +2291,6 @@ const server = http.createServer(async (req, res) => {
2841
2291
  const email = await exchangeCodexOAuthCode(code, stateParam);
2842
2292
  sendJSON(req, res, 200, { success: true, email });
2843
2293
  } catch (e) {
2844
- codexOAuthState = { status: 'error', error: e.message, email: null };
2845
- codexOAuthPending = null;
2846
2294
  sendJSON(req, res, 400, { error: e.message });
2847
2295
  }
2848
2296
  return;
@@ -2864,8 +2312,6 @@ const server = http.createServer(async (req, res) => {
2864
2312
  const error = parsed.searchParams.get('error');
2865
2313
  if (error) {
2866
2314
  const desc = parsed.searchParams.get('error_description') || error;
2867
- codexOAuthState = { status: 'error', error: desc, email: null };
2868
- codexOAuthPending = null;
2869
2315
  sendJSON(req, res, 200, { error: desc });
2870
2316
  return;
2871
2317
  }
@@ -2874,8 +2320,6 @@ const server = http.createServer(async (req, res) => {
2874
2320
  const email = await exchangeCodexOAuthCode(code, state);
2875
2321
  sendJSON(req, res, 200, { success: true, email });
2876
2322
  } catch (e) {
2877
- codexOAuthState = { status: 'error', error: e.message, email: null };
2878
- codexOAuthPending = null;
2879
2323
  sendJSON(req, res, 400, { error: e.message });
2880
2324
  }
2881
2325
  return;
@@ -2889,21 +2333,21 @@ const server = http.createServer(async (req, res) => {
2889
2333
 
2890
2334
  if (agentId === 'codex' || agentId === 'cli-codex') {
2891
2335
  try {
2892
- const result = await startCodexOAuth(req);
2336
+ const result = await startCodexOAuth(req, { PORT, BASE_URL });
2893
2337
  const conversationId = '__agent_auth__';
2894
2338
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
2895
2339
  broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening OpenAI OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
2896
2340
 
2897
2341
  const pollId = setInterval(() => {
2898
- if (codexOAuthState.status === 'success') {
2342
+ if (getCodexOAuthState().status === 'success') {
2899
2343
  clearInterval(pollId);
2900
- const email = codexOAuthState.email || '';
2344
+ const email = getCodexOAuthState().email || '';
2901
2345
  broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
2902
2346
  broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
2903
- } else if (codexOAuthState.status === 'error') {
2347
+ } else if (getCodexOAuthState().status === 'error') {
2904
2348
  clearInterval(pollId);
2905
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2906
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexOAuthState.error, timestamp: Date.now() });
2349
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getCodexOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2350
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getCodexOAuthState().error, timestamp: Date.now() });
2907
2351
  }
2908
2352
  }, 1000);
2909
2353
 
@@ -2920,21 +2364,21 @@ const server = http.createServer(async (req, res) => {
2920
2364
 
2921
2365
  if (agentId === 'gemini') {
2922
2366
  try {
2923
- const result = await startGeminiOAuth(req);
2367
+ const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
2924
2368
  const conversationId = '__agent_auth__';
2925
2369
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
2926
2370
  broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening Google OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
2927
2371
 
2928
2372
  const pollId = setInterval(() => {
2929
- if (geminiOAuthState.status === 'success') {
2373
+ if (getGeminiOAuthState().status === 'success') {
2930
2374
  clearInterval(pollId);
2931
- const email = geminiOAuthState.email || '';
2375
+ const email = getGeminiOAuthState().email || '';
2932
2376
  broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
2933
2377
  broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
2934
- } else if (geminiOAuthState.status === 'error') {
2378
+ } else if (getGeminiOAuthState().status === 'error') {
2935
2379
  clearInterval(pollId);
2936
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${geminiOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2937
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: geminiOAuthState.error, timestamp: Date.now() });
2380
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getGeminiOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2381
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getGeminiOAuthState().error, timestamp: Date.now() });
2938
2382
  }
2939
2383
  }, 1000);
2940
2384
 
@@ -4516,7 +3960,7 @@ console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.leng
4516
3960
  registerSessionHandlers(wsRouter, {
4517
3961
  db: queries, discoveredAgents, modelCache,
4518
3962
  getAgentDescriptor, activeScripts, broadcastSync,
4519
- startGeminiOAuth, geminiOAuthState: () => geminiOAuthState
3963
+ startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
4520
3964
  });
4521
3965
  console.log('[INIT] registerSessionHandlers completed');
4522
3966
 
@@ -4536,10 +3980,12 @@ registerScriptHandlers(wsRouter, {
4536
3980
  });
4537
3981
 
4538
3982
  registerOAuthHandlers(wsRouter, {
4539
- startGeminiOAuth, exchangeGeminiOAuthCode,
4540
- geminiOAuthState: () => geminiOAuthState,
4541
- startCodexOAuth, exchangeCodexOAuthCode,
4542
- codexOAuthState: () => codexOAuthState,
3983
+ startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }),
3984
+ exchangeGeminiOAuthCode,
3985
+ geminiOAuthState: getGeminiOAuthState,
3986
+ startCodexOAuth: (req) => startCodexOAuth(req, { PORT, BASE_URL }),
3987
+ exchangeCodexOAuthCode,
3988
+ codexOAuthState: getCodexOAuthState,
4543
3989
  });
4544
3990
 
4545
3991
  wsRouter.onLegacy((data, ws) => {