remote-codex 0.11.43 → 0.11.45
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/README.md +9 -0
- package/apps/relay-server/dist/index.js +17 -1
- package/apps/supervisor-api/dist/index.js +2189 -1798
- package/apps/supervisor-web/dist/assets/index-BO9S3vTX.css +1 -0
- package/apps/supervisor-web/dist/assets/index-GqVDOqbI.js +22 -0
- package/apps/supervisor-web/dist/assets/{thread-ui-Dmrigdek.js → thread-ui-BWC_ljvN.js} +11 -11
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +426 -35
- package/docs/windows.md +81 -0
- package/package.json +14 -3
- package/packages/claude/src/runtimeAdapter.test.ts +32 -4
- package/packages/claude/src/runtimeAdapter.ts +21 -22
- package/packages/codex/src/appServerManager.test.ts +47 -0
- package/packages/codex/src/appServerManager.ts +6 -2
- package/packages/codex/src/runtimeAdapter.test.ts +9 -2
- package/packages/opencode/src/historyItems.ts +7 -6
- package/packages/opencode/src/runtimeAdapter.ts +7 -11
- package/packages/process-runtime/src/index.test.ts +132 -0
- package/packages/process-runtime/src/index.ts +253 -0
- package/packages/shared/src/index.ts +12 -0
- package/scripts/service-manager.mjs +112 -4
- package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
- package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
- package/scripts/windows/relay-smoke.ps1 +16 -0
- package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
- package/scripts/windows/validate-real-codex.mjs +680 -0
- package/apps/supervisor-web/dist/assets/index-BT0SM9C-.js +0 -21
- package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest" />
|
|
12
12
|
<title>Remote Codex</title>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-GqVDOqbI.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/assets/markdown-vendor-RZk8L7-L.js">
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/thread-ui-
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/thread-ui-BWC_ljvN.js">
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BO9S3vTX.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-stone-950">
|
|
25
25
|
<div id="root"></div>
|
package/bin/remote-codex.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import readline from 'node:readline/promises';
|
|
8
8
|
import { spawn, spawnSync } from 'node:child_process';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import net from 'node:net';
|
|
10
11
|
|
|
11
12
|
const binDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const packageRoot = path.resolve(binDir, '..');
|
|
@@ -19,6 +20,13 @@ const supervisorSourceEntry = path.join(packageRoot, 'apps', 'supervisor-api', '
|
|
|
19
20
|
const relaySupervisorConfigPath = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_CONFIG
|
|
20
21
|
? path.resolve(process.env.REMOTE_CODEX_RELAY_SUPERVISOR_CONFIG)
|
|
21
22
|
: path.join(os.homedir(), '.remote-codex', 'relay-supervisor.json');
|
|
23
|
+
const relaySupervisorStatePath = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_STATE
|
|
24
|
+
? path.resolve(process.env.REMOTE_CODEX_RELAY_SUPERVISOR_STATE)
|
|
25
|
+
: path.join(os.homedir(), '.remote-codex', 'relay-supervisor-state.json');
|
|
26
|
+
const relaySupervisorStartLockPath = `${relaySupervisorStatePath}.start.lock`;
|
|
27
|
+
const relaySupervisorLogPath = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_LOG
|
|
28
|
+
? path.resolve(process.env.REMOTE_CODEX_RELAY_SUPERVISOR_LOG)
|
|
29
|
+
: path.join(os.homedir(), '.remote-codex', 'logs', 'relay-supervisor.log');
|
|
22
30
|
const relaySupervisorTmuxSession = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_TMUX_SESSION?.trim()
|
|
23
31
|
|| 'remote-codex-relay-supervisor';
|
|
24
32
|
const relaySupervisorConfigKeys = [
|
|
@@ -204,11 +212,19 @@ async function runRelaySupervisor() {
|
|
|
204
212
|
return;
|
|
205
213
|
}
|
|
206
214
|
if (action === 'status') {
|
|
207
|
-
|
|
215
|
+
if (process.platform === 'win32') {
|
|
216
|
+
await relaySupervisorWindowsStatus();
|
|
217
|
+
} else {
|
|
218
|
+
relaySupervisorStatus();
|
|
219
|
+
}
|
|
208
220
|
return;
|
|
209
221
|
}
|
|
210
222
|
if (action === 'stop') {
|
|
211
|
-
|
|
223
|
+
if (process.platform === 'win32') {
|
|
224
|
+
await stopRelaySupervisorWindows();
|
|
225
|
+
} else {
|
|
226
|
+
stopRelaySupervisorTmux();
|
|
227
|
+
}
|
|
212
228
|
return;
|
|
213
229
|
}
|
|
214
230
|
if (action !== 'start' && action !== 'run') {
|
|
@@ -217,6 +233,10 @@ async function runRelaySupervisor() {
|
|
|
217
233
|
process.exit(1);
|
|
218
234
|
}
|
|
219
235
|
await ensureRelaySupervisorConfig();
|
|
236
|
+
if (action === 'start' && process.platform === 'win32') {
|
|
237
|
+
await startRelaySupervisorWindows();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
220
240
|
if (action === 'start' && shouldStartRelaySupervisorInTmux()) {
|
|
221
241
|
startRelaySupervisorTmux();
|
|
222
242
|
return;
|
|
@@ -274,10 +294,36 @@ function runRelaySupervisorForeground() {
|
|
|
274
294
|
validateRequiredEnv(guidance);
|
|
275
295
|
printEnvSummary(guidance);
|
|
276
296
|
|
|
297
|
+
const { commandToRun, args } = resolveSupervisorLaunch();
|
|
298
|
+
|
|
299
|
+
const supervisor = spawn(commandToRun, args, {
|
|
300
|
+
cwd: packageRoot,
|
|
301
|
+
env: relaySupervisorProcessEnv(),
|
|
302
|
+
stdio: 'inherit',
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
supervisor.on('exit', (code, signal) => {
|
|
306
|
+
if (signal) {
|
|
307
|
+
process.kill(process.pid, signal);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (code && code !== 0) {
|
|
312
|
+
console.error(`remote-codex relay-supervisor exited with code ${code}. Check the relay server URL, device token, local REMOTE_CODEX_RELAY_SUPERVISOR_PORT, and environment values above.`);
|
|
313
|
+
}
|
|
314
|
+
process.exit(code ?? 1);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
supervisor.on('error', (error) => {
|
|
318
|
+
console.error(`Failed to run remote-codex relay-supervisor: ${error.message}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function resolveSupervisorLaunch() {
|
|
277
324
|
const supervisorEntry = fs.existsSync(supervisorDistEntry)
|
|
278
325
|
? supervisorDistEntry
|
|
279
326
|
: supervisorSourceEntry;
|
|
280
|
-
let commandToRun = process.execPath;
|
|
281
327
|
let args = [supervisorEntry];
|
|
282
328
|
|
|
283
329
|
if (!fs.existsSync(supervisorEntry)) {
|
|
@@ -295,37 +341,330 @@ function runRelaySupervisorForeground() {
|
|
|
295
341
|
}
|
|
296
342
|
args = [tsxEntry, supervisorSourceEntry];
|
|
297
343
|
}
|
|
344
|
+
return { commandToRun: process.execPath, args };
|
|
345
|
+
}
|
|
298
346
|
|
|
299
|
-
|
|
347
|
+
function relaySupervisorProcessEnv(extra = {}) {
|
|
348
|
+
return {
|
|
349
|
+
...process.env,
|
|
350
|
+
REMOTE_CODEX_MODE: 'relay',
|
|
351
|
+
REMOTE_CODEX_PACKAGE_ROOT: process.env.REMOTE_CODEX_PACKAGE_ROOT ?? packageRoot,
|
|
352
|
+
REMOTE_CODEX_DISABLE_BUILD_RESTART:
|
|
353
|
+
process.env.REMOTE_CODEX_DISABLE_BUILD_RESTART ?? (sourceCheckout ? 'false' : 'true'),
|
|
354
|
+
...extra,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function startRelaySupervisorWindows() {
|
|
359
|
+
const releaseStartLock = acquireRelaySupervisorStartLock();
|
|
360
|
+
if (!releaseStartLock) {
|
|
361
|
+
process.exitCode = 1;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await startRelaySupervisorWindowsUnlocked();
|
|
366
|
+
} finally {
|
|
367
|
+
releaseStartLock();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function startRelaySupervisorWindowsUnlocked() {
|
|
372
|
+
persistRelaySupervisorRuntimeConfig();
|
|
373
|
+
const existing = readRelaySupervisorState();
|
|
374
|
+
if (existing) {
|
|
375
|
+
const response = await requestRelaySupervisorLifecycle(existing, 'status').catch(() => null);
|
|
376
|
+
if (response?.ok === true && response.instanceId === existing.instanceId) {
|
|
377
|
+
console.log(`remote-codex relay-supervisor is already running (pid ${existing.pid}).`);
|
|
378
|
+
console.log(`Logs: ${existing.logPath ?? relaySupervisorLogPath}`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (isPidAlive(existing.pid)) {
|
|
382
|
+
console.error('A relay-supervisor state file exists for a live process, but its identity could not be verified.');
|
|
383
|
+
console.error(`Refusing to replace or terminate pid ${existing.pid}. Inspect ${relaySupervisorStatePath}.`);
|
|
384
|
+
process.exitCode = 1;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
removeRelaySupervisorState();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const { commandToRun, args } = resolveSupervisorLaunch();
|
|
391
|
+
const instanceId = crypto.randomUUID();
|
|
392
|
+
const controlToken = randomSecret(32);
|
|
393
|
+
const identityHash = crypto
|
|
394
|
+
.createHash('sha256')
|
|
395
|
+
.update(`${relaySupervisorConfigPath}\0${os.userInfo().username}`)
|
|
396
|
+
.digest('hex')
|
|
397
|
+
.slice(0, 24);
|
|
398
|
+
const controlEndpoint = `\\\\.\\pipe\\remote-codex-relay-supervisor-${identityHash}`;
|
|
399
|
+
fs.mkdirSync(path.dirname(relaySupervisorLogPath), { recursive: true });
|
|
400
|
+
rotateRelaySupervisorLog();
|
|
401
|
+
const logFd = fs.openSync(relaySupervisorLogPath, 'a');
|
|
402
|
+
const child = spawn(commandToRun, args, {
|
|
300
403
|
cwd: packageRoot,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
404
|
+
detached: true,
|
|
405
|
+
windowsHide: true,
|
|
406
|
+
env: relaySupervisorProcessEnv({
|
|
407
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_ENDPOINT: controlEndpoint,
|
|
408
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_TOKEN: controlToken,
|
|
409
|
+
REMOTE_CODEX_LIFECYCLE_INSTANCE_ID: instanceId,
|
|
410
|
+
REMOTE_CODEX_ENABLED_AGENT_PROVIDERS:
|
|
411
|
+
process.env.REMOTE_CODEX_ENABLED_AGENT_PROVIDERS ?? 'codex',
|
|
412
|
+
}),
|
|
413
|
+
stdio: ['ignore', logFd, logFd],
|
|
309
414
|
});
|
|
415
|
+
child.unref();
|
|
416
|
+
fs.closeSync(logFd);
|
|
417
|
+
if (!child.pid) {
|
|
418
|
+
console.error('Failed to start relay-supervisor in the background.');
|
|
419
|
+
process.exitCode = 1;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
310
422
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
423
|
+
const state = {
|
|
424
|
+
schemaVersion: 1,
|
|
425
|
+
pid: child.pid,
|
|
426
|
+
instanceId,
|
|
427
|
+
controlToken,
|
|
428
|
+
controlEndpoint,
|
|
429
|
+
startedAt: new Date().toISOString(),
|
|
430
|
+
host: envValue(['REMOTE_CODEX_RELAY_SUPERVISOR_HOST', 'HOST'], '127.0.0.1'),
|
|
431
|
+
port: Number(envValue(['REMOTE_CODEX_RELAY_SUPERVISOR_PORT', 'PORT'], '8787')),
|
|
432
|
+
logPath: relaySupervisorLogPath,
|
|
433
|
+
version: readPackageVersion(),
|
|
434
|
+
};
|
|
435
|
+
writeRelaySupervisorState(state);
|
|
436
|
+
|
|
437
|
+
const deadline = Date.now() + 15_000;
|
|
438
|
+
while (Date.now() < deadline) {
|
|
439
|
+
if (!isPidAlive(child.pid)) {
|
|
440
|
+
console.error(`relay-supervisor exited before becoming ready. Logs: ${relaySupervisorLogPath}`);
|
|
441
|
+
removeRelaySupervisorState();
|
|
442
|
+
process.exitCode = 1;
|
|
314
443
|
return;
|
|
315
444
|
}
|
|
445
|
+
const response = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
|
|
446
|
+
if (response?.ok === true && response.instanceId === instanceId) {
|
|
447
|
+
console.log(`Started remote-codex relay-supervisor (pid ${child.pid}).`);
|
|
448
|
+
console.log(`Logs: ${relaySupervisorLogPath}`);
|
|
449
|
+
console.log('Status: remote-codex relay-supervisor status');
|
|
450
|
+
console.log('Stop: remote-codex relay-supervisor stop');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
await sleep(250);
|
|
454
|
+
}
|
|
455
|
+
console.error(`Timed out waiting for relay-supervisor lifecycle control. Logs: ${relaySupervisorLogPath}`);
|
|
456
|
+
if (isPidAlive(child.pid)) {
|
|
457
|
+
spawnSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
|
|
458
|
+
windowsHide: true,
|
|
459
|
+
stdio: 'ignore',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
removeRelaySupervisorState();
|
|
463
|
+
process.exitCode = 1;
|
|
464
|
+
}
|
|
316
465
|
|
|
317
|
-
|
|
318
|
-
|
|
466
|
+
function acquireRelaySupervisorStartLock() {
|
|
467
|
+
fs.mkdirSync(path.dirname(relaySupervisorStartLockPath), { recursive: true });
|
|
468
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
469
|
+
try {
|
|
470
|
+
const descriptor = fs.openSync(relaySupervisorStartLockPath, 'wx', 0o600);
|
|
471
|
+
fs.writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`);
|
|
472
|
+
fs.fsyncSync(descriptor);
|
|
473
|
+
fs.closeSync(descriptor);
|
|
474
|
+
restrictWindowsPathAcl(relaySupervisorStartLockPath, false);
|
|
475
|
+
return () => {
|
|
476
|
+
try { fs.unlinkSync(relaySupervisorStartLockPath); } catch { /* Best-effort lock cleanup. */ }
|
|
477
|
+
};
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
480
|
+
const owner = readStartLockOwner();
|
|
481
|
+
if (attempt === 0 && isRelaySupervisorStartLockStale(owner)) {
|
|
482
|
+
try { fs.unlinkSync(relaySupervisorStartLockPath); } catch { /* Retry reports contention. */ }
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
console.error(`Another relay-supervisor start operation is active${owner?.pid ? ` (pid ${owner.pid})` : ''}.`);
|
|
486
|
+
return null;
|
|
319
487
|
}
|
|
320
|
-
|
|
321
|
-
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
322
491
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
492
|
+
function isRelaySupervisorStartLockStale(owner) {
|
|
493
|
+
if (owner) return !isPidAlive(owner.pid);
|
|
494
|
+
try {
|
|
495
|
+
return Date.now() - fs.statSync(relaySupervisorStartLockPath).mtimeMs > 30_000;
|
|
496
|
+
} catch {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function readStartLockOwner() {
|
|
502
|
+
try {
|
|
503
|
+
const value = JSON.parse(fs.readFileSync(relaySupervisorStartLockPath, 'utf8'));
|
|
504
|
+
return Number.isInteger(value?.pid) ? value : null;
|
|
505
|
+
} catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function relaySupervisorWindowsStatus() {
|
|
511
|
+
const state = readRelaySupervisorState();
|
|
512
|
+
if (!state) {
|
|
513
|
+
console.log('remote-codex relay-supervisor is not running.');
|
|
514
|
+
process.exitCode = 1;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const response = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
|
|
518
|
+
if (response?.ok !== true || response.instanceId !== state.instanceId) {
|
|
519
|
+
if (!isPidAlive(state.pid)) {
|
|
520
|
+
removeRelaySupervisorState();
|
|
521
|
+
console.log('remote-codex relay-supervisor is not running; stale state was removed.');
|
|
522
|
+
} else {
|
|
523
|
+
console.log(`remote-codex relay-supervisor state is unverified for live pid ${state.pid}.`);
|
|
524
|
+
}
|
|
525
|
+
process.exitCode = 1;
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
console.log('State: running');
|
|
529
|
+
console.log(`PID: ${state.pid}`);
|
|
530
|
+
console.log(`Started: ${state.startedAt}`);
|
|
531
|
+
console.log(`API: http://${state.host}:${state.port}`);
|
|
532
|
+
console.log(`Logs: ${state.logPath}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function stopRelaySupervisorWindows() {
|
|
536
|
+
const state = readRelaySupervisorState();
|
|
537
|
+
if (!state) {
|
|
538
|
+
console.log('remote-codex relay-supervisor is not running.');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const status = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
|
|
542
|
+
if (status?.ok !== true || status.instanceId !== state.instanceId) {
|
|
543
|
+
if (!isPidAlive(state.pid)) {
|
|
544
|
+
removeRelaySupervisorState();
|
|
545
|
+
console.log('remote-codex relay-supervisor was not running; stale state was removed.');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
console.error(`Refusing to stop unverified live pid ${state.pid}.`);
|
|
549
|
+
process.exitCode = 1;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const shutdown = await requestRelaySupervisorLifecycle(state, 'shutdown').catch(() => null);
|
|
554
|
+
if (shutdown?.ok !== true || shutdown.instanceId !== state.instanceId) {
|
|
555
|
+
console.error('relay-supervisor rejected the authenticated shutdown request.');
|
|
556
|
+
process.exitCode = 1;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const deadline = Date.now() + 10_000;
|
|
560
|
+
while (Date.now() < deadline && isPidAlive(state.pid)) {
|
|
561
|
+
await sleep(250);
|
|
562
|
+
}
|
|
563
|
+
if (isPidAlive(state.pid)) {
|
|
564
|
+
const finalIdentity = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
|
|
565
|
+
if (finalIdentity?.instanceId === state.instanceId) {
|
|
566
|
+
spawnSync('taskkill.exe', ['/PID', String(state.pid), '/T', '/F'], {
|
|
567
|
+
windowsHide: true,
|
|
568
|
+
stdio: 'ignore',
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (isPidAlive(state.pid)) {
|
|
573
|
+
console.error(`relay-supervisor pid ${state.pid} did not stop. State was preserved.`);
|
|
574
|
+
process.exitCode = 1;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
removeRelaySupervisorState();
|
|
578
|
+
console.log('Stopped remote-codex relay-supervisor.');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function requestRelaySupervisorLifecycle(state, action, timeoutMs = 2_000) {
|
|
582
|
+
return new Promise((resolve, reject) => {
|
|
583
|
+
const socket = net.createConnection(state.controlEndpoint);
|
|
584
|
+
const timer = setTimeout(() => {
|
|
585
|
+
socket.destroy();
|
|
586
|
+
reject(new Error('Lifecycle control request timed out.'));
|
|
587
|
+
}, timeoutMs);
|
|
588
|
+
let output = '';
|
|
589
|
+
socket.setEncoding('utf8');
|
|
590
|
+
socket.once('connect', () => {
|
|
591
|
+
socket.write(`${JSON.stringify({
|
|
592
|
+
action,
|
|
593
|
+
token: state.controlToken,
|
|
594
|
+
instanceId: state.instanceId,
|
|
595
|
+
})}\n`);
|
|
596
|
+
});
|
|
597
|
+
socket.on('data', (chunk) => {
|
|
598
|
+
output += chunk;
|
|
599
|
+
const newline = output.indexOf('\n');
|
|
600
|
+
if (newline < 0) return;
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
socket.end();
|
|
603
|
+
try {
|
|
604
|
+
resolve(JSON.parse(output.slice(0, newline)));
|
|
605
|
+
} catch (error) {
|
|
606
|
+
reject(error);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
socket.once('error', (error) => {
|
|
610
|
+
clearTimeout(timer);
|
|
611
|
+
reject(error);
|
|
612
|
+
});
|
|
326
613
|
});
|
|
327
614
|
}
|
|
328
615
|
|
|
616
|
+
function readRelaySupervisorState() {
|
|
617
|
+
try {
|
|
618
|
+
const state = JSON.parse(fs.readFileSync(relaySupervisorStatePath, 'utf8'));
|
|
619
|
+
if (
|
|
620
|
+
state?.schemaVersion !== 1 ||
|
|
621
|
+
!Number.isInteger(state.pid) ||
|
|
622
|
+
typeof state.instanceId !== 'string' ||
|
|
623
|
+
typeof state.controlToken !== 'string' ||
|
|
624
|
+
typeof state.controlEndpoint !== 'string'
|
|
625
|
+
) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
return state;
|
|
629
|
+
} catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function writeRelaySupervisorState(state) {
|
|
635
|
+
writePrivateJsonFile(relaySupervisorStatePath, state);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function removeRelaySupervisorState() {
|
|
639
|
+
try {
|
|
640
|
+
fs.unlinkSync(relaySupervisorStatePath);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (error?.code !== 'ENOENT') {
|
|
643
|
+
console.error(`Failed to remove relay-supervisor state: ${error.message}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function rotateRelaySupervisorLog() {
|
|
649
|
+
const rotatedPath = `${relaySupervisorLogPath}.1`;
|
|
650
|
+
if (fs.existsSync(rotatedPath)) fs.rmSync(rotatedPath, { force: true });
|
|
651
|
+
if (fs.existsSync(relaySupervisorLogPath)) fs.renameSync(relaySupervisorLogPath, rotatedPath);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function isPidAlive(pid) {
|
|
655
|
+
if (!Number.isInteger(pid) || pid <= 1) return false;
|
|
656
|
+
try {
|
|
657
|
+
process.kill(pid, 0);
|
|
658
|
+
return true;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function sleep(milliseconds) {
|
|
665
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
666
|
+
}
|
|
667
|
+
|
|
329
668
|
function shouldStartRelaySupervisorInTmux() {
|
|
330
669
|
const setting = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_TMUX?.trim().toLowerCase();
|
|
331
670
|
if (['0', 'false', 'no', 'off'].includes(setting ?? '')) {
|
|
@@ -520,12 +859,59 @@ function readRelaySupervisorConfig() {
|
|
|
520
859
|
}
|
|
521
860
|
|
|
522
861
|
function writeRelaySupervisorConfig(config) {
|
|
523
|
-
|
|
524
|
-
|
|
862
|
+
writePrivateJsonFile(relaySupervisorConfigPath, config);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function writePrivateJsonFile(filePath, value) {
|
|
866
|
+
const directory = path.dirname(filePath);
|
|
867
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
868
|
+
const temporaryPath = `${filePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
869
|
+
let descriptor = null;
|
|
525
870
|
try {
|
|
526
|
-
fs.
|
|
527
|
-
|
|
528
|
-
|
|
871
|
+
descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
|
|
872
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`);
|
|
873
|
+
fs.fsyncSync(descriptor);
|
|
874
|
+
fs.closeSync(descriptor);
|
|
875
|
+
descriptor = null;
|
|
876
|
+
try {
|
|
877
|
+
fs.chmodSync(temporaryPath, 0o600);
|
|
878
|
+
fs.chmodSync(directory, 0o700);
|
|
879
|
+
} catch {
|
|
880
|
+
// ACLs are applied separately on Windows.
|
|
881
|
+
}
|
|
882
|
+
if (process.platform === 'win32') {
|
|
883
|
+
restrictWindowsPathAcl(directory, true);
|
|
884
|
+
restrictWindowsPathAcl(temporaryPath, false);
|
|
885
|
+
}
|
|
886
|
+
fs.renameSync(temporaryPath, filePath);
|
|
887
|
+
if (process.platform === 'win32') {
|
|
888
|
+
restrictWindowsPathAcl(filePath, false);
|
|
889
|
+
}
|
|
890
|
+
} catch (error) {
|
|
891
|
+
if (descriptor !== null) {
|
|
892
|
+
try { fs.closeSync(descriptor); } catch { /* Preserve the original write failure. */ }
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
896
|
+
} catch {
|
|
897
|
+
// Preserve the original write failure.
|
|
898
|
+
}
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function restrictWindowsPathAcl(targetPath, directory) {
|
|
904
|
+
const username = process.env.USERDOMAIN && process.env.USERNAME
|
|
905
|
+
? `${process.env.USERDOMAIN}\\${process.env.USERNAME}`
|
|
906
|
+
: process.env.USERNAME ?? os.userInfo().username;
|
|
907
|
+
const permission = directory ? '(OI)(CI)F' : 'F';
|
|
908
|
+
const result = spawnSync(
|
|
909
|
+
'icacls.exe',
|
|
910
|
+
[targetPath, '/inheritance:r', '/grant:r', `${username}:${permission}`, `SYSTEM:${permission}`],
|
|
911
|
+
{ windowsHide: true, encoding: 'utf8' },
|
|
912
|
+
);
|
|
913
|
+
if (result.status !== 0) {
|
|
914
|
+
console.error(`Warning: unable to restrict Windows ACL for ${targetPath}.`);
|
|
529
915
|
}
|
|
530
916
|
}
|
|
531
917
|
|
|
@@ -980,6 +1366,14 @@ Typical flow:
|
|
|
980
1366
|
}
|
|
981
1367
|
|
|
982
1368
|
function printRelaySupervisorHelp() {
|
|
1369
|
+
const processManagement = process.platform === 'win32'
|
|
1370
|
+
? `On Windows, "start" launches a detached supervisor managed through an
|
|
1371
|
+
authenticated local named pipe. "status" verifies the instance identity and
|
|
1372
|
+
"stop" requests a clean shutdown before any process-tree fallback.`
|
|
1373
|
+
: `By default, this command tries to start the supervisor in a detached tmux
|
|
1374
|
+
session named "${relaySupervisorTmuxSession}" so the device stays online
|
|
1375
|
+
after the launching terminal closes. If tmux is not installed, or if
|
|
1376
|
+
REMOTE_CODEX_RELAY_SUPERVISOR_TMUX=0 is set, it runs in the foreground.`;
|
|
983
1377
|
console.log(`remote-codex relay-supervisor
|
|
984
1378
|
|
|
985
1379
|
Run the private-machine supervisor backend in relay mode. This is the process
|
|
@@ -997,14 +1391,11 @@ This command automatically sets for the child supervisor:
|
|
|
997
1391
|
REMOTE_CODEX_MODE=relay
|
|
998
1392
|
|
|
999
1393
|
Default process management:
|
|
1000
|
-
|
|
1001
|
-
session named "${relaySupervisorTmuxSession}" so the device stays online
|
|
1002
|
-
after the launching terminal closes. If tmux is not installed, or if
|
|
1003
|
-
REMOTE_CODEX_RELAY_SUPERVISOR_TMUX=0 is set, it runs in the foreground.
|
|
1394
|
+
${processManagement}
|
|
1004
1395
|
|
|
1005
1396
|
Use "remote-codex relay-supervisor run" for explicit foreground/debug mode.
|
|
1006
|
-
Use "remote-codex relay-supervisor status" to inspect the
|
|
1007
|
-
Use "remote-codex relay-supervisor stop" to stop the
|
|
1397
|
+
Use "remote-codex relay-supervisor status" to inspect the managed instance.
|
|
1398
|
+
Use "remote-codex relay-supervisor stop" to stop the managed instance.
|
|
1008
1399
|
|
|
1009
1400
|
Interactive setup:
|
|
1010
1401
|
When REMOTE_CODEX_RELAY_SERVER_URL or REMOTE_CODEX_RELAY_AGENT_TOKEN is
|
|
@@ -1013,8 +1404,8 @@ Interactive setup:
|
|
|
1013
1404
|
${relaySupervisorConfigPath}
|
|
1014
1405
|
|
|
1015
1406
|
The saved config also includes generated local supervisor auth/session values
|
|
1016
|
-
and copied setup values such as REMOTE_CODEX_RELAY_SUPERVISOR_PORT, so
|
|
1017
|
-
|
|
1407
|
+
and copied setup values such as REMOTE_CODEX_RELAY_SUPERVISOR_PORT, so a
|
|
1408
|
+
managed child process can start with the same effective configuration.
|
|
1018
1409
|
|
|
1019
1410
|
Use "remote-codex relay-supervisor reset" to delete the saved config.
|
|
1020
1411
|
|
package/docs/windows.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Native Windows support
|
|
2
|
+
|
|
3
|
+
Remote Codex Relay Supervisor contains a native Windows implementation with the following initial contract. Until the Windows CI and Windows 11 release checklist pass, treat it as a release candidate rather than a verified production support claim:
|
|
4
|
+
|
|
5
|
+
- Windows 11 x64;
|
|
6
|
+
- Node.js 22 LTS;
|
|
7
|
+
- native Codex provider (`codex.exe` or the npm `codex.cmd` shim);
|
|
8
|
+
- local workspaces under the current user's home directory or another local drive;
|
|
9
|
+
- Relay mode over the existing WebSocket protocol.
|
|
10
|
+
|
|
11
|
+
The built-in Terminal plugin is intentionally unavailable on native Windows. Windows does not need tmux, WSL, Git Bash, ConPTY, or C++ Build Tools to start the Relay Supervisor. Claude Code, OpenCode, UNC workspaces, Windows ARM64, and unattended system services are not part of the first supported release.
|
|
12
|
+
|
|
13
|
+
## Install prerequisites
|
|
14
|
+
|
|
15
|
+
Install Node.js 22 LTS and native Codex. The official Codex installer is:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Verify the runtime:
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
node --version
|
|
25
|
+
codex --version
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Foreground mode
|
|
29
|
+
|
|
30
|
+
Set the Relay connection values, then run the Supervisor in the foreground:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
$env:REMOTE_CODEX_RELAY_SERVER_URL = 'wss://relay.example.com'
|
|
34
|
+
$env:REMOTE_CODEX_RELAY_AGENT_TOKEN = '<device-token>'
|
|
35
|
+
remote-codex relay-supervisor run
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The interactive setup can save these values in `%USERPROFILE%\.remote-codex\relay-supervisor.json`. The file contains secrets and must not be copied into a repository or attached to bug reports.
|
|
39
|
+
|
|
40
|
+
## Managed background mode
|
|
41
|
+
|
|
42
|
+
On Windows, `start`, `status`, and `stop` use a detached process with an authenticated local named-pipe control channel:
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
remote-codex relay-supervisor start
|
|
46
|
+
remote-codex relay-supervisor status
|
|
47
|
+
remote-codex relay-supervisor stop
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Logs are written to `%USERPROFILE%\.remote-codex\logs\relay-supervisor.log`. Shutdown first asks Fastify, Relay, Codex app-server, and SQLite to close cleanly; forced process-tree termination is only a timeout fallback after instance identity verification.
|
|
51
|
+
|
|
52
|
+
## Start automatically after logon
|
|
53
|
+
|
|
54
|
+
Run the bundled PowerShell installer as the same user who owns the Codex login and workspaces:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
& '<remote-codex-package>\scripts\windows\install-relay-supervisor-task.ps1'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The scheduled task uses the current user's interactive token. It does not run as `LocalSystem`, and secrets are not included in task arguments.
|
|
61
|
+
|
|
62
|
+
To remove the task while preserving configuration and databases:
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
& '<remote-codex-package>\scripts\windows\uninstall-relay-supervisor-task.ps1'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Pass `-PurgeData` only when the data under `%USERPROFILE%\.remote-codex` should also be permanently removed.
|
|
69
|
+
|
|
70
|
+
## Diagnostics
|
|
71
|
+
|
|
72
|
+
Useful non-secret diagnostics:
|
|
73
|
+
|
|
74
|
+
```powershell
|
|
75
|
+
Get-Command node,npm,codex,git | Format-List Name,Source,Version
|
|
76
|
+
remote-codex --version
|
|
77
|
+
remote-codex relay-supervisor status
|
|
78
|
+
Get-Content "$env:USERPROFILE\.remote-codex\logs\relay-supervisor.log" -Tail 100
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Redact tokens, passwords, session secrets, authorization headers, and the `controlToken` in `relay-supervisor-state.json` before sharing diagnostics.
|