agentgui 1.0.701 → 1.0.702
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.
- package/CLAUDE.md +0 -27
- package/lib/ws-handlers-util.js +18 -33
- package/package.json +1 -1
- package/server.js +144 -344
- package/static/js/agent-auth.js +106 -0
package/CLAUDE.md
CHANGED
|
@@ -82,7 +82,6 @@ XState v5 machines provide formal state tracking alongside (not replacing) the e
|
|
|
82
82
|
- `BASE_URL` - URL prefix (default: /gm)
|
|
83
83
|
- `STARTUP_CWD` - Working directory passed to agents
|
|
84
84
|
- `HOT_RELOAD` - Set to "false" to disable watch mode
|
|
85
|
-
- `CODEX_HOME` - Override Codex CLI home directory (default: `~/.codex`)
|
|
86
85
|
|
|
87
86
|
## ACP Tool Lifecycle
|
|
88
87
|
|
|
@@ -124,11 +123,6 @@ All routes are prefixed with `BASE_URL` (default `/gm`).
|
|
|
124
123
|
- `GET /api/tools/:id/history` - Get tool install/update history (query: limit, offset)
|
|
125
124
|
- `POST /api/tools/update` - Batch update all tools with available updates
|
|
126
125
|
- `POST /api/tools/refresh-all` - Refresh all tool statuses from package manager
|
|
127
|
-
- `POST /api/codex-oauth/start` - Start Codex CLI OAuth flow (returns `{ authUrl, mode }`)
|
|
128
|
-
- `GET /api/codex-oauth/status` - Get current Codex OAuth state `{ status, email, error }`
|
|
129
|
-
- `POST /api/codex-oauth/relay` - Relay OAuth code+state from remote browser (body: `{ code, state }`)
|
|
130
|
-
- `POST /api/codex-oauth/complete` - Complete OAuth by pasting redirect URL (body: `{ url }`)
|
|
131
|
-
- `GET /codex-oauth2callback` - OAuth callback endpoint (redirect_uri for local flows)
|
|
132
126
|
|
|
133
127
|
## Tool Update System
|
|
134
128
|
|
|
@@ -292,27 +286,6 @@ Speech models (~470MB total) are downloaded automatically on server startup. No
|
|
|
292
286
|
- **Client init:** `loadAgents()`, `loadConversations()`, `checkSpeechStatus()` run in parallel via `Promise.all()`.
|
|
293
287
|
- **`perMessageDeflate: false`** on WebSocket server — msgpack binary doesn't compress well, and zlib was blocking the event loop on every streaming_progress send.
|
|
294
288
|
|
|
295
|
-
## Codex CLI OAuth
|
|
296
|
-
|
|
297
|
-
OpenAI Codex CLI uses PKCE authorization code flow against `https://auth.openai.com`.
|
|
298
|
-
|
|
299
|
-
**Flow:**
|
|
300
|
-
1. `POST /api/codex-oauth/start` generates PKCE (SHA-256 S256 challenge), CSRF state, returns `authUrl`
|
|
301
|
-
2. User opens `authUrl` in browser and authenticates via OpenAI/ChatGPT
|
|
302
|
-
3. **Local**: Browser redirects to `http://localhost:1455/auth/callback` — but since agentgui's server is on a different port, the redirect goes to `GET /codex-oauth2callback` (agentgui intercepts via matching route). Token exchange happens server-side.
|
|
303
|
-
4. **Remote**: Redirect goes to `/codex-oauth2callback` which serves a relay page. Relay POSTs `{ code, state }` to `/api/codex-oauth/relay`. Token exchange happens on the server.
|
|
304
|
-
5. Tokens saved to `$CODEX_HOME/auth.json` (default: `~/.codex/auth.json`) as `{ auth_mode: "chatgpt", tokens: { id_token, access_token, refresh_token }, last_refresh }`
|
|
305
|
-
|
|
306
|
-
**Constants (in server.js):**
|
|
307
|
-
- Issuer: `https://auth.openai.com`
|
|
308
|
-
- Client ID: `app_EMoamEEZ73f0CkXaXp7hrann`
|
|
309
|
-
- Scopes: `openid profile email offline_access api.connectors.read api.connectors.invoke`
|
|
310
|
-
- Redirect URI (local): `http://localhost:1455/auth/callback` (actual callback goes to agentgui's `/codex-oauth2callback`)
|
|
311
|
-
|
|
312
|
-
**WebSocket handlers** (in `lib/ws-handlers-util.js`): `codex.start`, `codex.status`, `codex.relay`, `codex.complete`
|
|
313
|
-
|
|
314
|
-
**Agent auth**: `POST /api/agents/codex/auth` starts OAuth flow same as Gemini — broadcasts `script_started`/`script_output`/`script_stopped` events as OAuth progresses.
|
|
315
|
-
|
|
316
289
|
## ACP SDK Integration
|
|
317
290
|
|
|
318
291
|
- **@agentclientprotocol/sdk** (`^0.4.1`) added to dependencies
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -9,7 +9,7 @@ export function register(router, deps) {
|
|
|
9
9
|
const { queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
|
|
10
10
|
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
11
11
|
startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
|
|
12
|
-
|
|
12
|
+
startCodexDeviceAuth, codexDeviceAuthState,
|
|
13
13
|
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents } = deps;
|
|
14
14
|
|
|
15
15
|
router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
|
|
@@ -158,46 +158,31 @@ export function register(router, deps) {
|
|
|
158
158
|
} catch (e) { err(400, e.message); }
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
router.handle('gemini.complete', async (p) => {
|
|
162
|
-
const pastedUrl = (p.url || '').trim();
|
|
163
|
-
if (!pastedUrl) err(400, 'No URL provided');
|
|
164
|
-
let parsed;
|
|
165
|
-
try { parsed = new URL(pastedUrl); } catch { err(400, 'Invalid URL. Paste the full URL from the browser address bar.'); }
|
|
166
|
-
const urlError = parsed.searchParams.get('error');
|
|
167
|
-
if (urlError) {
|
|
168
|
-
const desc = parsed.searchParams.get('error_description') || urlError;
|
|
169
|
-
return { error: desc };
|
|
170
|
-
}
|
|
171
|
-
const code = parsed.searchParams.get('code');
|
|
172
|
-
const state = parsed.searchParams.get('state');
|
|
173
|
-
try {
|
|
174
|
-
const email = await exchangeGeminiOAuthCode(code, state);
|
|
175
|
-
return { success: true, email };
|
|
176
|
-
} catch (e) { err(400, e.message); }
|
|
177
|
-
});
|
|
178
|
-
|
|
179
161
|
router.handle('codex.start', async () => {
|
|
180
162
|
try {
|
|
181
|
-
const result =
|
|
182
|
-
|
|
163
|
+
const result = startCodexDeviceAuth();
|
|
164
|
+
const st = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
|
|
165
|
+
if (result.alreadyAuthenticated) return { status: 'success', authenticated: true };
|
|
166
|
+
const waitForCode = () => new Promise((resolve) => {
|
|
167
|
+
let tries = 12;
|
|
168
|
+
const poll = () => {
|
|
169
|
+
const state = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
|
|
170
|
+
if (state.authUrl || tries-- <= 0) resolve(state);
|
|
171
|
+
else setTimeout(poll, 400);
|
|
172
|
+
};
|
|
173
|
+
poll();
|
|
174
|
+
});
|
|
175
|
+
const state = await waitForCode();
|
|
176
|
+
return { status: state.status, authUrl: state.authUrl, userCode: state.userCode };
|
|
183
177
|
} catch (e) { err(500, e.message); }
|
|
184
178
|
});
|
|
185
179
|
|
|
186
180
|
router.handle('codex.status', () => {
|
|
187
|
-
const st = typeof
|
|
181
|
+
const st = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
|
|
188
182
|
return st;
|
|
189
183
|
});
|
|
190
184
|
|
|
191
|
-
router.handle('
|
|
192
|
-
const { code, state } = p;
|
|
193
|
-
if (!code || !state) err(400, 'Missing code or state');
|
|
194
|
-
try {
|
|
195
|
-
const email = await exchangeCodexOAuthCode(code, state);
|
|
196
|
-
return { success: true, email };
|
|
197
|
-
} catch (e) { err(400, e.message); }
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
router.handle('codex.complete', async (p) => {
|
|
185
|
+
router.handle('gemini.complete', async (p) => {
|
|
201
186
|
const pastedUrl = (p.url || '').trim();
|
|
202
187
|
if (!pastedUrl) err(400, 'No URL provided');
|
|
203
188
|
let parsed;
|
|
@@ -210,7 +195,7 @@ export function register(router, deps) {
|
|
|
210
195
|
const code = parsed.searchParams.get('code');
|
|
211
196
|
const state = parsed.searchParams.get('state');
|
|
212
197
|
try {
|
|
213
|
-
const email = await
|
|
198
|
+
const email = await exchangeGeminiOAuthCode(code, state);
|
|
214
199
|
return { success: true, email };
|
|
215
200
|
} catch (e) { err(400, e.message); }
|
|
216
201
|
});
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -696,6 +696,96 @@ const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
|
|
|
696
696
|
let geminiOAuthState = { status: 'idle', error: null, email: null };
|
|
697
697
|
let geminiOAuthPending = null;
|
|
698
698
|
|
|
699
|
+
const CODEX_AUTH_FILE = path.join(os.homedir(), '.codex', 'auth.json');
|
|
700
|
+
let codexDeviceAuthState = { status: 'idle', error: null, userCode: null, authUrl: null };
|
|
701
|
+
let codexDeviceAuthProcess = null;
|
|
702
|
+
|
|
703
|
+
function getCodexAuthStatus() {
|
|
704
|
+
try {
|
|
705
|
+
if (fs.existsSync(CODEX_AUTH_FILE)) {
|
|
706
|
+
const creds = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf-8'));
|
|
707
|
+
if (creds.auth_mode === 'oauth' || creds.access_token || creds.refresh_token) {
|
|
708
|
+
return { authenticated: true, detail: creds.email || 'oauth' };
|
|
709
|
+
}
|
|
710
|
+
if (creds.auth_mode === 'apikey' && creds.OPENAI_API_KEY) {
|
|
711
|
+
return { authenticated: true, detail: 'api-key' };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} catch (_) {}
|
|
715
|
+
return { authenticated: false, detail: 'no credentials' };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function startCodexDeviceAuth() {
|
|
719
|
+
if (codexDeviceAuthProcess) {
|
|
720
|
+
return { alreadyRunning: true };
|
|
721
|
+
}
|
|
722
|
+
const codexStatus = getCodexAuthStatus();
|
|
723
|
+
if (codexStatus.authenticated) {
|
|
724
|
+
codexDeviceAuthState = { status: 'success', error: null, userCode: null, authUrl: null };
|
|
725
|
+
return { alreadyAuthenticated: true };
|
|
726
|
+
}
|
|
727
|
+
codexDeviceAuthState = { status: 'pending', error: null, userCode: null, authUrl: null };
|
|
728
|
+
const child = spawn('npx', ['@openai/codex', 'login', '--device-auth'], {
|
|
729
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
730
|
+
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
731
|
+
shell: os.platform() === 'win32',
|
|
732
|
+
});
|
|
733
|
+
codexDeviceAuthProcess = child;
|
|
734
|
+
|
|
735
|
+
const onData = (chunk) => {
|
|
736
|
+
const text = chunk.toString().replace(/\x1b\[[0-9;]*m/g, '');
|
|
737
|
+
const urlMatch = text.match(/https:\/\/auth\.openai\.com\/codex\/device[^\s]*/);
|
|
738
|
+
const codeMatch = text.match(/\b([0-9A-Z]{4}-[0-9A-Z]{5})\b/);
|
|
739
|
+
if (urlMatch && !codexDeviceAuthState.authUrl) codexDeviceAuthState.authUrl = urlMatch[0];
|
|
740
|
+
if (codeMatch && !codexDeviceAuthState.userCode) codexDeviceAuthState.userCode = codeMatch[1];
|
|
741
|
+
};
|
|
742
|
+
child.stdout.on('data', onData);
|
|
743
|
+
child.stderr.on('data', onData);
|
|
744
|
+
|
|
745
|
+
const authFileWatcher = fs.watchFile ? null : null;
|
|
746
|
+
const pollInterval = setInterval(() => {
|
|
747
|
+
const st = getCodexAuthStatus();
|
|
748
|
+
if (st.authenticated) {
|
|
749
|
+
clearInterval(pollInterval);
|
|
750
|
+
clearTimeout(timeoutId);
|
|
751
|
+
codexDeviceAuthState = { status: 'success', error: null, userCode: codexDeviceAuthState.userCode, authUrl: codexDeviceAuthState.authUrl };
|
|
752
|
+
if (codexDeviceAuthProcess) { try { codexDeviceAuthProcess.kill('SIGTERM'); } catch (_) {} codexDeviceAuthProcess = null; }
|
|
753
|
+
}
|
|
754
|
+
}, 1500);
|
|
755
|
+
|
|
756
|
+
const timeoutId = setTimeout(() => {
|
|
757
|
+
clearInterval(pollInterval);
|
|
758
|
+
if (codexDeviceAuthState.status === 'pending') {
|
|
759
|
+
codexDeviceAuthState = { status: 'error', error: 'Authentication timed out', userCode: null, authUrl: null };
|
|
760
|
+
}
|
|
761
|
+
if (codexDeviceAuthProcess) { try { codexDeviceAuthProcess.kill('SIGTERM'); } catch (_) {} codexDeviceAuthProcess = null; }
|
|
762
|
+
}, 5 * 60 * 1000);
|
|
763
|
+
|
|
764
|
+
child.on('error', (e) => {
|
|
765
|
+
clearInterval(pollInterval);
|
|
766
|
+
clearTimeout(timeoutId);
|
|
767
|
+
codexDeviceAuthState = { status: 'error', error: e.message, userCode: null, authUrl: null };
|
|
768
|
+
codexDeviceAuthProcess = null;
|
|
769
|
+
});
|
|
770
|
+
child.on('close', (code) => {
|
|
771
|
+
clearInterval(pollInterval);
|
|
772
|
+
clearTimeout(timeoutId);
|
|
773
|
+
codexDeviceAuthProcess = null;
|
|
774
|
+
if (codexDeviceAuthState.status === 'pending') {
|
|
775
|
+
if (code === 0) {
|
|
776
|
+
const st = getCodexAuthStatus();
|
|
777
|
+
codexDeviceAuthState = st.authenticated
|
|
778
|
+
? { status: 'success', error: null, userCode: codexDeviceAuthState.userCode, authUrl: codexDeviceAuthState.authUrl }
|
|
779
|
+
: { status: 'error', error: 'Process exited without saving credentials', userCode: null, authUrl: null };
|
|
780
|
+
} else {
|
|
781
|
+
codexDeviceAuthState = { status: 'error', error: 'Authentication cancelled', userCode: null, authUrl: null };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
return { started: true };
|
|
787
|
+
}
|
|
788
|
+
|
|
699
789
|
function buildBaseUrl(req) {
|
|
700
790
|
const override = process.env.AGENTGUI_BASE_URL;
|
|
701
791
|
if (override) return override.replace(/\/+$/, '');
|
|
@@ -949,238 +1039,6 @@ function getGeminiOAuthStatus() {
|
|
|
949
1039
|
return null;
|
|
950
1040
|
}
|
|
951
1041
|
|
|
952
|
-
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
953
|
-
const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
|
|
954
|
-
const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
|
|
955
|
-
const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
956
|
-
const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
|
|
957
|
-
const CODEX_OAUTH_PORT = 1455;
|
|
958
|
-
|
|
959
|
-
let codexOAuthState = { status: 'idle', error: null, email: null };
|
|
960
|
-
let codexOAuthPending = null;
|
|
961
|
-
|
|
962
|
-
function generatePkce() {
|
|
963
|
-
const verifierBytes = crypto.randomBytes(64);
|
|
964
|
-
const codeVerifier = verifierBytes.toString('base64url');
|
|
965
|
-
const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
|
|
966
|
-
const codeChallenge = challengeBytes.toString('base64url');
|
|
967
|
-
return { codeVerifier, codeChallenge };
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function parseJwtEmail(jwt) {
|
|
971
|
-
try {
|
|
972
|
-
const parts = jwt.split('.');
|
|
973
|
-
if (parts.length < 2) return '';
|
|
974
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
975
|
-
return payload.email || payload['https://api.openai.com/profile']?.email || '';
|
|
976
|
-
} catch (_) { return ''; }
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function saveCodexCredentials(tokens) {
|
|
980
|
-
if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
|
|
981
|
-
const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
|
|
982
|
-
fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
|
|
983
|
-
try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function getCodexOAuthStatus() {
|
|
987
|
-
try {
|
|
988
|
-
if (fs.existsSync(CODEX_AUTH_FILE)) {
|
|
989
|
-
const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
|
|
990
|
-
if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
|
|
991
|
-
const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
|
|
992
|
-
return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
} catch (_) {}
|
|
996
|
-
return null;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
async function startCodexOAuth(req) {
|
|
1000
|
-
const remote = isRemoteRequest(req);
|
|
1001
|
-
const redirectUri = remote
|
|
1002
|
-
? `${buildBaseUrl(req)}${BASE_URL}/codex-oauth2callback`
|
|
1003
|
-
: `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
|
|
1004
|
-
|
|
1005
|
-
const pkce = generatePkce();
|
|
1006
|
-
const csrfToken = crypto.randomBytes(32).toString('hex');
|
|
1007
|
-
const relayUrl = remote ? `${buildBaseUrl(req)}${BASE_URL}/api/codex-oauth/relay` : null;
|
|
1008
|
-
const state = encodeOAuthState(csrfToken, relayUrl);
|
|
1009
|
-
|
|
1010
|
-
const params = new URLSearchParams({
|
|
1011
|
-
response_type: 'code',
|
|
1012
|
-
client_id: CODEX_CLIENT_ID,
|
|
1013
|
-
redirect_uri: redirectUri,
|
|
1014
|
-
scope: CODEX_SCOPES,
|
|
1015
|
-
code_challenge: pkce.codeChallenge,
|
|
1016
|
-
code_challenge_method: 'S256',
|
|
1017
|
-
id_token_add_organizations: 'true',
|
|
1018
|
-
codex_cli_simplified_flow: 'true',
|
|
1019
|
-
state,
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
|
|
1023
|
-
const mode = remote ? 'remote' : 'local';
|
|
1024
|
-
|
|
1025
|
-
codexOAuthPending = { pkce, redirectUri, state: csrfToken };
|
|
1026
|
-
codexOAuthState = { status: 'pending', error: null, email: null };
|
|
1027
|
-
|
|
1028
|
-
setTimeout(() => {
|
|
1029
|
-
if (codexOAuthState.status === 'pending') {
|
|
1030
|
-
codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
|
|
1031
|
-
codexOAuthPending = null;
|
|
1032
|
-
}
|
|
1033
|
-
}, 5 * 60 * 1000);
|
|
1034
|
-
|
|
1035
|
-
return { authUrl, mode };
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
async function exchangeCodexOAuthCode(code, stateParam) {
|
|
1039
|
-
if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
1040
|
-
|
|
1041
|
-
const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
|
|
1042
|
-
const { csrfToken } = decodeOAuthState(stateParam);
|
|
1043
|
-
|
|
1044
|
-
if (csrfToken !== expectedCsrf) {
|
|
1045
|
-
codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
1046
|
-
codexOAuthPending = null;
|
|
1047
|
-
throw new Error('State mismatch - possible CSRF attack.');
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
if (!code) {
|
|
1051
|
-
codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
|
|
1052
|
-
codexOAuthPending = null;
|
|
1053
|
-
throw new Error('No authorization code received.');
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
const body = new URLSearchParams({
|
|
1057
|
-
grant_type: 'authorization_code',
|
|
1058
|
-
code,
|
|
1059
|
-
redirect_uri: redirectUri,
|
|
1060
|
-
client_id: CODEX_CLIENT_ID,
|
|
1061
|
-
code_verifier: pkce.codeVerifier,
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
|
|
1065
|
-
method: 'POST',
|
|
1066
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1067
|
-
body: body.toString(),
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
if (!resp.ok) {
|
|
1071
|
-
const text = await resp.text();
|
|
1072
|
-
codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
|
|
1073
|
-
codexOAuthPending = null;
|
|
1074
|
-
throw new Error(`Token exchange failed (${resp.status}): ${text}`);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
const tokens = await resp.json();
|
|
1078
|
-
const email = parseJwtEmail(tokens.id_token || '');
|
|
1079
|
-
|
|
1080
|
-
saveCodexCredentials(tokens);
|
|
1081
|
-
codexOAuthState = { status: 'success', error: null, email };
|
|
1082
|
-
codexOAuthPending = null;
|
|
1083
|
-
|
|
1084
|
-
return email;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
function codexOAuthResultPage(title, message, success) {
|
|
1088
|
-
const color = success ? '#10b981' : '#ef4444';
|
|
1089
|
-
const icon = success ? '✓' : '✗';
|
|
1090
|
-
return `<!DOCTYPE html><html><head><title>${title}</title></head>
|
|
1091
|
-
<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;">
|
|
1092
|
-
<div style="text-align:center;max-width:400px;padding:2rem;">
|
|
1093
|
-
<div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
|
|
1094
|
-
<h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
|
|
1095
|
-
<p style="color:#9ca3af;">${message}</p>
|
|
1096
|
-
<p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
|
|
1097
|
-
</div></body></html>`;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function codexOAuthRelayPage(code, state, error) {
|
|
1101
|
-
const stateData = decodeOAuthState(state || '');
|
|
1102
|
-
const relayUrl = stateData.relayUrl || '';
|
|
1103
|
-
const escapedCode = (code || '').replace(/['"\\]/g, '');
|
|
1104
|
-
const escapedState = (state || '').replace(/['"\\]/g, '');
|
|
1105
|
-
const escapedError = (error || '').replace(/['"\\]/g, '');
|
|
1106
|
-
const escapedRelay = relayUrl.replace(/['"\\]/g, '');
|
|
1107
|
-
return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
|
|
1108
|
-
<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;">
|
|
1109
|
-
<div id="status" style="text-align:center;max-width:400px;padding:2rem;">
|
|
1110
|
-
<div id="spinner" style="font-size:2rem;margin-bottom:1rem;">⌛</div>
|
|
1111
|
-
<h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
|
|
1112
|
-
<p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
|
|
1113
|
-
</div>
|
|
1114
|
-
<script>
|
|
1115
|
-
(function() {
|
|
1116
|
-
var code = '${escapedCode}';
|
|
1117
|
-
var state = '${escapedState}';
|
|
1118
|
-
var error = '${escapedError}';
|
|
1119
|
-
var relayUrl = '${escapedRelay}';
|
|
1120
|
-
function show(icon, title, msg, color) {
|
|
1121
|
-
document.getElementById('spinner').textContent = icon;
|
|
1122
|
-
document.getElementById('spinner').style.color = color;
|
|
1123
|
-
document.getElementById('title').textContent = title;
|
|
1124
|
-
document.getElementById('msg').textContent = msg;
|
|
1125
|
-
}
|
|
1126
|
-
if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
|
|
1127
|
-
if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
|
|
1128
|
-
if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
|
|
1129
|
-
fetch(relayUrl, {
|
|
1130
|
-
method: 'POST',
|
|
1131
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1132
|
-
body: JSON.stringify({ code: code, state: state })
|
|
1133
|
-
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
1134
|
-
if (data.success) {
|
|
1135
|
-
show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
|
|
1136
|
-
} else {
|
|
1137
|
-
show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
|
|
1138
|
-
}
|
|
1139
|
-
}).catch(function(e) {
|
|
1140
|
-
show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
|
|
1141
|
-
});
|
|
1142
|
-
})();
|
|
1143
|
-
</script>
|
|
1144
|
-
</body></html>`;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
async function handleCodexOAuthCallback(req, res) {
|
|
1148
|
-
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
1149
|
-
const code = reqUrl.searchParams.get('code');
|
|
1150
|
-
const state = reqUrl.searchParams.get('state');
|
|
1151
|
-
const error = reqUrl.searchParams.get('error');
|
|
1152
|
-
const errorDesc = reqUrl.searchParams.get('error_description');
|
|
1153
|
-
|
|
1154
|
-
if (error) {
|
|
1155
|
-
const desc = errorDesc || error;
|
|
1156
|
-
codexOAuthState = { status: 'error', error: desc, email: null };
|
|
1157
|
-
codexOAuthPending = null;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
const stateData = decodeOAuthState(state || '');
|
|
1161
|
-
if (stateData.relayUrl) {
|
|
1162
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1163
|
-
res.end(codexOAuthRelayPage(code, state, errorDesc || error));
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
if (!codexOAuthPending) {
|
|
1168
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1169
|
-
res.end(codexOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
try {
|
|
1174
|
-
if (error) throw new Error(errorDesc || error);
|
|
1175
|
-
const email = await exchangeCodexOAuthCode(code, state);
|
|
1176
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1177
|
-
res.end(codexOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
|
|
1178
|
-
} catch (e) {
|
|
1179
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1180
|
-
res.end(codexOAuthResultPage('Authentication Failed', e.message, false));
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
1042
|
const PROVIDER_CONFIGS = {
|
|
1185
1043
|
'anthropic': {
|
|
1186
1044
|
name: 'Anthropic', configPaths: [
|
|
@@ -1269,13 +1127,6 @@ function getProviderConfigs() {
|
|
|
1269
1127
|
continue;
|
|
1270
1128
|
}
|
|
1271
1129
|
}
|
|
1272
|
-
if (providerId === 'codex') {
|
|
1273
|
-
const oauthStatus = getCodexOAuthStatus();
|
|
1274
|
-
if (oauthStatus) {
|
|
1275
|
-
configs[providerId] = { name: config.name, ...oauthStatus };
|
|
1276
|
-
continue;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
1130
|
for (const configPath of config.configPaths) {
|
|
1280
1131
|
try {
|
|
1281
1132
|
if (fs.existsSync(configPath)) {
|
|
@@ -1406,11 +1257,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1406
1257
|
return;
|
|
1407
1258
|
}
|
|
1408
1259
|
|
|
1409
|
-
if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
|
|
1410
|
-
await handleCodexOAuthCallback(req, res);
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
1260
|
if (pathOnly === '/api/conversations' && req.method === 'GET') {
|
|
1415
1261
|
const conversations = queries.getConversationsList();
|
|
1416
1262
|
// Filter out stale streaming state using a single bulk query instead of N+1 per-conversation queries
|
|
@@ -2421,6 +2267,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2421
2267
|
} else {
|
|
2422
2268
|
status.detail = 'no credentials';
|
|
2423
2269
|
}
|
|
2270
|
+
} else if (agent.id === 'codex') {
|
|
2271
|
+
const codexSt = getCodexAuthStatus();
|
|
2272
|
+
status.authenticated = codexSt.authenticated;
|
|
2273
|
+
status.detail = codexSt.detail;
|
|
2424
2274
|
} else {
|
|
2425
2275
|
status.detail = 'unknown';
|
|
2426
2276
|
}
|
|
@@ -2866,110 +2716,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
2866
2716
|
return;
|
|
2867
2717
|
}
|
|
2868
2718
|
|
|
2869
|
-
if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
|
|
2870
|
-
try {
|
|
2871
|
-
const result = await startCodexOAuth(req);
|
|
2872
|
-
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
2873
|
-
} catch (e) {
|
|
2874
|
-
console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
|
|
2875
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
2876
|
-
}
|
|
2877
|
-
return;
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
|
|
2881
|
-
sendJSON(req, res, 200, codexOAuthState);
|
|
2882
|
-
return;
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
if (pathOnly === '/api/codex-oauth/relay' && req.method === 'POST') {
|
|
2886
|
-
try {
|
|
2887
|
-
const body = await parseBody(req);
|
|
2888
|
-
const { code, state: stateParam } = body;
|
|
2889
|
-
if (!code || !stateParam) {
|
|
2890
|
-
sendJSON(req, res, 400, { error: 'Missing code or state' });
|
|
2891
|
-
return;
|
|
2892
|
-
}
|
|
2893
|
-
const email = await exchangeCodexOAuthCode(code, stateParam);
|
|
2894
|
-
sendJSON(req, res, 200, { success: true, email });
|
|
2895
|
-
} catch (e) {
|
|
2896
|
-
codexOAuthState = { status: 'error', error: e.message, email: null };
|
|
2897
|
-
codexOAuthPending = null;
|
|
2898
|
-
sendJSON(req, res, 400, { error: e.message });
|
|
2899
|
-
}
|
|
2900
|
-
return;
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
if (pathOnly === '/api/codex-oauth/complete' && req.method === 'POST') {
|
|
2904
|
-
try {
|
|
2905
|
-
const body = await parseBody(req);
|
|
2906
|
-
const pastedUrl = (body.url || '').trim();
|
|
2907
|
-
if (!pastedUrl) {
|
|
2908
|
-
sendJSON(req, res, 400, { error: 'No URL provided' });
|
|
2909
|
-
return;
|
|
2910
|
-
}
|
|
2911
|
-
let parsed;
|
|
2912
|
-
try { parsed = new URL(pastedUrl); } catch (_) {
|
|
2913
|
-
sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' });
|
|
2914
|
-
return;
|
|
2915
|
-
}
|
|
2916
|
-
const error = parsed.searchParams.get('error');
|
|
2917
|
-
if (error) {
|
|
2918
|
-
const desc = parsed.searchParams.get('error_description') || error;
|
|
2919
|
-
codexOAuthState = { status: 'error', error: desc, email: null };
|
|
2920
|
-
codexOAuthPending = null;
|
|
2921
|
-
sendJSON(req, res, 200, { error: desc });
|
|
2922
|
-
return;
|
|
2923
|
-
}
|
|
2924
|
-
const code = parsed.searchParams.get('code');
|
|
2925
|
-
const state = parsed.searchParams.get('state');
|
|
2926
|
-
const email = await exchangeCodexOAuthCode(code, state);
|
|
2927
|
-
sendJSON(req, res, 200, { success: true, email });
|
|
2928
|
-
} catch (e) {
|
|
2929
|
-
codexOAuthState = { status: 'error', error: e.message, email: null };
|
|
2930
|
-
codexOAuthPending = null;
|
|
2931
|
-
sendJSON(req, res, 400, { error: e.message });
|
|
2932
|
-
}
|
|
2933
|
-
return;
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
2719
|
const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
|
|
2937
2720
|
if (agentAuthMatch && req.method === 'POST') {
|
|
2938
2721
|
const agentId = agentAuthMatch[1];
|
|
2939
2722
|
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
2940
2723
|
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
|
|
2941
2724
|
|
|
2942
|
-
if (agentId === 'codex' || agentId === 'cli-codex') {
|
|
2943
|
-
try {
|
|
2944
|
-
const result = await startCodexOAuth(req);
|
|
2945
|
-
const conversationId = '__agent_auth__';
|
|
2946
|
-
broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
|
|
2947
|
-
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() });
|
|
2948
|
-
|
|
2949
|
-
const pollId = setInterval(() => {
|
|
2950
|
-
if (codexOAuthState.status === 'success') {
|
|
2951
|
-
clearInterval(pollId);
|
|
2952
|
-
const email = codexOAuthState.email || '';
|
|
2953
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
2954
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
2955
|
-
} else if (codexOAuthState.status === 'error') {
|
|
2956
|
-
clearInterval(pollId);
|
|
2957
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
2958
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexOAuthState.error, timestamp: Date.now() });
|
|
2959
|
-
}
|
|
2960
|
-
}, 1000);
|
|
2961
|
-
|
|
2962
|
-
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
|
|
2963
|
-
|
|
2964
|
-
sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
|
|
2965
|
-
return;
|
|
2966
|
-
} catch (e) {
|
|
2967
|
-
console.error('[codex-oauth] /api/agents/codex/auth failed:', e);
|
|
2968
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
2969
|
-
return;
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
2725
|
if (agentId === 'gemini') {
|
|
2974
2726
|
try {
|
|
2975
2727
|
const result = await startGeminiOAuth(req);
|
|
@@ -3001,6 +2753,55 @@ const server = http.createServer(async (req, res) => {
|
|
|
3001
2753
|
}
|
|
3002
2754
|
}
|
|
3003
2755
|
|
|
2756
|
+
if (agentId === 'codex') {
|
|
2757
|
+
const conversationId = '__agent_auth__';
|
|
2758
|
+
const result = startCodexDeviceAuth();
|
|
2759
|
+
if (result.alreadyRunning) {
|
|
2760
|
+
sendJSON(req, res, 200, { ok: true, agentId, authUrl: codexDeviceAuthState.authUrl, userCode: codexDeviceAuthState.userCode, status: 'pending' });
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
if (result.alreadyAuthenticated) {
|
|
2764
|
+
sendJSON(req, res, 200, { ok: true, agentId, status: 'success' });
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
|
|
2768
|
+
broadcastSync({ type: 'script_output', conversationId, data: '\x1b[36mStarting Codex device authorization...\x1b[0m\r\n', stream: 'stdout', timestamp: Date.now() });
|
|
2769
|
+
|
|
2770
|
+
const waitForCode = (tries) => {
|
|
2771
|
+
if (tries <= 0) return;
|
|
2772
|
+
if (codexDeviceAuthState.authUrl && codexDeviceAuthState.userCode) {
|
|
2773
|
+
const msg = `\r\nVisit: ${codexDeviceAuthState.authUrl}\r\nEnter code: ${codexDeviceAuthState.userCode}\r\n`;
|
|
2774
|
+
broadcastSync({ type: 'script_output', conversationId, data: msg, stream: 'stdout', timestamp: Date.now() });
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
setTimeout(() => waitForCode(tries - 1), 500);
|
|
2778
|
+
};
|
|
2779
|
+
waitForCode(10);
|
|
2780
|
+
|
|
2781
|
+
const pollId = setInterval(() => {
|
|
2782
|
+
if (codexDeviceAuthState.status === 'success') {
|
|
2783
|
+
clearInterval(pollId);
|
|
2784
|
+
broadcastSync({ type: 'script_output', conversationId, data: '\r\n\x1b[32mCodex authentication successful\x1b[0m\r\n', stream: 'stdout', timestamp: Date.now() });
|
|
2785
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
2786
|
+
} else if (codexDeviceAuthState.status === 'error') {
|
|
2787
|
+
clearInterval(pollId);
|
|
2788
|
+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexDeviceAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
2789
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexDeviceAuthState.error, timestamp: Date.now() });
|
|
2790
|
+
}
|
|
2791
|
+
}, 1500);
|
|
2792
|
+
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000 + 5000);
|
|
2793
|
+
|
|
2794
|
+
const waitForInfo = (tries) => {
|
|
2795
|
+
if (codexDeviceAuthState.authUrl || tries <= 0) {
|
|
2796
|
+
sendJSON(req, res, 200, { ok: true, agentId, authUrl: codexDeviceAuthState.authUrl, userCode: codexDeviceAuthState.userCode, status: codexDeviceAuthState.status });
|
|
2797
|
+
} else {
|
|
2798
|
+
setTimeout(() => waitForInfo(tries - 1), 400);
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
waitForInfo(12);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
3004
2805
|
const authCommands = {
|
|
3005
2806
|
'claude-code': { cmd: 'claude', args: ['setup-token'] },
|
|
3006
2807
|
'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
|
|
@@ -4532,8 +4333,7 @@ registerUtilHandlers(wsRouter, {
|
|
|
4532
4333
|
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
4533
4334
|
startGeminiOAuth, exchangeGeminiOAuthCode,
|
|
4534
4335
|
geminiOAuthState: () => geminiOAuthState,
|
|
4535
|
-
|
|
4536
|
-
codexOAuthState: () => codexOAuthState,
|
|
4336
|
+
startCodexDeviceAuth, codexDeviceAuthState: () => codexDeviceAuthState,
|
|
4537
4337
|
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents
|
|
4538
4338
|
});
|
|
4539
4339
|
|
package/static/js/agent-auth.js
CHANGED
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
|
|
129
129
|
|
|
130
130
|
var oauthPollInterval = null, oauthPollTimeout = null, oauthFallbackTimer = null;
|
|
131
|
+
var codexPollInterval = null, codexPollTimeout = null;
|
|
131
132
|
|
|
132
133
|
function cleanupOAuthPolling() {
|
|
133
134
|
if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
|
|
@@ -135,6 +136,96 @@
|
|
|
135
136
|
if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
function cleanupCodexPolling() {
|
|
140
|
+
if (codexPollInterval) { clearInterval(codexPollInterval); codexPollInterval = null; }
|
|
141
|
+
if (codexPollTimeout) { clearTimeout(codexPollTimeout); codexPollTimeout = null; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function removeCodexModal() {
|
|
145
|
+
var el = document.getElementById('codexDeviceModal');
|
|
146
|
+
if (el) el.remove();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function showCodexDeviceModal(authUrl, userCode) {
|
|
150
|
+
removeCodexModal();
|
|
151
|
+
var overlay = document.createElement('div');
|
|
152
|
+
overlay.id = 'codexDeviceModal';
|
|
153
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
|
154
|
+
var displayUrl = authUrl || 'https://auth.openai.com/codex/device';
|
|
155
|
+
var displayCode = userCode || '';
|
|
156
|
+
overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:30rem;width:calc(100% - 2rem);box-shadow:0 25px 50px rgba(0,0,0,0.5);color:var(--color-text-primary,white);font-family:system-ui,sans-serif;" onclick="event.stopPropagation()">' +
|
|
157
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;">' +
|
|
158
|
+
'<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Codex Sign-In</h2>' +
|
|
159
|
+
'<button id="codexModalClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
|
|
160
|
+
'<div id="codexModalContent" style="text-align:center;">' +
|
|
161
|
+
'<div style="font-size:2rem;margin-bottom:1rem;animation:pulse 2s infinite;">⏳</div>' +
|
|
162
|
+
'<p style="font-size:0.85rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 1rem;">Follow these steps to sign in with your OpenAI account:</p>' +
|
|
163
|
+
'<div style="margin-bottom:1rem;">' +
|
|
164
|
+
'<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0 0 0.5rem;text-align:left;">1. Open this link in your browser:</p>' +
|
|
165
|
+
'<div style="display:flex;gap:0.5rem;align-items:center;">' +
|
|
166
|
+
'<a href="' + esc(displayUrl) + '" target="_blank" style="flex:1;padding:0.5rem 0.75rem;background:var(--color-primary,#3b82f6);color:white;text-decoration:none;border-radius:0.375rem;font-size:0.8rem;font-weight:600;text-align:center;">Open Sign-In Page</a>' +
|
|
167
|
+
'</div></div>' +
|
|
168
|
+
'<div style="margin-bottom:1.25rem;">' +
|
|
169
|
+
'<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0 0 0.5rem;text-align:left;">2. Enter this one-time code:</p>' +
|
|
170
|
+
'<div style="display:flex;gap:0.5rem;align-items:center;">' +
|
|
171
|
+
'<div id="codexUserCode" style="flex:1;padding:0.625rem;background:var(--color-bg-primary,#111827);border:2px solid var(--color-primary,#3b82f6);border-radius:0.5rem;font-family:monospace;font-size:1.25rem;font-weight:700;letter-spacing:0.15em;text-align:center;">' + esc(displayCode) + '</div>' +
|
|
172
|
+
'<button id="codexCopyBtn" style="padding:0.5rem 0.75rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.375rem;color:var(--color-text-primary,white);font-size:0.75rem;cursor:pointer;flex-shrink:0;">Copy</button>' +
|
|
173
|
+
'</div></div>' +
|
|
174
|
+
'<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0;">This dialog will close automatically when sign-in completes.</p>' +
|
|
175
|
+
'</div>' +
|
|
176
|
+
'<div id="codexModalSuccess" style="display:none;text-align:center;padding:1rem 0;">' +
|
|
177
|
+
'<div style="font-size:3rem;color:#10b981;margin-bottom:0.75rem;">✓</div>' +
|
|
178
|
+
'<p style="font-weight:600;margin:0 0 0.25rem;">Authentication Successful</p>' +
|
|
179
|
+
'<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0;">Codex CLI is now authenticated.</p>' +
|
|
180
|
+
'</div>' +
|
|
181
|
+
'<div style="margin-top:1.25rem;">' +
|
|
182
|
+
'<button id="codexModalCancel" style="width:100%;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button></div>' +
|
|
183
|
+
'<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}</style></div>';
|
|
184
|
+
document.body.appendChild(overlay);
|
|
185
|
+
|
|
186
|
+
var dismiss = function() { cleanupCodexPolling(); authRunning = false; removeCodexModal(); };
|
|
187
|
+
document.getElementById('codexModalClose').addEventListener('click', dismiss);
|
|
188
|
+
document.getElementById('codexModalCancel').addEventListener('click', dismiss);
|
|
189
|
+
|
|
190
|
+
var copyBtn = document.getElementById('codexCopyBtn');
|
|
191
|
+
if (copyBtn && displayCode) {
|
|
192
|
+
copyBtn.addEventListener('click', function(e) {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
navigator.clipboard.writeText(displayCode).then(function() {
|
|
195
|
+
copyBtn.textContent = 'Copied!';
|
|
196
|
+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
|
|
197
|
+
}).catch(function() {});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
cleanupCodexPolling();
|
|
202
|
+
codexPollInterval = setInterval(function() {
|
|
203
|
+
window.wsClient.rpc('codex.status')
|
|
204
|
+
.then(function(st) {
|
|
205
|
+
if (st.status === 'success') {
|
|
206
|
+
cleanupCodexPolling();
|
|
207
|
+
authRunning = false;
|
|
208
|
+
var content = document.getElementById('codexModalContent');
|
|
209
|
+
var success = document.getElementById('codexModalSuccess');
|
|
210
|
+
var cancel = document.getElementById('codexModalCancel');
|
|
211
|
+
if (content) content.style.display = 'none';
|
|
212
|
+
if (success) success.style.display = 'block';
|
|
213
|
+
if (cancel) cancel.textContent = 'Close';
|
|
214
|
+
setTimeout(function() { removeCodexModal(); refresh(); }, 2500);
|
|
215
|
+
} else if (st.status === 'error') {
|
|
216
|
+
cleanupCodexPolling();
|
|
217
|
+
authRunning = false;
|
|
218
|
+
removeCodexModal();
|
|
219
|
+
refresh();
|
|
220
|
+
}
|
|
221
|
+
}).catch(function() {});
|
|
222
|
+
}, 1500);
|
|
223
|
+
codexPollTimeout = setTimeout(function() {
|
|
224
|
+
cleanupCodexPolling();
|
|
225
|
+
if (authRunning) { authRunning = false; removeCodexModal(); }
|
|
226
|
+
}, 5 * 60 * 1000);
|
|
227
|
+
}
|
|
228
|
+
|
|
138
229
|
function showOAuthWaitingModal() {
|
|
139
230
|
removeOAuthModal();
|
|
140
231
|
var overlay = document.createElement('div');
|
|
@@ -217,6 +308,19 @@
|
|
|
217
308
|
|
|
218
309
|
function triggerAuth(agentId) {
|
|
219
310
|
if (authRunning) return;
|
|
311
|
+
if (agentId === 'codex') {
|
|
312
|
+
authRunning = true;
|
|
313
|
+
window.wsClient.rpc('codex.start')
|
|
314
|
+
.then(function(data) {
|
|
315
|
+
if (data.status === 'success') {
|
|
316
|
+
authRunning = false;
|
|
317
|
+
refresh();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
showCodexDeviceModal(data.authUrl, data.userCode);
|
|
321
|
+
}).catch(function() { authRunning = false; });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
220
324
|
window.wsClient.rpc('agent.auth', { id: agentId })
|
|
221
325
|
.then(function(data) {
|
|
222
326
|
if (data.ok) {
|
|
@@ -271,6 +375,8 @@
|
|
|
271
375
|
authRunning = false;
|
|
272
376
|
removeOAuthModal();
|
|
273
377
|
cleanupOAuthPolling();
|
|
378
|
+
cleanupCodexPolling();
|
|
379
|
+
removeCodexModal();
|
|
274
380
|
var term = getTerminal();
|
|
275
381
|
var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
|
|
276
382
|
if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
|