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.
Files changed (28) hide show
  1. package/README.md +9 -0
  2. package/apps/relay-server/dist/index.js +17 -1
  3. package/apps/supervisor-api/dist/index.js +2189 -1798
  4. package/apps/supervisor-web/dist/assets/index-BO9S3vTX.css +1 -0
  5. package/apps/supervisor-web/dist/assets/index-GqVDOqbI.js +22 -0
  6. package/apps/supervisor-web/dist/assets/{thread-ui-Dmrigdek.js → thread-ui-BWC_ljvN.js} +11 -11
  7. package/apps/supervisor-web/dist/index.html +3 -3
  8. package/bin/remote-codex.mjs +426 -35
  9. package/docs/windows.md +81 -0
  10. package/package.json +14 -3
  11. package/packages/claude/src/runtimeAdapter.test.ts +32 -4
  12. package/packages/claude/src/runtimeAdapter.ts +21 -22
  13. package/packages/codex/src/appServerManager.test.ts +47 -0
  14. package/packages/codex/src/appServerManager.ts +6 -2
  15. package/packages/codex/src/runtimeAdapter.test.ts +9 -2
  16. package/packages/opencode/src/historyItems.ts +7 -6
  17. package/packages/opencode/src/runtimeAdapter.ts +7 -11
  18. package/packages/process-runtime/src/index.test.ts +132 -0
  19. package/packages/process-runtime/src/index.ts +253 -0
  20. package/packages/shared/src/index.ts +12 -0
  21. package/scripts/service-manager.mjs +112 -4
  22. package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
  23. package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
  24. package/scripts/windows/relay-smoke.ps1 +16 -0
  25. package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
  26. package/scripts/windows/validate-real-codex.mjs +680 -0
  27. package/apps/supervisor-web/dist/assets/index-BT0SM9C-.js +0 -21
  28. package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
@@ -0,0 +1,262 @@
1
+ import crypto from 'node:crypto';
2
+ import fsp from 'node:fs/promises';
3
+ import net from 'node:net';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import crossSpawn from 'cross-spawn';
9
+
10
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
11
+ const packageRoot = path.resolve(scriptDir, '..');
12
+ const temporaryRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'Remote Codex Relay Smoke '));
13
+ const workspaceRoot = path.join(temporaryRoot, 'workspace root 开发');
14
+ const relayPort = await reservePort();
15
+ const supervisorPort = await reservePort();
16
+ const relayBaseUrl = `http://127.0.0.1:${relayPort}`;
17
+ const processes = [];
18
+
19
+ try {
20
+ await fsp.mkdir(workspaceRoot, { recursive: true });
21
+ const relayEnvironment = {
22
+ REMOTE_CODEX_ADMIN_USERNAME: 'smoke-admin',
23
+ REMOTE_CODEX_ADMIN_PASSWORD: 'smoke-admin-password',
24
+ REMOTE_CODEX_RELAY_SESSION_SECRET: 'smoke-session-secret-32-characters',
25
+ REMOTE_CODEX_RELAY_REGISTRATION_ENABLED: 'true',
26
+ REMOTE_CODEX_RELAY_DATA_DIR: path.join(temporaryRoot, 'relay data'),
27
+ REMOTE_CODEX_RELAY_HOST: '127.0.0.1',
28
+ REMOTE_CODEX_RELAY_PORT: String(relayPort),
29
+ };
30
+ const relayEntry = path.join(packageRoot, 'apps', 'relay-server', 'dist', 'index.js');
31
+ const relay = startProcess(relayEntry, relayEnvironment);
32
+ processes.push(relay);
33
+ await waitForHttp(`${relayBaseUrl}/healthz`, relay, 20_000);
34
+
35
+ const username = `relay-smoke-${crypto.randomBytes(4).toString('hex')}`;
36
+ const registration = await jsonRequest(`${relayBaseUrl}/relay/auth/register`, {
37
+ method: 'POST',
38
+ body: { email: `${username}@example.test`, username, password: 'relay-smoke-password' },
39
+ });
40
+ const userToken = requiredString(registration.token, 'registration token');
41
+ const deviceRegistration = await jsonRequest(`${relayBaseUrl}/relay/devices`, {
42
+ method: 'POST', token: userToken, body: { name: `Windows smoke ${process.platform}` },
43
+ });
44
+ const deviceId = requiredString(deviceRegistration.device?.id, 'device id');
45
+ const deviceToken = requiredString(deviceRegistration.token, 'device token');
46
+
47
+ const instanceId = crypto.randomUUID();
48
+ const controlToken = crypto.randomBytes(32).toString('base64url');
49
+ const controlEndpoint = process.platform === 'win32'
50
+ ? `\\\\.\\pipe\\remote-codex-relay-smoke-${instanceId}`
51
+ : path.join('/tmp', `rc-relay-${instanceId.slice(0, 12)}.sock`);
52
+ const supervisor = startProcess(path.join(packageRoot, 'apps', 'supervisor-api', 'dist', 'index.js'), {
53
+ HOST: '127.0.0.1',
54
+ PORT: String(supervisorPort),
55
+ DATABASE_URL: path.join(temporaryRoot, 'supervisor.sqlite'),
56
+ WORKSPACE_ROOT: workspaceRoot,
57
+ CODEX_HOME: path.join(temporaryRoot, '.codex'),
58
+ REMOTE_CODEX_PACKAGE_ROOT: packageRoot,
59
+ REMOTE_CODEX_MODE: 'relay',
60
+ REMOTE_CODEX_ADMIN_USERNAME: 'supervisor-admin',
61
+ REMOTE_CODEX_ADMIN_PASSWORD: 'supervisor-admin-password',
62
+ REMOTE_CODEX_SESSION_SECRET: 'supervisor-session-secret-32-characters',
63
+ REMOTE_CODEX_RELAY_SERVER_URL: `ws://127.0.0.1:${relayPort}`,
64
+ REMOTE_CODEX_RELAY_AGENT_TOKEN: deviceToken,
65
+ REMOTE_CODEX_ENABLED_AGENT_PROVIDERS: 'relay-smoke-fake-only',
66
+ REMOTE_CODEX_E2E_FAKE_RUNTIME: 'true',
67
+ REMOTE_CODEX_LIFECYCLE_CONTROL_ENDPOINT: controlEndpoint,
68
+ REMOTE_CODEX_LIFECYCLE_CONTROL_TOKEN: controlToken,
69
+ REMOTE_CODEX_LIFECYCLE_INSTANCE_ID: instanceId,
70
+ });
71
+ processes.push(supervisor);
72
+
73
+ const deviceApi = `${relayBaseUrl}/relay/devices/${deviceId}`;
74
+ await waitForHttp(`${deviceApi}/healthz`, supervisor, 30_000, userToken);
75
+ const workspace = await jsonRequest(`${deviceApi}/api/workspaces`, {
76
+ method: 'POST', token: userToken,
77
+ body: { absPath: path.join(workspaceRoot, 'project with spaces 项目') },
78
+ });
79
+ const workspaceId = requiredString(workspace.id, 'workspace id');
80
+ const thread = await jsonRequest(`${deviceApi}/api/threads/start`, {
81
+ method: 'POST', token: userToken,
82
+ body: {
83
+ workspaceId, provider: 'claude', model: 'ios-e2e-stream',
84
+ approvalMode: 'yolo', title: 'Windows relay smoke',
85
+ },
86
+ });
87
+ const threadId = requiredString(thread.id, 'thread id');
88
+ await jsonRequest(`${deviceApi}/api/threads/${threadId}/prompt`, {
89
+ method: 'POST', token: userToken, body: { prompt: 'WINDOWS_RELAY_SMOKE' },
90
+ });
91
+ await waitForJson(
92
+ `${deviceApi}/api/threads/${threadId}`, userToken,
93
+ (value) => {
94
+ const serialized = JSON.stringify(value);
95
+ return serialized.includes('IOS_STREAM_DELTA_READY') &&
96
+ !serialized.includes('IOS_STREAM_COMPLETED');
97
+ }, 5_000,
98
+ );
99
+ const detail = await waitForJson(
100
+ `${deviceApi}/api/threads/${threadId}`, userToken,
101
+ (value) => JSON.stringify(value).includes('IOS_STREAM_COMPLETED'), 35_000,
102
+ );
103
+ const lastTurn = detail.turns?.at?.(-1);
104
+ if (lastTurn?.status !== 'completed') {
105
+ throw new Error(`Fake runtime turn did not complete: ${JSON.stringify(lastTurn)}`);
106
+ }
107
+
108
+ await jsonRequest(`${deviceApi}/api/threads/${threadId}/prompt`, {
109
+ method: 'POST', token: userToken, body: { prompt: 'WINDOWS_RELAY_FOLLOWUP' },
110
+ });
111
+ await waitForJson(
112
+ `${deviceApi}/api/threads/${threadId}`, userToken,
113
+ (value) => value.turns?.length === 2 && value.turns.at(-1)?.status === 'completed', 35_000,
114
+ );
115
+
116
+ await stopProcess(relay);
117
+ const restartedRelay = startProcess(relayEntry, relayEnvironment);
118
+ processes.push(restartedRelay);
119
+ await waitForHttp(`${relayBaseUrl}/healthz`, restartedRelay, 20_000);
120
+ await waitForHttp(`${deviceApi}/healthz`, supervisor, 30_000, userToken);
121
+ const reloadedAfterReconnect = await jsonRequest(`${deviceApi}/api/threads/${threadId}`, {
122
+ token: userToken,
123
+ });
124
+ if (reloadedAfterReconnect.turns?.length !== 2) {
125
+ throw new Error('Transcript did not survive Relay disconnect and reconnect.');
126
+ }
127
+
128
+ const status = await requestControl({ controlEndpoint, controlToken, instanceId }, 'status');
129
+ if (status.ok !== true || status.instanceId !== instanceId) {
130
+ throw new Error(`Unexpected lifecycle status: ${JSON.stringify(status)}`);
131
+ }
132
+ await requestControl({ controlEndpoint, controlToken, instanceId }, 'shutdown');
133
+ await waitForExit(supervisor, 10_000);
134
+ console.log(`Relay supervisor smoke passed on ${process.platform}: device ${deviceId}, thread ${threadId}.`);
135
+ } catch (error) {
136
+ const diagnostics = processes
137
+ .map((child) => `${path.basename(child.entry)} output:\n${child.output()}`).join('\n');
138
+ throw new Error(`${error instanceof Error ? error.message : String(error)}\n${diagnostics}`);
139
+ } finally {
140
+ await Promise.allSettled(processes.map((child) => stopProcess(child)));
141
+ await fsp.rm(temporaryRoot, { recursive: true, force: true });
142
+ }
143
+
144
+ function startProcess(entry, additionalEnv) {
145
+ const child = crossSpawn(process.execPath, [entry], {
146
+ cwd: packageRoot,
147
+ windowsHide: true,
148
+ env: { ...process.env, NODE_ENV: 'production', LOG_LEVEL: 'warn', ...additionalEnv },
149
+ stdio: ['ignore', 'pipe', 'pipe'],
150
+ });
151
+ let output = '';
152
+ child.stdout?.on('data', (chunk) => { output += String(chunk); });
153
+ child.stderr?.on('data', (chunk) => { output += String(chunk); });
154
+ return Object.assign(child, { entry, output: () => output });
155
+ }
156
+
157
+ async function jsonRequest(url, options = {}) {
158
+ const response = await fetch(url, {
159
+ method: options.method ?? 'GET',
160
+ headers: {
161
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
162
+ ...(options.body ? { 'content-type': 'application/json' } : {}),
163
+ },
164
+ ...(options.body ? { body: JSON.stringify(options.body) } : {}),
165
+ signal: AbortSignal.timeout(5_000),
166
+ });
167
+ const text = await response.text();
168
+ let value;
169
+ try { value = text ? JSON.parse(text) : null; } catch { value = text; }
170
+ if (!response.ok) {
171
+ throw new Error(`${options.method ?? 'GET'} ${url} returned ${response.status}: ${text}`);
172
+ }
173
+ return value;
174
+ }
175
+
176
+ async function waitForHttp(url, child, timeoutMs, token) {
177
+ const deadline = Date.now() + timeoutMs;
178
+ while (Date.now() < deadline) {
179
+ if (child.exitCode !== null) throw new Error(`${path.basename(child.entry)} exited with ${child.exitCode}.`);
180
+ try { await jsonRequest(url, { token }); return; } catch { await delay(200); }
181
+ }
182
+ throw new Error(`Timed out waiting for ${url}.`);
183
+ }
184
+
185
+ async function waitForJson(url, token, predicate, timeoutMs) {
186
+ const deadline = Date.now() + timeoutMs;
187
+ let latest = null;
188
+ while (Date.now() < deadline) {
189
+ latest = await jsonRequest(url, { token });
190
+ if (predicate(latest)) return latest;
191
+ await delay(100);
192
+ }
193
+ throw new Error(`Timed out waiting for expected response from ${url}: ${JSON.stringify(latest)}`);
194
+ }
195
+
196
+ function requiredString(value, label) {
197
+ if (typeof value !== 'string' || !value) throw new Error(`Missing ${label}: ${JSON.stringify(value)}`);
198
+ return value;
199
+ }
200
+
201
+ function reservePort() {
202
+ return new Promise((resolve, reject) => {
203
+ const server = net.createServer();
204
+ server.once('error', reject);
205
+ server.listen(0, '127.0.0.1', () => {
206
+ const address = server.address();
207
+ const port = typeof address === 'object' && address ? address.port : null;
208
+ server.close(() => port ? resolve(port) : reject(new Error('Unable to reserve a port.')));
209
+ });
210
+ });
211
+ }
212
+
213
+ function requestControl(state, action) {
214
+ return new Promise((resolve, reject) => {
215
+ const socket = net.createConnection(state.controlEndpoint);
216
+ const timer = setTimeout(() => { socket.destroy(); reject(new Error('Lifecycle control timed out.')); }, 2_000);
217
+ let output = '';
218
+ socket.setEncoding('utf8');
219
+ socket.once('connect', () => socket.write(`${JSON.stringify({
220
+ action, token: state.controlToken, instanceId: state.instanceId,
221
+ })}\n`));
222
+ socket.on('data', (chunk) => {
223
+ output += chunk;
224
+ const newline = output.indexOf('\n');
225
+ if (newline < 0) return;
226
+ clearTimeout(timer);
227
+ socket.end();
228
+ resolve(JSON.parse(output.slice(0, newline)));
229
+ });
230
+ socket.once('error', (error) => { clearTimeout(timer); reject(error); });
231
+ });
232
+ }
233
+
234
+ function waitForExit(child, timeoutMs) {
235
+ if (child.exitCode !== null) return Promise.resolve();
236
+ return new Promise((resolve, reject) => {
237
+ const timer = setTimeout(() => reject(new Error(`${path.basename(child.entry)} did not exit cleanly.`)), timeoutMs);
238
+ child.once('exit', () => { clearTimeout(timer); resolve(); });
239
+ });
240
+ }
241
+
242
+ async function stopProcess(child) {
243
+ if (child.exitCode !== null) return;
244
+ child.kill('SIGTERM');
245
+ try {
246
+ await waitForExit(child, 3_000);
247
+ } catch {
248
+ if (process.platform === 'win32' && child.pid) {
249
+ await new Promise((resolve) => {
250
+ const killer = crossSpawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
251
+ killer.once('close', resolve);
252
+ killer.once('error', resolve);
253
+ });
254
+ } else {
255
+ child.kill('SIGKILL');
256
+ }
257
+ }
258
+ }
259
+
260
+ function delay(milliseconds) {
261
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
262
+ }
@@ -0,0 +1,44 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$TaskName = 'Remote Codex Relay Supervisor',
4
+ [string]$PackageRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
5
+ )
6
+
7
+ $ErrorActionPreference = 'Stop'
8
+ $node = (Get-Command node.exe -ErrorAction Stop).Source
9
+ $entry = Join-Path $PackageRoot 'bin\remote-codex.mjs'
10
+ if (-not (Test-Path -LiteralPath $entry -PathType Leaf)) {
11
+ throw "Remote Codex entrypoint was not found: $entry"
12
+ }
13
+
14
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
15
+ $action = New-ScheduledTaskAction `
16
+ -Execute $node `
17
+ -Argument ('"{0}" relay-supervisor start' -f $entry) `
18
+ -WorkingDirectory $PackageRoot
19
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User $identity
20
+ $principal = New-ScheduledTaskPrincipal `
21
+ -UserId $identity `
22
+ -LogonType Interactive `
23
+ -RunLevel Limited
24
+ $settings = New-ScheduledTaskSettingsSet `
25
+ -AllowStartIfOnBatteries `
26
+ -DontStopIfGoingOnBatteries `
27
+ -RestartCount 5 `
28
+ -RestartInterval (New-TimeSpan -Minutes 1) `
29
+ -ExecutionTimeLimit ([TimeSpan]::Zero) `
30
+ -MultipleInstances IgnoreNew
31
+
32
+ Register-ScheduledTask `
33
+ -TaskName $TaskName `
34
+ -Action $action `
35
+ -Trigger $trigger `
36
+ -Principal $principal `
37
+ -Settings $settings `
38
+ -Description 'Starts the current user Remote Codex Relay Supervisor after logon.' `
39
+ -Force | Out-Null
40
+
41
+ Write-Host "Installed scheduled task: $TaskName"
42
+ Write-Host 'Starting the relay supervisor now...'
43
+ & $node $entry relay-supervisor start
44
+ exit $LASTEXITCODE
@@ -0,0 +1,16 @@
1
+ $ErrorActionPreference = 'Stop'
2
+
3
+ $PackageRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
4
+ $SmokeScript = Join-Path $PackageRoot 'scripts\verify-relay-supervisor-smoke.mjs'
5
+ $Node = (Get-Command node.exe -ErrorAction Stop).Source
6
+
7
+ Push-Location $PackageRoot
8
+ try {
9
+ & $Node $SmokeScript
10
+ if ($LASTEXITCODE -ne 0) {
11
+ throw "Relay smoke test exited with code $LASTEXITCODE."
12
+ }
13
+ }
14
+ finally {
15
+ Pop-Location
16
+ }
@@ -0,0 +1,27 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$TaskName = 'Remote Codex Relay Supervisor',
4
+ [switch]$PurgeData,
5
+ [string]$PackageRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
6
+ )
7
+
8
+ $ErrorActionPreference = 'Stop'
9
+ $node = (Get-Command node.exe -ErrorAction Stop).Source
10
+ $entry = Join-Path $PackageRoot 'bin\remote-codex.mjs'
11
+
12
+ if (Test-Path -LiteralPath $entry -PathType Leaf) {
13
+ & $node $entry relay-supervisor stop
14
+ }
15
+
16
+ Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
17
+ Write-Host "Removed scheduled task: $TaskName"
18
+
19
+ if ($PurgeData) {
20
+ $dataDirectory = Join-Path $env:USERPROFILE '.remote-codex'
21
+ if (Test-Path -LiteralPath $dataDirectory -PathType Container) {
22
+ Remove-Item -LiteralPath $dataDirectory -Recurse -Force
23
+ Write-Host "Removed data directory: $dataDirectory"
24
+ }
25
+ } else {
26
+ Write-Host 'Configuration, logs, and databases were preserved under %USERPROFILE%\.remote-codex.'
27
+ }