agentgui 1.0.702 → 1.0.703
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 +27 -0
- package/lib/ws-handlers-util.js +33 -18
- package/package.json +1 -1
- package/server.js +344 -144
- package/static/js/agent-auth.js +0 -106
- package/static/js/client.js +0 -7
- package/acp-queries.js +0 -182
- package/static/js/codec.js +0 -2
- package/static/js/sync-debug.js +0 -146
- package/test-cli-detection.mjs +0 -37
- package/test-fixes.mjs +0 -103
- package/test-thread-steering.mjs +0 -100
package/server.js
CHANGED
|
@@ -696,96 +696,6 @@ 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
|
-
|
|
789
699
|
function buildBaseUrl(req) {
|
|
790
700
|
const override = process.env.AGENTGUI_BASE_URL;
|
|
791
701
|
if (override) return override.replace(/\/+$/, '');
|
|
@@ -1039,6 +949,238 @@ function getGeminiOAuthStatus() {
|
|
|
1039
949
|
return null;
|
|
1040
950
|
}
|
|
1041
951
|
|
|
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
|
+
|
|
1042
1184
|
const PROVIDER_CONFIGS = {
|
|
1043
1185
|
'anthropic': {
|
|
1044
1186
|
name: 'Anthropic', configPaths: [
|
|
@@ -1127,6 +1269,13 @@ function getProviderConfigs() {
|
|
|
1127
1269
|
continue;
|
|
1128
1270
|
}
|
|
1129
1271
|
}
|
|
1272
|
+
if (providerId === 'codex') {
|
|
1273
|
+
const oauthStatus = getCodexOAuthStatus();
|
|
1274
|
+
if (oauthStatus) {
|
|
1275
|
+
configs[providerId] = { name: config.name, ...oauthStatus };
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1130
1279
|
for (const configPath of config.configPaths) {
|
|
1131
1280
|
try {
|
|
1132
1281
|
if (fs.existsSync(configPath)) {
|
|
@@ -1257,6 +1406,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1257
1406
|
return;
|
|
1258
1407
|
}
|
|
1259
1408
|
|
|
1409
|
+
if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
|
|
1410
|
+
await handleCodexOAuthCallback(req, res);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1260
1414
|
if (pathOnly === '/api/conversations' && req.method === 'GET') {
|
|
1261
1415
|
const conversations = queries.getConversationsList();
|
|
1262
1416
|
// Filter out stale streaming state using a single bulk query instead of N+1 per-conversation queries
|
|
@@ -2267,10 +2421,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2267
2421
|
} else {
|
|
2268
2422
|
status.detail = 'no credentials';
|
|
2269
2423
|
}
|
|
2270
|
-
} else if (agent.id === 'codex') {
|
|
2271
|
-
const codexSt = getCodexAuthStatus();
|
|
2272
|
-
status.authenticated = codexSt.authenticated;
|
|
2273
|
-
status.detail = codexSt.detail;
|
|
2274
2424
|
} else {
|
|
2275
2425
|
status.detail = 'unknown';
|
|
2276
2426
|
}
|
|
@@ -2716,12 +2866,110 @@ const server = http.createServer(async (req, res) => {
|
|
|
2716
2866
|
return;
|
|
2717
2867
|
}
|
|
2718
2868
|
|
|
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
|
+
|
|
2719
2936
|
const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
|
|
2720
2937
|
if (agentAuthMatch && req.method === 'POST') {
|
|
2721
2938
|
const agentId = agentAuthMatch[1];
|
|
2722
2939
|
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
2723
2940
|
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
|
|
2724
2941
|
|
|
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
|
+
|
|
2725
2973
|
if (agentId === 'gemini') {
|
|
2726
2974
|
try {
|
|
2727
2975
|
const result = await startGeminiOAuth(req);
|
|
@@ -2753,55 +3001,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2753
3001
|
}
|
|
2754
3002
|
}
|
|
2755
3003
|
|
|
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
|
-
|
|
2805
3004
|
const authCommands = {
|
|
2806
3005
|
'claude-code': { cmd: 'claude', args: ['setup-token'] },
|
|
2807
3006
|
'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
|
|
@@ -4333,7 +4532,8 @@ registerUtilHandlers(wsRouter, {
|
|
|
4333
4532
|
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
4334
4533
|
startGeminiOAuth, exchangeGeminiOAuthCode,
|
|
4335
4534
|
geminiOAuthState: () => geminiOAuthState,
|
|
4336
|
-
|
|
4535
|
+
startCodexOAuth, exchangeCodexOAuthCode,
|
|
4536
|
+
codexOAuthState: () => codexOAuthState,
|
|
4337
4537
|
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents
|
|
4338
4538
|
});
|
|
4339
4539
|
|
package/static/js/agent-auth.js
CHANGED
|
@@ -128,7 +128,6 @@
|
|
|
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;
|
|
132
131
|
|
|
133
132
|
function cleanupOAuthPolling() {
|
|
134
133
|
if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
|
|
@@ -136,96 +135,6 @@
|
|
|
136
135
|
if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
|
|
137
136
|
}
|
|
138
137
|
|
|
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
|
-
|
|
229
138
|
function showOAuthWaitingModal() {
|
|
230
139
|
removeOAuthModal();
|
|
231
140
|
var overlay = document.createElement('div');
|
|
@@ -308,19 +217,6 @@
|
|
|
308
217
|
|
|
309
218
|
function triggerAuth(agentId) {
|
|
310
219
|
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
|
-
}
|
|
324
220
|
window.wsClient.rpc('agent.auth', { id: agentId })
|
|
325
221
|
.then(function(data) {
|
|
326
222
|
if (data.ok) {
|
|
@@ -375,8 +271,6 @@
|
|
|
375
271
|
authRunning = false;
|
|
376
272
|
removeOAuthModal();
|
|
377
273
|
cleanupOAuthPolling();
|
|
378
|
-
cleanupCodexPolling();
|
|
379
|
-
removeCodexModal();
|
|
380
274
|
var term = getTerminal();
|
|
381
275
|
var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
|
|
382
276
|
if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
|