remote-codex 0.11.44 → 0.11.49
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 +84 -5
- package/apps/relay-server/dist/index.js +18 -2
- package/apps/supervisor-api/dist/index.js +10034 -2782
- package/apps/supervisor-web/dist/assets/c-BIGW1oBm.js +1 -0
- package/apps/supervisor-web/dist/assets/{core-DODVy7wn.js → core-B0prQzhr.js} +1 -1
- package/apps/supervisor-web/dist/assets/cpp-DIPi6g--.js +1 -0
- package/apps/supervisor-web/dist/assets/csharp-DSvCPggb.js +1 -0
- package/apps/supervisor-web/dist/assets/go-C27-OAKa.js +1 -0
- package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +22 -0
- package/apps/supervisor-web/dist/assets/index-Dy8PgXgw.css +1 -0
- package/apps/supervisor-web/dist/assets/java-CylS5w8V.js +1 -0
- package/apps/supervisor-web/dist/assets/{markdown-vendor-RZk8L7-L.js → markdown-vendor-BG6PurxI.js} +33 -33
- package/apps/supervisor-web/dist/assets/ruby-B6AkvBWc.js +1 -0
- package/apps/supervisor-web/dist/assets/rust-B1yitclQ.js +1 -0
- package/apps/supervisor-web/dist/assets/{shellscript-CEILq0vU.js → shellscript-DfDnw5Jg.js} +1 -1
- package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +3968 -0
- package/apps/supervisor-web/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/apps/supervisor-web/dist/index.html +4 -4
- package/bin/remote-codex.mjs +426 -35
- package/docs/windows-device-manager.zh.md +143 -0
- package/docs/windows-device-setup.zh.md +263 -0
- package/docs/windows-one-click-installer-research.zh.md +481 -0
- package/docs/windows.md +87 -0
- package/package.json +20 -4
- package/packages/acp/src/agent-catalog.test.ts +57 -0
- package/packages/acp/src/agent-catalog.ts +361 -0
- package/packages/acp/src/catalog-runtime.test.ts +99 -0
- package/packages/acp/src/catalog-runtime.ts +507 -0
- package/packages/acp/src/codex-environment.test.ts +58 -0
- package/packages/acp/src/codex-environment.ts +33 -0
- package/packages/acp/src/index.ts +5 -0
- package/packages/acp/src/item-mapper.test.ts +137 -0
- package/packages/acp/src/item-mapper.ts +473 -0
- package/packages/acp/src/runtimeAdapter.test.ts +79 -0
- package/packages/acp/src/runtimeAdapter.ts +1193 -0
- package/packages/acp/src/terminal-service.test.ts +31 -0
- package/packages/acp/src/terminal-service.ts +137 -0
- package/packages/agent-runtime/src/runtime-errors.ts +1 -0
- package/packages/agent-runtime/src/types.ts +8 -0
- package/packages/claude/src/runtimeAdapter.test.ts +2 -2
- package/packages/claude/src/runtimeAdapter.ts +10 -19
- 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/db/migrations/0030_thread_agent_id.sql +9 -0
- package/packages/db/src/repositories.ts +3 -0
- package/packages/db/src/schema.ts +1 -0
- 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/agent-providers.ts +10 -1
- package/packages/shared/src/index.ts +36 -0
- package/scripts/service-manager.mjs +112 -4
- package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
- package/scripts/windows/build-device-manager.ps1 +64 -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-BcCLYWAf.css +0 -1
- package/apps/supervisor-web/dist/assets/index-CO8cU7a0.js +0 -21
- package/apps/supervisor-web/dist/assets/thread-ui-Dmrigdek.js +0 -3714
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { ChildProcess, SpawnOptions } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import crossSpawn from 'cross-spawn';
|
|
6
|
+
|
|
7
|
+
export interface SpawnProcessOptions {
|
|
8
|
+
command: string;
|
|
9
|
+
args?: readonly string[];
|
|
10
|
+
cwd?: string;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
stdio?: SpawnOptions['stdio'];
|
|
13
|
+
detached?: boolean;
|
|
14
|
+
windowsHide?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RunProcessOptions extends Omit<SpawnProcessOptions, 'stdio' | 'detached'> {
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
maxOutputBytes?: number;
|
|
20
|
+
input?: string | Buffer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProcessResult {
|
|
24
|
+
code: number | null;
|
|
25
|
+
signal: NodeJS.Signals | null;
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
timedOut: boolean;
|
|
29
|
+
outputTruncated: boolean;
|
|
30
|
+
spawnError: Error | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ParsedCommandLine {
|
|
34
|
+
command: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a persisted/display command into argv without evaluating a shell.
|
|
42
|
+
* Supports the quoting used by the built-in runtime install commands. Shell
|
|
43
|
+
* operators and environment expansion are intentionally treated as literals.
|
|
44
|
+
*/
|
|
45
|
+
export function parseCommandLine(value: string): ParsedCommandLine {
|
|
46
|
+
const tokens: string[] = [];
|
|
47
|
+
let token = '';
|
|
48
|
+
let quote: 'single' | 'double' | null = null;
|
|
49
|
+
let tokenStarted = false;
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
52
|
+
const character = value[index]!;
|
|
53
|
+
if (quote === 'single') {
|
|
54
|
+
if (character === "'") {
|
|
55
|
+
quote = null;
|
|
56
|
+
} else {
|
|
57
|
+
token += character;
|
|
58
|
+
}
|
|
59
|
+
tokenStarted = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (quote === 'double') {
|
|
63
|
+
if (character === '"') {
|
|
64
|
+
quote = null;
|
|
65
|
+
} else if (
|
|
66
|
+
character === '\\' &&
|
|
67
|
+
(value[index + 1] === '"' || value[index + 1] === '\\')
|
|
68
|
+
) {
|
|
69
|
+
token += value[index + 1];
|
|
70
|
+
index += 1;
|
|
71
|
+
} else {
|
|
72
|
+
token += character;
|
|
73
|
+
}
|
|
74
|
+
tokenStarted = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (/\s/.test(character)) {
|
|
79
|
+
if (tokenStarted) {
|
|
80
|
+
tokens.push(token);
|
|
81
|
+
token = '';
|
|
82
|
+
tokenStarted = false;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (character === "'") {
|
|
87
|
+
quote = 'single';
|
|
88
|
+
tokenStarted = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (character === '"') {
|
|
92
|
+
quote = 'double';
|
|
93
|
+
tokenStarted = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
token += character;
|
|
97
|
+
tokenStarted = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (quote) {
|
|
101
|
+
throw new Error('Command contains an unterminated quoted argument.');
|
|
102
|
+
}
|
|
103
|
+
if (tokenStarted) {
|
|
104
|
+
tokens.push(token);
|
|
105
|
+
}
|
|
106
|
+
const [command, ...args] = tokens;
|
|
107
|
+
if (!command) {
|
|
108
|
+
throw new Error('Command is empty.');
|
|
109
|
+
}
|
|
110
|
+
return { command, args };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Spawn an executable without constructing a shell command string.
|
|
115
|
+
* cross-spawn is used so Windows PATHEXT and npm .cmd shims work consistently.
|
|
116
|
+
*/
|
|
117
|
+
export function spawnProcess(options: SpawnProcessOptions): ChildProcess {
|
|
118
|
+
return crossSpawn(options.command, [...(options.args ?? [])], {
|
|
119
|
+
cwd: options.cwd,
|
|
120
|
+
env: options.env,
|
|
121
|
+
stdio: options.stdio ?? 'pipe',
|
|
122
|
+
detached: options.detached,
|
|
123
|
+
windowsHide: options.windowsHide ?? true,
|
|
124
|
+
shell: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function runProcess(options: RunProcessOptions): Promise<ProcessResult> {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const child = spawnProcess({
|
|
131
|
+
...options,
|
|
132
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
133
|
+
});
|
|
134
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
135
|
+
let stdout: Buffer = Buffer.alloc(0);
|
|
136
|
+
let stderr: Buffer = Buffer.alloc(0);
|
|
137
|
+
let timedOut = false;
|
|
138
|
+
let outputTruncated = false;
|
|
139
|
+
let spawnError: Error | null = null;
|
|
140
|
+
let settled = false;
|
|
141
|
+
|
|
142
|
+
const append = (current: Buffer, chunk: Buffer | string) => {
|
|
143
|
+
const nextChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
144
|
+
const remaining = Math.max(0, maxOutputBytes - current.byteLength);
|
|
145
|
+
if (nextChunk.byteLength > remaining) {
|
|
146
|
+
outputTruncated = true;
|
|
147
|
+
}
|
|
148
|
+
return remaining === 0
|
|
149
|
+
? current
|
|
150
|
+
: Buffer.concat([current, nextChunk.subarray(0, remaining)]);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
child.stdout?.on('data', (chunk: Buffer | string) => {
|
|
154
|
+
stdout = append(stdout, chunk);
|
|
155
|
+
});
|
|
156
|
+
child.stderr?.on('data', (chunk: Buffer | string) => {
|
|
157
|
+
stderr = append(stderr, chunk);
|
|
158
|
+
});
|
|
159
|
+
child.on('error', (error) => {
|
|
160
|
+
spawnError = error;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const finish = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
164
|
+
if (settled) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
settled = true;
|
|
168
|
+
if (timer) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
}
|
|
171
|
+
resolve({
|
|
172
|
+
code: spawnError ? null : code,
|
|
173
|
+
signal,
|
|
174
|
+
stdout: stdout.toString('utf8'),
|
|
175
|
+
stderr: stderr.toString('utf8'),
|
|
176
|
+
timedOut,
|
|
177
|
+
outputTruncated,
|
|
178
|
+
spawnError,
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
child.on('close', finish);
|
|
183
|
+
const timer = options.timeoutMs && options.timeoutMs > 0
|
|
184
|
+
? setTimeout(() => {
|
|
185
|
+
timedOut = true;
|
|
186
|
+
child.kill('SIGTERM');
|
|
187
|
+
}, options.timeoutMs)
|
|
188
|
+
: null;
|
|
189
|
+
|
|
190
|
+
if (options.input !== undefined) {
|
|
191
|
+
child.stdin?.end(options.input);
|
|
192
|
+
} else {
|
|
193
|
+
child.stdin?.end();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function pathEnvironmentValue(env: NodeJS.ProcessEnv) {
|
|
199
|
+
const entry = Object.entries(env).find(([key]) => key.toLowerCase() === 'path');
|
|
200
|
+
return entry?.[1] ?? '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function windowsExtensions(command: string, env: NodeJS.ProcessEnv) {
|
|
204
|
+
if (path.extname(command)) {
|
|
205
|
+
return [''];
|
|
206
|
+
}
|
|
207
|
+
const pathExtEntry = Object.entries(env).find(([key]) => key.toLowerCase() === 'pathext');
|
|
208
|
+
const extensions = (pathExtEntry?.[1] ?? '.COM;.EXE;.BAT;.CMD')
|
|
209
|
+
.split(';')
|
|
210
|
+
.map((extension) => extension.trim())
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
return ['', ...extensions];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function isFile(candidate: string) {
|
|
216
|
+
try {
|
|
217
|
+
return (await fs.stat(candidate)).isFile();
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Resolve a command for diagnostics only. Execution must still use spawnProcess. */
|
|
224
|
+
export async function resolveExecutable(
|
|
225
|
+
command: string,
|
|
226
|
+
options: {
|
|
227
|
+
cwd?: string;
|
|
228
|
+
env?: NodeJS.ProcessEnv;
|
|
229
|
+
platform?: NodeJS.Platform;
|
|
230
|
+
} = {},
|
|
231
|
+
): Promise<string | null> {
|
|
232
|
+
const env = options.env ?? process.env;
|
|
233
|
+
const platform = options.platform ?? process.platform;
|
|
234
|
+
const cwd = options.cwd ?? process.cwd();
|
|
235
|
+
const hasPathSeparator = command.includes('/') || command.includes('\\');
|
|
236
|
+
const searchDirectories = hasPathSeparator
|
|
237
|
+
? ['']
|
|
238
|
+
: pathEnvironmentValue(env).split(path.delimiter).filter(Boolean);
|
|
239
|
+
const extensions = platform === 'win32' ? windowsExtensions(command, env) : [''];
|
|
240
|
+
|
|
241
|
+
for (const directory of searchDirectories) {
|
|
242
|
+
const base = hasPathSeparator
|
|
243
|
+
? (path.isAbsolute(command) ? command : path.resolve(cwd, command))
|
|
244
|
+
: path.join(directory, command);
|
|
245
|
+
for (const extension of extensions) {
|
|
246
|
+
const candidate = `${base}${extension}`;
|
|
247
|
+
if (await isFile(candidate)) {
|
|
248
|
+
return path.resolve(candidate);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const agentBackendIds = ['codex', 'claude', 'opencode'] as const;
|
|
1
|
+
export const agentBackendIds = ['codex', 'claude', 'opencode', 'acp'] as const;
|
|
2
2
|
|
|
3
3
|
export type AgentBackendIdDto = (typeof agentBackendIds)[number];
|
|
4
4
|
|
|
@@ -42,6 +42,15 @@ export const agentBackendMetadata: Record<AgentBackendIdDto, AgentBackendMetadat
|
|
|
42
42
|
defaultHomeDir: '.opencode',
|
|
43
43
|
defaultCommand: 'opencode',
|
|
44
44
|
},
|
|
45
|
+
acp: {
|
|
46
|
+
displayName: 'ACP Agent',
|
|
47
|
+
description: 'Agent Client Protocol runtime over stdio.',
|
|
48
|
+
defaultTransport: 'stdio',
|
|
49
|
+
homeEnvVar: 'ACP_HOME',
|
|
50
|
+
commandEnvVar: 'ACP_COMMAND',
|
|
51
|
+
defaultHomeDir: '.acp',
|
|
52
|
+
defaultCommand: 'grok agent stdio',
|
|
53
|
+
},
|
|
45
54
|
};
|
|
46
55
|
|
|
47
56
|
export function isAgentBackendId(value: unknown): value is AgentBackendIdDto {
|
|
@@ -64,6 +64,15 @@ export interface RuntimeConfigDto {
|
|
|
64
64
|
port: number;
|
|
65
65
|
workspaceRoot: string;
|
|
66
66
|
environment: string;
|
|
67
|
+
platform?: string;
|
|
68
|
+
architecture?: string;
|
|
69
|
+
nodeVersion?: string;
|
|
70
|
+
capabilities?: {
|
|
71
|
+
terminal: boolean;
|
|
72
|
+
tmux: boolean;
|
|
73
|
+
managedSignals: boolean;
|
|
74
|
+
windowsTaskScheduler: boolean;
|
|
75
|
+
};
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
export interface AuthSessionDto {
|
|
@@ -707,6 +716,28 @@ export interface ModelOptionDto {
|
|
|
707
716
|
supportsPerformanceMode?: boolean;
|
|
708
717
|
supportedReasoningEfforts: ReasoningEffortOptionDto[];
|
|
709
718
|
defaultReasoningEffort: ReasoningEffortDto | null;
|
|
719
|
+
selectionKind?: 'model' | 'agent';
|
|
720
|
+
acpAgent?: AcpAgentOptionMetadataDto | null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export type AcpAgentAvailabilityDto =
|
|
724
|
+
| 'ready'
|
|
725
|
+
| 'base_missing'
|
|
726
|
+
| 'adapter_missing'
|
|
727
|
+
| 'server_unavailable';
|
|
728
|
+
|
|
729
|
+
export interface AcpAgentOptionMetadataDto {
|
|
730
|
+
transport: 'native' | 'adapter' | 'custom';
|
|
731
|
+
availability: AcpAgentAvailabilityDto;
|
|
732
|
+
baseCommand: string;
|
|
733
|
+
baseProbeCommand: string;
|
|
734
|
+
serverCommand: string;
|
|
735
|
+
serverProbeCommand: string;
|
|
736
|
+
baseVersion: string | null;
|
|
737
|
+
serverVersion: string | null;
|
|
738
|
+
installCommand: string | null;
|
|
739
|
+
busy: boolean;
|
|
740
|
+
statusMessage: string;
|
|
710
741
|
}
|
|
711
742
|
|
|
712
743
|
export interface VersionDto {
|
|
@@ -920,6 +951,7 @@ export interface ThreadDto {
|
|
|
920
951
|
id: string;
|
|
921
952
|
workspaceId: string;
|
|
922
953
|
provider: AgentBackendIdDto;
|
|
954
|
+
agentId?: string | null;
|
|
923
955
|
providerSessionId: string | null;
|
|
924
956
|
source: ThreadSourceDto;
|
|
925
957
|
title: string;
|
|
@@ -1066,6 +1098,9 @@ export interface PluginManifestDto {
|
|
|
1066
1098
|
export interface PluginDto extends PluginManifestDto {
|
|
1067
1099
|
enabled: boolean;
|
|
1068
1100
|
source?: 'builtin' | 'imported' | null;
|
|
1101
|
+
available?: boolean;
|
|
1102
|
+
unavailableReasonCode?: 'unsupported_platform' | null;
|
|
1103
|
+
unavailableReason?: string | null;
|
|
1069
1104
|
}
|
|
1070
1105
|
|
|
1071
1106
|
export interface UpdatePluginInput {
|
|
@@ -1582,6 +1617,7 @@ export interface CreateThreadInput {
|
|
|
1582
1617
|
workspaceId: string;
|
|
1583
1618
|
title?: string;
|
|
1584
1619
|
provider?: AgentBackendIdDto;
|
|
1620
|
+
agentId?: string;
|
|
1585
1621
|
model: string;
|
|
1586
1622
|
reasoningEffort?: ReasoningEffortDto | null;
|
|
1587
1623
|
approvalMode: ApprovalMode;
|
|
@@ -2,9 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import net from 'node:net';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
8
9
|
|
|
9
10
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const repoRoot = path.resolve(scriptDir, '..');
|
|
@@ -67,6 +68,12 @@ async function startService() {
|
|
|
67
68
|
await assertTcpPortAvailable(apiHost, apiPort, 'API');
|
|
68
69
|
await assertTcpPortAvailable(serviceHost, servicePort, 'Web');
|
|
69
70
|
|
|
71
|
+
const apiInstanceId = crypto.randomUUID();
|
|
72
|
+
const apiControlToken = crypto.randomBytes(32).toString('base64url');
|
|
73
|
+
const apiControlEndpoint = process.platform === 'win32'
|
|
74
|
+
? `\\\\.\\pipe\\remote-codex-service-${crypto.createHash('sha256').update(serviceDir).digest('hex').slice(0, 24)}`
|
|
75
|
+
: path.join(serviceDir, 'api-control.sock');
|
|
76
|
+
|
|
70
77
|
const apiPid = spawnDetached(process.execPath, [apiEntry], apiLogPath, {
|
|
71
78
|
NODE_ENV: 'production',
|
|
72
79
|
HOST: apiHost,
|
|
@@ -76,6 +83,9 @@ async function startService() {
|
|
|
76
83
|
REMOTE_CODEX_PACKAGE_ROOT: repoRoot,
|
|
77
84
|
REMOTE_CODEX_DISABLE_BUILD_RESTART:
|
|
78
85
|
process.env.REMOTE_CODEX_DISABLE_BUILD_RESTART ?? (supportsSourceRestart ? 'false' : 'true'),
|
|
86
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_ENDPOINT: apiControlEndpoint,
|
|
87
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_TOKEN: apiControlToken,
|
|
88
|
+
REMOTE_CODEX_LIFECYCLE_INSTANCE_ID: apiInstanceId,
|
|
79
89
|
});
|
|
80
90
|
|
|
81
91
|
try {
|
|
@@ -108,11 +118,14 @@ async function startService() {
|
|
|
108
118
|
apiHost,
|
|
109
119
|
apiPort,
|
|
110
120
|
apiPid,
|
|
121
|
+
apiInstanceId,
|
|
122
|
+
apiControlToken,
|
|
123
|
+
apiControlEndpoint,
|
|
111
124
|
webPid,
|
|
112
125
|
apiLogPath,
|
|
113
126
|
webLogPath,
|
|
114
127
|
};
|
|
115
|
-
await
|
|
128
|
+
await writePrivateState(state);
|
|
116
129
|
|
|
117
130
|
console.log(`Started supervisor service.`);
|
|
118
131
|
console.log(`Web: http://${serviceHost}:${servicePort} (pid ${webPid})`);
|
|
@@ -305,8 +318,14 @@ async function probeHttp(url) {
|
|
|
305
318
|
}
|
|
306
319
|
|
|
307
320
|
async function stopState(state) {
|
|
308
|
-
|
|
309
|
-
|
|
321
|
+
stopPid(state.webPid);
|
|
322
|
+
let apiShutdownRequested = false;
|
|
323
|
+
if (state.apiControlEndpoint && state.apiControlToken && state.apiInstanceId) {
|
|
324
|
+
const response = await requestLifecycleControl(state, 'shutdown').catch(() => null);
|
|
325
|
+
apiShutdownRequested = response?.ok === true && response.instanceId === state.apiInstanceId;
|
|
326
|
+
}
|
|
327
|
+
if (!apiShutdownRequested) {
|
|
328
|
+
stopPid(state.apiPid);
|
|
310
329
|
}
|
|
311
330
|
|
|
312
331
|
const deadline = Date.now() + 5_000;
|
|
@@ -331,6 +350,44 @@ async function readState() {
|
|
|
331
350
|
}
|
|
332
351
|
}
|
|
333
352
|
|
|
353
|
+
async function writePrivateState(state) {
|
|
354
|
+
await fsp.mkdir(serviceDir, { recursive: true, mode: 0o700 });
|
|
355
|
+
await fsp.chmod(serviceDir, 0o700).catch(() => undefined);
|
|
356
|
+
const temporaryPath = `${stateFile}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
357
|
+
let handle = null;
|
|
358
|
+
try {
|
|
359
|
+
handle = await fsp.open(temporaryPath, 'wx', 0o600);
|
|
360
|
+
await handle.writeFile(`${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
361
|
+
await handle.sync();
|
|
362
|
+
await handle.close();
|
|
363
|
+
handle = null;
|
|
364
|
+
await fsp.rename(temporaryPath, stateFile);
|
|
365
|
+
if (process.platform === 'win32') {
|
|
366
|
+
const username = process.env.USERDOMAIN && process.env.USERNAME
|
|
367
|
+
? `${process.env.USERDOMAIN}\\${process.env.USERNAME}`
|
|
368
|
+
: process.env.USERNAME ?? os.userInfo().username;
|
|
369
|
+
spawnSync('icacls.exe', [
|
|
370
|
+
serviceDir,
|
|
371
|
+
'/inheritance:r',
|
|
372
|
+
'/grant:r',
|
|
373
|
+
`${username}:(OI)(CI)F`,
|
|
374
|
+
'SYSTEM:(OI)(CI)F',
|
|
375
|
+
], { windowsHide: true, stdio: 'ignore' });
|
|
376
|
+
spawnSync('icacls.exe', [
|
|
377
|
+
stateFile,
|
|
378
|
+
'/inheritance:r',
|
|
379
|
+
'/grant:r',
|
|
380
|
+
`${username}:F`,
|
|
381
|
+
'SYSTEM:F',
|
|
382
|
+
], { windowsHide: true, stdio: 'ignore' });
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
await handle?.close().catch(() => undefined);
|
|
386
|
+
await fsp.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
334
391
|
function serviceStateAlive(state) {
|
|
335
392
|
return [state.apiPid, state.webPid].some((pid) => isProcessAlive(pid));
|
|
336
393
|
}
|
|
@@ -340,6 +397,14 @@ function stopPid(pid) {
|
|
|
340
397
|
return;
|
|
341
398
|
}
|
|
342
399
|
|
|
400
|
+
if (process.platform === 'win32') {
|
|
401
|
+
spawn('taskkill.exe', ['/PID', String(pid), '/T'], {
|
|
402
|
+
windowsHide: true,
|
|
403
|
+
stdio: 'ignore',
|
|
404
|
+
}).unref();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
343
408
|
try {
|
|
344
409
|
process.kill(-pid, 'SIGTERM');
|
|
345
410
|
return;
|
|
@@ -359,6 +424,14 @@ function forceStopPid(pid) {
|
|
|
359
424
|
return;
|
|
360
425
|
}
|
|
361
426
|
|
|
427
|
+
if (process.platform === 'win32') {
|
|
428
|
+
spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], {
|
|
429
|
+
windowsHide: true,
|
|
430
|
+
stdio: 'ignore',
|
|
431
|
+
}).unref();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
362
435
|
try {
|
|
363
436
|
process.kill(-pid, 'SIGKILL');
|
|
364
437
|
return;
|
|
@@ -412,3 +485,38 @@ function sleep(milliseconds) {
|
|
|
412
485
|
setTimeout(resolve, milliseconds);
|
|
413
486
|
});
|
|
414
487
|
}
|
|
488
|
+
|
|
489
|
+
function requestLifecycleControl(state, action, timeoutMs = 2_000) {
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
const socket = net.createConnection(state.apiControlEndpoint);
|
|
492
|
+
const timer = setTimeout(() => {
|
|
493
|
+
socket.destroy();
|
|
494
|
+
reject(new Error('Lifecycle control request timed out.'));
|
|
495
|
+
}, timeoutMs);
|
|
496
|
+
let output = '';
|
|
497
|
+
socket.setEncoding('utf8');
|
|
498
|
+
socket.once('connect', () => {
|
|
499
|
+
socket.write(`${JSON.stringify({
|
|
500
|
+
action,
|
|
501
|
+
token: state.apiControlToken,
|
|
502
|
+
instanceId: state.apiInstanceId,
|
|
503
|
+
})}\n`);
|
|
504
|
+
});
|
|
505
|
+
socket.on('data', (chunk) => {
|
|
506
|
+
output += chunk;
|
|
507
|
+
const newline = output.indexOf('\n');
|
|
508
|
+
if (newline < 0) return;
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
socket.end();
|
|
511
|
+
try {
|
|
512
|
+
resolve(JSON.parse(output.slice(0, newline)));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
reject(error);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
socket.once('error', (error) => {
|
|
518
|
+
clearTimeout(timer);
|
|
519
|
+
reject(error);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|