Critical fixes: - ALWAYS sync active AG X provider to endpoints.json on startup (previously only synced on first run, causing LS to use stale data) - Add --no-sandbox to proxy process spawns (fixes SIGTRAP crash on Linux) - Add --no-sandbox for all Electron modes (not just headless) - Add --ag-reset flag to re-trigger welcome screen - Bump version to 2.0.4 Root causes fixed: 1. endpoints.json was never updated after first-run, so LS used wrong provider 2. Electron child processes (translation proxy) crashed due to SUID sandbox 3. No way to re-show provider selection after initial setup
510 lines
20 KiB
JavaScript
510 lines
20 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.LS_BINARY = void 0;
|
|
exports.getLsCL = getLsCL;
|
|
exports.getLsProcess = getLsProcess;
|
|
exports.getLsPort = getLsPort;
|
|
exports.clearLsProcess = clearLsProcess;
|
|
exports.extractCrashStackTrace = extractCrashStackTrace;
|
|
exports.startLanguageServer = startLanguageServer;
|
|
exports.setIntentionalTermination = setIntentionalTermination;
|
|
exports.startAndMonitorLanguageServer = startAndMonitorLanguageServer;
|
|
exports.killLanguageServer = killLanguageServer;
|
|
exports.setupLocalCertTrust = setupLocalCertTrust;
|
|
const child_process_1 = require("child_process");
|
|
const electron_1 = require("electron");
|
|
const shell_env_1 = require("shell-env");
|
|
const fs = __importStar(require("fs"));
|
|
const path_1 = __importDefault(require("path"));
|
|
const readline = __importStar(require("readline"));
|
|
const stream_1 = require("stream");
|
|
const paths_1 = require("./paths");
|
|
const constants_1 = require("./constants");
|
|
const utils_1 = require("./utils");
|
|
// ---------------------------------------------------------------------------
|
|
// Config
|
|
// ---------------------------------------------------------------------------
|
|
const LS_STARTUP_TIMEOUT_MS = 60000;
|
|
// ---------------------------------------------------------------------------
|
|
// Crash Monitoring Constants
|
|
// ---------------------------------------------------------------------------
|
|
const RESTART_WINDOW_MS = 60000;
|
|
const MAX_RESTARTS = 3;
|
|
const RESTART_COOLDOWN_MS = 2000;
|
|
const MAX_STDERR_BUFFER = 100000;
|
|
const CRASH_TRIGGER_PHRASES = [
|
|
'panic:',
|
|
'fatal error:',
|
|
'unexpected fault address',
|
|
'runtime:',
|
|
'running GoogleExitFunction',
|
|
'panic serving',
|
|
];
|
|
const isWindows = process.platform === 'win32';
|
|
const binName = isWindows ? 'language_server.exe' : 'language_server';
|
|
exports.LS_BINARY = electron_1.app.isPackaged
|
|
? path_1.default.join(process.resourcesPath, 'bin', binName)
|
|
: process.env.CODEIUM_LANGUAGE_SERVER_BIN ||
|
|
path_1.default.join(__dirname, '..', 'bin', binName);
|
|
/**
|
|
* Gets the build CL of the language server by running it with --stamp.
|
|
*/
|
|
function getLsCL() {
|
|
return new Promise((resolve) => {
|
|
(0, child_process_1.execFile)(exports.LS_BINARY, ['--stamp'], (error, stdout, _stderr) => {
|
|
if (error) {
|
|
console.error('Failed to get LS stamp:', error);
|
|
resolve('');
|
|
return;
|
|
}
|
|
const match = /Built at CL: (\d+)/.exec(stdout);
|
|
if (match) {
|
|
resolve(match[1]);
|
|
}
|
|
else {
|
|
resolve('');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
// Pattern: "listening on <proto> port at <N> for HTTP or HTTPS"
|
|
const PORT_PATTERN = /listening on \w+ port at (\d+) for HTTP(S)?\b/i;
|
|
// Pattern: OAuth authorization URL
|
|
const AUTH_URL_PATTERN = /https:\/\/accounts\.google\.com\/o\/oauth2\/auth\S+/;
|
|
// ---------------------------------------------------------------------------
|
|
// State
|
|
// ---------------------------------------------------------------------------
|
|
let _lsProcess = null;
|
|
let _proxyProcess = null;
|
|
let _lsPort = 0;
|
|
let _intentionalTermination = false;
|
|
let _restartCount = 0;
|
|
let _lastRestartTime = 0;
|
|
/** Returns the active language server process, or null if not running. */
|
|
function getLsProcess() {
|
|
return _lsProcess;
|
|
}
|
|
/** Returns the active language server port, or 0 if not running. */
|
|
function getLsPort() {
|
|
return _lsPort;
|
|
}
|
|
/** Clears the language server process reference (call after killing it). */
|
|
function clearLsProcess() {
|
|
_lsProcess = null;
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Crash log extraction
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Extract lines after a crash trigger phrase from a list of stderr lines.
|
|
* Returns all lines from the first trigger phrase onwards.
|
|
*/
|
|
function getLinesAfterCrash(lines) {
|
|
const crashLines = [];
|
|
let foundTrigger = false;
|
|
for (const line of lines) {
|
|
if (CRASH_TRIGGER_PHRASES.some((phrase) => line.includes(phrase))) {
|
|
foundTrigger = true;
|
|
}
|
|
if (foundTrigger) {
|
|
crashLines.push(line);
|
|
}
|
|
}
|
|
return crashLines;
|
|
}
|
|
/**
|
|
* Best-effort extraction of the crash stack trace from buffered stderr.
|
|
* Returns the stack trace string, or undefined if no trigger phrase was found.
|
|
*/
|
|
function extractCrashStackTrace(stderr) {
|
|
const lines = stderr.split('\n');
|
|
const crashLines = getLinesAfterCrash(lines);
|
|
return crashLines.length > 0 ? crashLines.join('\n') : undefined;
|
|
}
|
|
/**
|
|
* Sets environment variables for bundled node modules so the language
|
|
* server can find them.
|
|
*
|
|
* NOTE: If you add a new module that needs to be executed this way:
|
|
* 1. Add it to `asarUnpack` in `package.json` so it is available on the filesystem.
|
|
* 2. Add it to `modules` in the callsite of setupNodeModules.
|
|
*/
|
|
function setupNodeModules(env, modules) {
|
|
for (const mod of modules) {
|
|
let entryPoint = '';
|
|
if (!electron_1.app.isPackaged) {
|
|
entryPoint = path_1.default.join(__dirname, '..', 'node_modules', mod.name, ...mod.relativePath);
|
|
}
|
|
else {
|
|
entryPoint = path_1.default.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', mod.name, ...mod.relativePath);
|
|
}
|
|
env[mod.envVar] = entryPoint;
|
|
}
|
|
}
|
|
/**
|
|
* Spawn the language server and resolve with a LanguageServerHandle once
|
|
* the LS reports its HTTP port. Rejects on timeout or unexpected exit
|
|
* during startup.
|
|
*
|
|
* After resolving, callers should monitor `handle.exitPromise` to detect
|
|
* crashes that occur after startup.
|
|
*/
|
|
const os = require('os');
|
|
function ensureProxyStarted() {
|
|
return new Promise((resolve) => {
|
|
if (_proxyProcess) {
|
|
resolve();
|
|
return;
|
|
}
|
|
try {
|
|
const home = os.homedir();
|
|
const endpointsPath = path_1.default.join(home, '.codex', 'endpoints.json');
|
|
const activePath = path_1.default.join(home, '.codex', '.active-endpoint.json');
|
|
const proxyConfigDir = path_1.default.join(home, '.cache', 'codex-proxy');
|
|
const activeConfigPath = path_1.default.join(proxyConfigDir, 'proxy-active.json');
|
|
if (!fs.existsSync(proxyConfigDir)) {
|
|
fs.mkdirSync(proxyConfigDir, { recursive: true });
|
|
}
|
|
let endpointsData = { default: "", endpoints: [] };
|
|
if (fs.existsSync(endpointsPath)) {
|
|
endpointsData = JSON.parse(fs.readFileSync(endpointsPath, 'utf8'));
|
|
}
|
|
let activeName = "";
|
|
if (fs.existsSync(activePath)) {
|
|
try {
|
|
const activeData = JSON.parse(fs.readFileSync(activePath, 'utf8'));
|
|
activeName = activeData.active;
|
|
} catch (e) {}
|
|
}
|
|
if (!activeName) {
|
|
activeName = endpointsData.default || (endpointsData.endpoints[0] && endpointsData.endpoints[0].name) || "";
|
|
}
|
|
const endpoint = endpointsData.endpoints.find(e => e.name === activeName) || endpointsData.endpoints[0];
|
|
if (!endpoint) {
|
|
console.log('[Codex] No active endpoint configured in ~/.codex/endpoints.json');
|
|
resolve();
|
|
return;
|
|
}
|
|
const modelList = endpoint.models || [];
|
|
const pcfg = {
|
|
port: 48080,
|
|
backend_type: endpoint.backend_type,
|
|
target_url: endpoint.base_url,
|
|
api_key: endpoint.api_key,
|
|
cc_version: endpoint.cc_version || "",
|
|
oauth_provider: endpoint.oauth_provider || "",
|
|
reasoning_enabled: endpoint.reasoning_enabled !== undefined ? endpoint.reasoning_enabled : true,
|
|
reasoning_effort: endpoint.reasoning_effort || "medium",
|
|
models: modelList.map(m => ({ id: m, object: "model", created: 1700000000, owned_by: endpoint.name }))
|
|
};
|
|
fs.writeFileSync(activeConfigPath, JSON.stringify(pcfg, null, 2), 'utf8');
|
|
console.log('[AG X] Starting built-in Node.js translation proxy for:', endpoint.name);
|
|
// Use Electron's built-in Node.js to run our translation proxy
|
|
const proxyScript = path_1.default.join(__dirname, 'services', 'translationProxy.js');
|
|
const nodeBin = process.execPath;
|
|
_proxyProcess = (0, child_process_1.spawn)(nodeBin, ['--no-sandbox', proxyScript], {
|
|
stdio: 'ignore',
|
|
detached: true,
|
|
env: { ...process.env }
|
|
});
|
|
_proxyProcess.unref();
|
|
setTimeout(() => {
|
|
resolve();
|
|
}, 500);
|
|
} catch (err) {
|
|
console.error('[AG X] Failed to auto-start translation proxy:', err);
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
function startLanguageServer(port, csrf, headless) {
|
|
return new Promise(async (resolve, reject) => {
|
|
await ensureProxyStarted();
|
|
process.env.ANTIGRAVITY_API_SERVER_URL = 'http://127.0.0.1:48080';
|
|
process.env.ANTIGRAVITY_CLOUD_CODE_ENDPOINT = 'http://127.0.0.1:48080';
|
|
const logStream = fs.createWriteStream((0, paths_1.getLsLogPath)(), { flags: 'w' });
|
|
// We need to pass the override flags because the LS is running in standalone mode
|
|
const args = [
|
|
'--standalone',
|
|
'--override_ide_name',
|
|
'ag-x',
|
|
'--subclient_type',
|
|
'hub',
|
|
'--override_ide_version',
|
|
electron_1.app.getVersion(),
|
|
'--override_user_agent_name',
|
|
'ag-x',
|
|
'--https_server_port',
|
|
String(port),
|
|
'--csrf_token',
|
|
csrf,
|
|
'--app_data_dir',
|
|
(0, paths_1.getAppDataDirName)(),
|
|
'--api_server_url',
|
|
process.env.ANTIGRAVITY_API_SERVER_URL || 'https://generativelanguage.googleapis.com',
|
|
'--cloud_code_endpoint',
|
|
process.env.ANTIGRAVITY_CLOUD_CODE_ENDPOINT || 'https://daily-cloudcode-pa.googleapis.com',
|
|
'--enable_sidecars',
|
|
];
|
|
if (headless) {
|
|
args.push('--headless');
|
|
}
|
|
console.log(`\nSpawning: ${exports.LS_BINARY} ${args.join(' ')}\n`);
|
|
// Electron apps don't inherit shell environment variables when they are not launched through the terminal.
|
|
// We need to load the shell env explicitly so the language server can discover tools in the user's environment.
|
|
const env = { ...process.env, ...(0, shell_env_1.shellEnvSync)() };
|
|
// We don't read the file to avoid adding start up latency.
|
|
// LS will read when browser recording encoder is invoked.
|
|
env['AGY_BROWSER_ACTIVE_PORT_FILE'] = (0, paths_1.getActivePortFilePath)();
|
|
(0, utils_1.setupNodeWrapper)(env);
|
|
setupNodeModules(env, [
|
|
{
|
|
name: 'chrome-devtools-mcp',
|
|
envVar: 'CHROME_DEVTOOLS_MCP_JS',
|
|
relativePath: ['build', 'src', 'bin', 'chrome-devtools-mcp.js'],
|
|
},
|
|
]);
|
|
_lsProcess = (0, child_process_1.spawn)(exports.LS_BINARY, args, {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env,
|
|
});
|
|
if (!headless) {
|
|
// Close stdin immediately — the LS may block waiting for metadata on stdin.
|
|
_lsProcess.stdin.end();
|
|
}
|
|
const combined = new stream_1.PassThrough();
|
|
_lsProcess.stdout.pipe(combined, { end: false });
|
|
_lsProcess.stderr.pipe(combined, { end: false });
|
|
// Buffer stderr for crash log extraction (ring buffer)
|
|
const stderrChunks = [];
|
|
let stderrLength = 0;
|
|
_lsProcess.stderr.on('data', (data) => {
|
|
const str = data.toString();
|
|
stderrChunks.push(str);
|
|
stderrLength += str.length;
|
|
while (stderrChunks.length > 0 && stderrLength > MAX_STDERR_BUFFER) {
|
|
stderrLength -= stderrChunks.shift().length;
|
|
}
|
|
});
|
|
let resolved = false;
|
|
let logStreamEnded = false;
|
|
const timer = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
reject(new Error(`Timeout: language server did not report its port within ${LS_STARTUP_TIMEOUT_MS / 1000}s`));
|
|
}
|
|
}, LS_STARTUP_TIMEOUT_MS);
|
|
const rl = readline.createInterface({ input: combined, crlfDelay: Infinity });
|
|
rl.on('close', () => {
|
|
if (!logStreamEnded) {
|
|
logStreamEnded = true;
|
|
logStream.end();
|
|
}
|
|
});
|
|
rl.on('line', (line) => {
|
|
if (!logStreamEnded) {
|
|
logStream.write(line + '\n');
|
|
}
|
|
if (!resolved) {
|
|
const m = PORT_PATTERN.exec(line);
|
|
if (m) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
const actualPort = parseInt(m[1], 10);
|
|
_lsPort = actualPort;
|
|
resolve({
|
|
port: actualPort,
|
|
process: _lsProcess,
|
|
exitPromise,
|
|
});
|
|
}
|
|
}
|
|
const authMatch = AUTH_URL_PATTERN.exec(line);
|
|
if (authMatch) {
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(' Please visit the following URL to authorize.');
|
|
console.log(' After authorizing, paste the authorization code below.');
|
|
console.log(` ${authMatch[0]}`);
|
|
console.log('='.repeat(60) + '\n');
|
|
}
|
|
});
|
|
// Exit promise — resolves whenever the process exits (whether during
|
|
// startup or after). Includes crash stack trace extraction.
|
|
const exitPromise = new Promise((exitResolve) => {
|
|
_lsProcess.on('exit', (code, signal) => {
|
|
if (!logStreamEnded) {
|
|
logStreamEnded = true;
|
|
logStream.end();
|
|
}
|
|
const fullStderr = stderrChunks.join('');
|
|
const crashStackTrace = extractCrashStackTrace(fullStderr);
|
|
// If we haven't resolved the startup promise yet, reject it.
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timer);
|
|
reject(new Error(`Language server exited unexpectedly (code=${code}, signal=${signal})`));
|
|
}
|
|
exitResolve({ code, signal, crashStackTrace });
|
|
});
|
|
});
|
|
});
|
|
}
|
|
/** Sets whether the termination was intentional (suppresses crash reports). */
|
|
function setIntentionalTermination(value) {
|
|
_intentionalTermination = value;
|
|
}
|
|
/**
|
|
* Start the language server AND set up the restart monitoring loop.
|
|
* Resolves with the handle on first successful startup.
|
|
*/
|
|
async function startAndMonitorLanguageServer(port, csrf, options = {}) {
|
|
setIntentionalTermination(false); // Reset
|
|
const handle = await startLanguageServer(port, csrf, options.headless);
|
|
_lsPort = handle.port;
|
|
if (options.onPortChanged) {
|
|
options.onPortChanged(_lsPort);
|
|
}
|
|
monitorLsCrashInternal(handle, port, csrf, options);
|
|
return handle;
|
|
}
|
|
function monitorLsCrashInternal(handle, port, csrf, options) {
|
|
void handle.exitPromise.then(async (exitInfo) => {
|
|
clearLsProcess();
|
|
if (_intentionalTermination) {
|
|
return;
|
|
}
|
|
const { code, signal, crashStackTrace } = exitInfo;
|
|
const summary = signal
|
|
? `killed by signal ${signal}`
|
|
: `exited with code ${code}`;
|
|
console.error(`\nLanguage server crashed: ${summary}`);
|
|
if (crashStackTrace) {
|
|
console.error('--- Crash Stack Trace ---');
|
|
console.error(crashStackTrace);
|
|
console.error('--- End Crash Stack trace ---');
|
|
}
|
|
const now = Date.now();
|
|
if (now - _lastRestartTime > RESTART_WINDOW_MS) {
|
|
_restartCount = 0;
|
|
}
|
|
_lastRestartTime = now;
|
|
if (_restartCount >= MAX_RESTARTS) {
|
|
const msg = `Language server crashed ${MAX_RESTARTS} times in a row. Giving up.`;
|
|
console.error(msg);
|
|
return;
|
|
}
|
|
_restartCount++;
|
|
console.log(`Attempting restart ${_restartCount}/${MAX_RESTARTS} in ${RESTART_COOLDOWN_MS / 1000}s...`);
|
|
await sleep(RESTART_COOLDOWN_MS);
|
|
if (_intentionalTermination) {
|
|
return;
|
|
}
|
|
try {
|
|
const newHandle = await startLanguageServer(port, csrf);
|
|
_lsPort = newHandle.port;
|
|
if (options.onPortChanged) {
|
|
options.onPortChanged(_lsPort);
|
|
}
|
|
// Recurse
|
|
monitorLsCrashInternal(newHandle, port, csrf, options);
|
|
}
|
|
catch (err) {
|
|
console.error(`Failed to restart language server: ${err.message}`);
|
|
}
|
|
});
|
|
}
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
async function killLanguageServer() {
|
|
setIntentionalTermination(true);
|
|
const proc = getLsProcess();
|
|
if (proc) {
|
|
const pid = proc.pid;
|
|
console.log('Shutting down language server…');
|
|
const exitPromise = new Promise((resolve) => {
|
|
proc.once('exit', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
proc.kill('SIGTERM');
|
|
const result = await Promise.race([
|
|
exitPromise.then(() => 'exited'),
|
|
new Promise((resolve) => setTimeout(() => resolve('timeout'), 5000)),
|
|
]);
|
|
if (result === 'timeout' && pid !== undefined) {
|
|
console.warn(`Language server (PID ${pid}) did not exit gracefully within 5s. Sending SIGKILL.`);
|
|
try {
|
|
process.kill(pid, 'SIGKILL');
|
|
}
|
|
catch {
|
|
// Process already dead or exited
|
|
}
|
|
}
|
|
clearLsProcess();
|
|
}
|
|
if (_proxyProcess) {
|
|
console.log('[Codex] Stopping background translation proxy…');
|
|
try {
|
|
_proxyProcess.kill('SIGTERM');
|
|
}
|
|
catch (e) {}
|
|
_proxyProcess = null;
|
|
}
|
|
}
|
|
/**
|
|
* Sets up certificate verification in Electron to trust the local self-signed cert
|
|
* used by the language server. It verifies that the certificate fingerprint matches
|
|
* the hardcoded `LS_CERT_FINGERPRINT`.
|
|
*
|
|
* TODO: Generate the cert.pem file dynamically
|
|
*/
|
|
function setupLocalCertTrust() {
|
|
electron_1.session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
|
if ((request.hostname === '127.0.0.1' || request.hostname === 'localhost') &&
|
|
request.certificate.fingerprint === constants_1.LS_CERT_FINGERPRINT) {
|
|
callback(0); // Accept
|
|
}
|
|
else {
|
|
callback(-3); // Default validation
|
|
}
|
|
});
|
|
}
|