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,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AcpTerminalService } from './terminal-service';
|
|
4
|
+
|
|
5
|
+
describe('AcpTerminalService', () => {
|
|
6
|
+
it('parses agents that send command and argv as one ACP command string', async () => {
|
|
7
|
+
const service = new AcpTerminalService(() => process.cwd());
|
|
8
|
+
const created = service.create({
|
|
9
|
+
sessionId: 'session-1',
|
|
10
|
+
command: `"${process.execPath}" -e "process.stdout.write('ACP_TERMINAL_OK')"`,
|
|
11
|
+
args: [],
|
|
12
|
+
outputByteLimit: 1_000,
|
|
13
|
+
});
|
|
14
|
+
const exit = await service.waitForExit({
|
|
15
|
+
sessionId: 'session-1',
|
|
16
|
+
terminalId: created.terminalId,
|
|
17
|
+
});
|
|
18
|
+
const output = service.output({
|
|
19
|
+
sessionId: 'session-1',
|
|
20
|
+
terminalId: created.terminalId,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(exit.exitCode).toBe(0);
|
|
24
|
+
expect(output.output).toBe('ACP_TERMINAL_OK');
|
|
25
|
+
expect(output.truncated).toBe(false);
|
|
26
|
+
service.release({
|
|
27
|
+
sessionId: 'session-1',
|
|
28
|
+
terminalId: created.terminalId,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
parseCommandLine,
|
|
8
|
+
spawnProcess,
|
|
9
|
+
} from '../../process-runtime/src/index';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_OUTPUT_BYTE_LIMIT = 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
interface AcpTerminalState {
|
|
14
|
+
child: ChildProcess;
|
|
15
|
+
chunks: Buffer[];
|
|
16
|
+
outputByteLimit: number;
|
|
17
|
+
exitStatus: acp.TerminalExitStatus | null;
|
|
18
|
+
exitPromise: Promise<acp.WaitForTerminalExitResponse>;
|
|
19
|
+
resolveExit: (status: acp.WaitForTerminalExitResponse) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function retainedOutput(state: AcpTerminalState) {
|
|
23
|
+
const complete = Buffer.concat(state.chunks);
|
|
24
|
+
if (complete.byteLength <= state.outputByteLimit) {
|
|
25
|
+
return { output: complete.toString('utf8'), truncated: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let start = complete.byteLength - state.outputByteLimit;
|
|
29
|
+
while (start < complete.byteLength && (complete[start]! & 0xc0) === 0x80) {
|
|
30
|
+
start += 1;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
output: complete.subarray(start).toString('utf8'),
|
|
34
|
+
truncated: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class AcpTerminalService {
|
|
39
|
+
private readonly terminals = new Map<string, AcpTerminalState>();
|
|
40
|
+
|
|
41
|
+
constructor(private readonly sessionCwd: (sessionId: string) => string | null) {}
|
|
42
|
+
|
|
43
|
+
create(params: acp.CreateTerminalRequest): acp.CreateTerminalResponse {
|
|
44
|
+
const terminalId = randomUUID();
|
|
45
|
+
const cwd = params.cwd ?? this.sessionCwd(params.sessionId) ?? process.cwd();
|
|
46
|
+
const parsed = params.args && params.args.length > 0
|
|
47
|
+
? { command: params.command, args: params.args }
|
|
48
|
+
: parseCommandLine(params.command);
|
|
49
|
+
const child = spawnProcess({
|
|
50
|
+
command: parsed.command,
|
|
51
|
+
args: parsed.args,
|
|
52
|
+
cwd,
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
...Object.fromEntries((params.env ?? []).map((entry) => [entry.name, entry.value])),
|
|
56
|
+
},
|
|
57
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
let resolveExit!: (status: acp.WaitForTerminalExitResponse) => void;
|
|
60
|
+
const exitPromise = new Promise<acp.WaitForTerminalExitResponse>((resolve) => {
|
|
61
|
+
resolveExit = resolve;
|
|
62
|
+
});
|
|
63
|
+
const state: AcpTerminalState = {
|
|
64
|
+
child,
|
|
65
|
+
chunks: [],
|
|
66
|
+
outputByteLimit: Math.max(1, params.outputByteLimit ?? DEFAULT_OUTPUT_BYTE_LIMIT),
|
|
67
|
+
exitStatus: null,
|
|
68
|
+
exitPromise,
|
|
69
|
+
resolveExit,
|
|
70
|
+
};
|
|
71
|
+
this.terminals.set(terminalId, state);
|
|
72
|
+
|
|
73
|
+
const append = (chunk: Buffer | string) => {
|
|
74
|
+
state.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
75
|
+
};
|
|
76
|
+
child.stdout?.on('data', append);
|
|
77
|
+
child.stderr?.on('data', append);
|
|
78
|
+
child.on('error', (error) => append(`${error.message}\n`));
|
|
79
|
+
child.on('close', (code, signal) => {
|
|
80
|
+
const exitStatus: acp.TerminalExitStatus = {
|
|
81
|
+
exitCode: code,
|
|
82
|
+
signal,
|
|
83
|
+
};
|
|
84
|
+
state.exitStatus = exitStatus;
|
|
85
|
+
state.resolveExit(exitStatus);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { terminalId };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
output(params: acp.TerminalOutputRequest): acp.TerminalOutputResponse {
|
|
92
|
+
const state = this.require(params.terminalId);
|
|
93
|
+
const output = retainedOutput(state);
|
|
94
|
+
return {
|
|
95
|
+
...output,
|
|
96
|
+
...(state.exitStatus ? { exitStatus: state.exitStatus } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
waitForExit(params: acp.WaitForTerminalExitRequest) {
|
|
101
|
+
return this.require(params.terminalId).exitPromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
kill(params: acp.KillTerminalRequest): acp.KillTerminalResponse {
|
|
105
|
+
const state = this.require(params.terminalId);
|
|
106
|
+
if (!state.exitStatus) {
|
|
107
|
+
state.child.kill('SIGTERM');
|
|
108
|
+
}
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
release(params: acp.ReleaseTerminalRequest): acp.ReleaseTerminalResponse {
|
|
113
|
+
const state = this.require(params.terminalId);
|
|
114
|
+
if (!state.exitStatus) {
|
|
115
|
+
state.child.kill('SIGTERM');
|
|
116
|
+
}
|
|
117
|
+
this.terminals.delete(params.terminalId);
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
stop() {
|
|
122
|
+
for (const state of this.terminals.values()) {
|
|
123
|
+
if (!state.exitStatus) {
|
|
124
|
+
state.child.kill('SIGTERM');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.terminals.clear();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private require(terminalId: string) {
|
|
131
|
+
const state = this.terminals.get(terminalId);
|
|
132
|
+
if (!state) {
|
|
133
|
+
throw new Error(`ACP terminal not found: ${terminalId}`);
|
|
134
|
+
}
|
|
135
|
+
return state;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -73,6 +73,7 @@ export function isRemoteThreadBootstrapError(error: unknown) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return (
|
|
76
|
+
(isRecord(error.details) && error.details.historyUnavailable === true) ||
|
|
76
77
|
error.message.includes('includeTurns is unavailable before first user message') ||
|
|
77
78
|
error.message.includes('is not materialized yet') ||
|
|
78
79
|
error.message.includes('no rollout found for thread id') ||
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import type {
|
|
3
|
+
AcpAgentOptionMetadataDto,
|
|
3
4
|
AgentBackendIdDto,
|
|
4
5
|
AgentBackendInstallationDto,
|
|
5
6
|
ThreadHistoryItemDto,
|
|
@@ -159,6 +160,8 @@ export interface AgentModel {
|
|
|
159
160
|
description: string;
|
|
160
161
|
}>;
|
|
161
162
|
defaultReasoningEffort: string | null;
|
|
163
|
+
selectionKind?: 'model' | 'agent';
|
|
164
|
+
acpAgent?: AcpAgentOptionMetadataDto | null;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
export type AgentSessionStatus =
|
|
@@ -228,6 +231,7 @@ export interface ReadAgentSessionOptions {
|
|
|
228
231
|
|
|
229
232
|
export interface StartAgentSessionInput {
|
|
230
233
|
cwd: string;
|
|
234
|
+
agentId?: string | null;
|
|
231
235
|
model: string;
|
|
232
236
|
reasoningEffort?: string | null;
|
|
233
237
|
approvalMode: 'yolo' | 'guarded';
|
|
@@ -237,6 +241,7 @@ export interface StartAgentSessionInput {
|
|
|
237
241
|
|
|
238
242
|
export interface StartAgentSessionResult {
|
|
239
243
|
provider: AgentProviderId;
|
|
244
|
+
agentId?: string | null;
|
|
240
245
|
providerSessionId: string;
|
|
241
246
|
model: string | null;
|
|
242
247
|
reasoningEffort?: string | null;
|
|
@@ -549,6 +554,8 @@ export interface AgentRuntime extends EventEmitter {
|
|
|
549
554
|
stop(): Promise<void>;
|
|
550
555
|
|
|
551
556
|
listModels(): Promise<AgentModel[]>;
|
|
557
|
+
listAgentOptions?(): Promise<AgentModel[]>;
|
|
558
|
+
listModelsForAgent?(agentId: string, cwd: string): Promise<AgentModel[]>;
|
|
552
559
|
listSessions(): Promise<AgentSessionSummary[]>;
|
|
553
560
|
listLoadedSessions(): Promise<string[]>;
|
|
554
561
|
readSession(
|
|
@@ -582,6 +589,7 @@ export interface AgentRuntime extends EventEmitter {
|
|
|
582
589
|
getGoal?(providerSessionId: string): Promise<AgentGoal | null>;
|
|
583
590
|
setGoal?(input: SetAgentGoalInput): Promise<AgentGoal>;
|
|
584
591
|
clearGoal?(providerSessionId: string): Promise<boolean>;
|
|
592
|
+
installModel?(modelId: string): Promise<void>;
|
|
585
593
|
getSubscriptionUsage?(): Promise<{
|
|
586
594
|
provider: 'codex' | 'claude';
|
|
587
595
|
authKind: 'subscription' | 'apiKey' | 'unknown';
|
|
@@ -1266,7 +1266,7 @@ describe('ClaudeRuntimeAdapter', () => {
|
|
|
1266
1266
|
message: {
|
|
1267
1267
|
role: 'user',
|
|
1268
1268
|
content:
|
|
1269
|
-
'<task-notification>\\n<task-id>task-1</task-id>\\n<tool-use-id>toolu_agent_xml</tool-use-id>\\n<status>completed</status>\\n<summary>Agent
|
|
1269
|
+
'<task-notification>\\n<task-id>task-1</task-id>\\n<tool-use-id>toolu_agent_xml</tool-use-id>\\n<status>completed</status>\\n<summary>Agent "Audit docs" finished</summary>\\n<result>Found one stale runbook.</result>\\n</task-notification>',
|
|
1270
1270
|
},
|
|
1271
1271
|
parent_tool_use_id: null,
|
|
1272
1272
|
uuid: '00000000-0000-4000-8000-000000000042' as any,
|
|
@@ -2380,7 +2380,7 @@ describe('ClaudeRuntimeAdapter', () => {
|
|
|
2380
2380
|
message: {
|
|
2381
2381
|
role: 'user',
|
|
2382
2382
|
content:
|
|
2383
|
-
'<task-notification>\\n<task-id>task-a</task-id>\\n<tool-use-id>toolu_task_a</tool-use-id>\\n<status>completed</status>\\n<summary>Agent
|
|
2383
|
+
'<task-notification>\\n<task-id>task-a</task-id>\\n<tool-use-id>toolu_task_a</tool-use-id>\\n<status>completed</status>\\n<summary>Agent "Audit infra docs" finished</summary>\\n<result>Infra docs need one migration checklist.</result>\\n</task-notification>',
|
|
2384
2384
|
},
|
|
2385
2385
|
parent_tool_use_id: null,
|
|
2386
2386
|
},
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
3
|
import fs from 'node:fs/promises';
|
|
5
4
|
import { createRequire } from 'node:module';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
7
|
|
|
10
8
|
import type {
|
|
11
9
|
AgentActionQuestion,
|
|
@@ -58,8 +56,9 @@ import {
|
|
|
58
56
|
userMessageHistoryItem,
|
|
59
57
|
userMessageToHistoryItem,
|
|
60
58
|
} from './historyItems';
|
|
59
|
+
import { runProcess } from '../../process-runtime/src/index';
|
|
60
|
+
import { isPathInside } from '../../workspace/src/index';
|
|
61
61
|
|
|
62
|
-
const execFileAsync = promisify(execFile);
|
|
63
62
|
|
|
64
63
|
type ClaudePromptInput = string | AsyncIterable<SDKUserMessage>;
|
|
65
64
|
type ClaudeMessageContent =
|
|
@@ -379,8 +378,7 @@ function resolvePromptAssetPath(assetPath: string, cwd: string | null | undefine
|
|
|
379
378
|
const resolvedPath = path.isAbsolute(assetPath)
|
|
380
379
|
? path.normalize(assetPath)
|
|
381
380
|
: path.resolve(cwd, assetPath);
|
|
382
|
-
|
|
383
|
-
if (relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))) {
|
|
381
|
+
if (isPathInside(cwd, resolvedPath)) {
|
|
384
382
|
return resolvedPath;
|
|
385
383
|
}
|
|
386
384
|
return null;
|
|
@@ -2566,12 +2564,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
2566
2564
|
|
|
2567
2565
|
const relativePath = `./.temp/threads/${input.localThreadId}/claude-history-${safeAssetFilePart(input.messageId)}-${input.blockIndex}.${extension}`;
|
|
2568
2566
|
const targetPath = path.resolve(input.workspacePath, relativePath);
|
|
2569
|
-
|
|
2570
|
-
if (
|
|
2571
|
-
relativeToWorkspace === '' ||
|
|
2572
|
-
relativeToWorkspace.startsWith('..') ||
|
|
2573
|
-
path.isAbsolute(relativeToWorkspace)
|
|
2574
|
-
) {
|
|
2567
|
+
if (!isPathInside(input.workspacePath, targetPath)) {
|
|
2575
2568
|
return null;
|
|
2576
2569
|
}
|
|
2577
2570
|
|
|
@@ -2833,12 +2826,10 @@ async function importOptionalPackage(specifier: string) {
|
|
|
2833
2826
|
}
|
|
2834
2827
|
|
|
2835
2828
|
async function npmGlobalRoot() {
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
return null;
|
|
2843
|
-
}
|
|
2829
|
+
const result = await runProcess({
|
|
2830
|
+
command: 'npm',
|
|
2831
|
+
args: ['root', '-g'],
|
|
2832
|
+
timeoutMs: 3_000,
|
|
2833
|
+
});
|
|
2834
|
+
return result.code === 0 ? result.stdout.trim() || null : null;
|
|
2844
2835
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
import { PassThrough, Writable } from 'node:stream';
|
|
4
7
|
|
|
5
8
|
import { describe, expect, it } from 'vitest';
|
|
@@ -80,6 +83,8 @@ class ScriptedChild extends EventEmitter {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
describe('CodexAppServerManager', () => {
|
|
86
|
+
const windowsIt = it.runIf(process.platform === 'win32');
|
|
87
|
+
|
|
83
88
|
it('starts against a newline-delimited JSON-RPC process', async () => {
|
|
84
89
|
const script = [
|
|
85
90
|
"const readline=require('node:readline');",
|
|
@@ -117,6 +122,48 @@ describe('CodexAppServerManager', () => {
|
|
|
117
122
|
await manager.stop();
|
|
118
123
|
});
|
|
119
124
|
|
|
125
|
+
windowsIt('starts an npm-style Codex command shim from a path with spaces', async () => {
|
|
126
|
+
const directory = await fs.mkdtemp(
|
|
127
|
+
path.join(os.tmpdir(), 'remote-codex app server shim '),
|
|
128
|
+
);
|
|
129
|
+
const scriptPath = path.join(directory, 'fake-codex.cjs');
|
|
130
|
+
const shimPath = path.join(directory, 'fake-codex.cmd');
|
|
131
|
+
const script = [
|
|
132
|
+
"const readline = require('node:readline');",
|
|
133
|
+
'const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });',
|
|
134
|
+
"rl.on('line', (line) => {",
|
|
135
|
+
' const message = JSON.parse(line);',
|
|
136
|
+
" if (message.method === 'initialize') {",
|
|
137
|
+
" process.stdout.write(JSON.stringify({ id: message.id, result: { userAgent: 'fake-cmd', codexHome: 'C:/tmp', platformFamily: 'windows', platformOs: 'windows' } }) + '\\n');",
|
|
138
|
+
' }',
|
|
139
|
+
'});',
|
|
140
|
+
].join('\n');
|
|
141
|
+
|
|
142
|
+
await fs.writeFile(scriptPath, script);
|
|
143
|
+
await fs.writeFile(
|
|
144
|
+
shimPath,
|
|
145
|
+
`@echo off\r\n"${process.execPath}" "%~dp0fake-codex.cjs" %*\r\n`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const manager = new CodexAppServerManager({
|
|
149
|
+
command: shimPath,
|
|
150
|
+
startupTimeoutMs: 3_000,
|
|
151
|
+
clientInfo: {
|
|
152
|
+
name: 'test',
|
|
153
|
+
title: 'test',
|
|
154
|
+
version: '0.1.0',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await manager.start();
|
|
160
|
+
expect(manager.getStatus().state).toBe('ready');
|
|
161
|
+
} finally {
|
|
162
|
+
await manager.stop();
|
|
163
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
120
167
|
it('surfaces spawn failures as a failed status instead of crashing', async () => {
|
|
121
168
|
class FailingChild extends EventEmitter {
|
|
122
169
|
stdout = new PassThrough();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ChildProcessWithoutNullStreams
|
|
1
|
+
import { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
|
|
4
|
+
import { spawnProcess as spawnCrossPlatformProcess } from '../../process-runtime/src/index';
|
|
5
|
+
|
|
4
6
|
import { JsonRpcClient, JsonRpcClientError } from './jsonrpc';
|
|
5
7
|
import {
|
|
6
8
|
AppServerStatusSnapshot,
|
|
@@ -250,7 +252,9 @@ export class CodexAppServerManager extends EventEmitter {
|
|
|
250
252
|
this.spawnProcess =
|
|
251
253
|
options.spawnProcess ??
|
|
252
254
|
((command: string, args: string[]) =>
|
|
253
|
-
|
|
255
|
+
spawnCrossPlatformProcess({
|
|
256
|
+
command,
|
|
257
|
+
args,
|
|
254
258
|
stdio: 'pipe'
|
|
255
259
|
}) as unknown as ChildProcessWithoutNullStreams);
|
|
256
260
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
|
|
@@ -120,7 +121,10 @@ describe('CodexRuntimeAdapter', () => {
|
|
|
120
121
|
{ type: 'text', text: 'Inspect this ', text_elements: [] },
|
|
121
122
|
{
|
|
122
123
|
type: 'localImage',
|
|
123
|
-
path:
|
|
124
|
+
path: path.join(
|
|
125
|
+
path.resolve('/tmp/workspace'),
|
|
126
|
+
'.temp/threads/thread-1/photo.png',
|
|
127
|
+
),
|
|
124
128
|
},
|
|
125
129
|
{ type: 'text', text: ' then summarize.', text_elements: [] },
|
|
126
130
|
],
|
|
@@ -141,7 +145,10 @@ describe('CodexRuntimeAdapter', () => {
|
|
|
141
145
|
{ type: 'text', text: 'Inspect ', text_elements: [] },
|
|
142
146
|
{
|
|
143
147
|
type: 'localImage',
|
|
144
|
-
path:
|
|
148
|
+
path: path.join(
|
|
149
|
+
path.resolve('/tmp/workspace'),
|
|
150
|
+
'.temp/threads/thread-1/photo.heic',
|
|
151
|
+
),
|
|
145
152
|
},
|
|
146
153
|
{ type: 'text', text: '.', text_elements: [] },
|
|
147
154
|
]);
|
|
@@ -29,6 +29,7 @@ export interface CreateThreadRecordInput {
|
|
|
29
29
|
workspaceId: string;
|
|
30
30
|
title: string;
|
|
31
31
|
provider?: string;
|
|
32
|
+
agentId?: string | null;
|
|
32
33
|
providerSessionId: string | null;
|
|
33
34
|
providerTurnId?: string | null;
|
|
34
35
|
model?: string | null;
|
|
@@ -47,6 +48,7 @@ export interface CreateThreadRecordInput {
|
|
|
47
48
|
|
|
48
49
|
export interface UpdateThreadRecordInput {
|
|
49
50
|
provider?: string;
|
|
51
|
+
agentId?: string | null;
|
|
50
52
|
providerSessionId?: string | null;
|
|
51
53
|
providerTurnId?: string | null;
|
|
52
54
|
title?: string;
|
|
@@ -278,6 +280,7 @@ export function createThreadRecord(db: DatabaseClient, input: CreateThreadRecord
|
|
|
278
280
|
id: randomUUID(),
|
|
279
281
|
workspaceId: input.workspaceId,
|
|
280
282
|
provider: input.provider ?? 'codex',
|
|
283
|
+
agentId: input.agentId ?? null,
|
|
281
284
|
providerSessionId: input.providerSessionId,
|
|
282
285
|
providerTurnId: input.providerTurnId ?? null,
|
|
283
286
|
source: input.source ?? 'supervisor',
|
|
@@ -23,6 +23,7 @@ export const threads = sqliteTable('threads', {
|
|
|
23
23
|
id: text('id').primaryKey(),
|
|
24
24
|
workspaceId: text('workspace_id').notNull(),
|
|
25
25
|
provider: text('provider').notNull().default('codex'),
|
|
26
|
+
agentId: text('agent_id'),
|
|
26
27
|
providerSessionId: text('provider_session_id'),
|
|
27
28
|
providerTurnId: text('provider_turn_id'),
|
|
28
29
|
source: text('source').notNull().default('supervisor'),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
+
import { isPathInside } from '../../workspace/src/index';
|
|
4
|
+
|
|
3
5
|
import type {
|
|
4
6
|
AgentHistoryItem,
|
|
5
7
|
AgentTurn,
|
|
@@ -163,15 +165,14 @@ function displayPath(pathValue: string | null, options: OpenCodeHistoryItemMappi
|
|
|
163
165
|
|
|
164
166
|
const root = path.resolve(options.workspacePath);
|
|
165
167
|
const absolutePath = path.resolve(pathValue);
|
|
168
|
+
if (!isPathInside(root, absolutePath)) {
|
|
169
|
+
return pathValue;
|
|
170
|
+
}
|
|
166
171
|
const relativePath = path.relative(root, absolutePath);
|
|
167
|
-
if (
|
|
168
|
-
!relativePath ||
|
|
169
|
-
relativePath.startsWith('..') ||
|
|
170
|
-
path.isAbsolute(relativePath)
|
|
171
|
-
) {
|
|
172
|
+
if (!relativePath) {
|
|
172
173
|
return pathValue;
|
|
173
174
|
}
|
|
174
|
-
return relativePath;
|
|
175
|
+
return relativePath.split(path.sep).join('/');
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
function toolIsLowInformationPatch(
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
2
|
import fs from 'node:fs/promises';
|
|
4
3
|
import net from 'node:net';
|
|
5
4
|
import { createRequire } from 'node:module';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
7
|
|
|
10
8
|
import type {
|
|
11
9
|
AgentHistoryItem,
|
|
@@ -37,8 +35,8 @@ import {
|
|
|
37
35
|
openCodeMessageToHistoryItems,
|
|
38
36
|
openCodeMessagesToPlanUpdate,
|
|
39
37
|
} from './historyItems';
|
|
38
|
+
import { runProcess } from '../../process-runtime/src/index';
|
|
40
39
|
|
|
41
|
-
const execFileAsync = promisify(execFile);
|
|
42
40
|
const openCodeWaitTimeoutMs = 1_500;
|
|
43
41
|
const openCodePromptPollIntervalMs = 500;
|
|
44
42
|
const openCodePromptTimeoutMs = 120_000;
|
|
@@ -1476,12 +1474,10 @@ function resolveOptionalPackage(
|
|
|
1476
1474
|
}
|
|
1477
1475
|
|
|
1478
1476
|
async function npmGlobalRoot() {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
return null;
|
|
1486
|
-
}
|
|
1477
|
+
const result = await runProcess({
|
|
1478
|
+
command: 'npm',
|
|
1479
|
+
args: ['root', '-g'],
|
|
1480
|
+
timeoutMs: 3_000,
|
|
1481
|
+
});
|
|
1482
|
+
return result.code === 0 ? result.stdout.trim() || null : null;
|
|
1487
1483
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { parseCommandLine, resolveExecutable, runProcess } from './index';
|
|
7
|
+
|
|
8
|
+
const temporaryDirectories: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await Promise.all(
|
|
12
|
+
temporaryDirectories.splice(0).map((directory) =>
|
|
13
|
+
fs.rm(directory, { recursive: true, force: true }),
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('runProcess', () => {
|
|
19
|
+
const windowsIt = it.runIf(process.platform === 'win32');
|
|
20
|
+
|
|
21
|
+
it('preserves argv values without shell interpretation', async () => {
|
|
22
|
+
const argument = 'space & (parentheses) 中文';
|
|
23
|
+
const result = await runProcess({
|
|
24
|
+
command: process.execPath,
|
|
25
|
+
args: ['-e', 'process.stdout.write(process.argv[1])', argument],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(result.code).toBe(0);
|
|
29
|
+
expect(result.stdout).toBe(argument);
|
|
30
|
+
expect(result.spawnError).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns stderr and a non-zero exit code', async () => {
|
|
34
|
+
const result = await runProcess({
|
|
35
|
+
command: process.execPath,
|
|
36
|
+
args: ['-e', "process.stderr.write('failure'); process.exit(7)"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result.code).toBe(7);
|
|
40
|
+
expect(result.stderr).toBe('failure');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('reports a missing executable', async () => {
|
|
44
|
+
const result = await runProcess({
|
|
45
|
+
command: `remote-codex-missing-${Date.now()}`,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result.code).toBeNull();
|
|
49
|
+
expect(result.spawnError).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('terminates a timed out process', async () => {
|
|
53
|
+
const result = await runProcess({
|
|
54
|
+
command: process.execPath,
|
|
55
|
+
args: ['-e', 'setInterval(() => {}, 1000)'],
|
|
56
|
+
timeoutMs: 50,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.timedOut).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('bounds captured output', async () => {
|
|
63
|
+
const result = await runProcess({
|
|
64
|
+
command: process.execPath,
|
|
65
|
+
args: ['-e', "process.stdout.write('x'.repeat(100))"],
|
|
66
|
+
maxOutputBytes: 10,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.stdout).toBe('x'.repeat(10));
|
|
70
|
+
expect(result.outputTruncated).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
windowsIt('executes an npm-style command shim from a path with spaces', async () => {
|
|
74
|
+
const directory = await fs.mkdtemp(
|
|
75
|
+
path.join(os.tmpdir(), 'remote-codex command shim '),
|
|
76
|
+
);
|
|
77
|
+
temporaryDirectories.push(directory);
|
|
78
|
+
const shimPath = path.join(directory, 'fake-codex.cmd');
|
|
79
|
+
await fs.writeFile(shimPath, '@echo off\r\necho %~1\r\n');
|
|
80
|
+
|
|
81
|
+
const result = await runProcess({
|
|
82
|
+
command: shimPath,
|
|
83
|
+
args: ['shim executed'],
|
|
84
|
+
timeoutMs: 3_000,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.code).toBe(0);
|
|
88
|
+
expect(result.stdout.trim()).toBe('shim executed');
|
|
89
|
+
expect(result.spawnError).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('parseCommandLine', () => {
|
|
94
|
+
it('parses built-in npm and quoted node commands without a shell', () => {
|
|
95
|
+
expect(parseCommandLine('npm install -g @openai/codex@latest')).toEqual({
|
|
96
|
+
command: 'npm',
|
|
97
|
+
args: ['install', '-g', '@openai/codex@latest'],
|
|
98
|
+
});
|
|
99
|
+
expect(parseCommandLine('node -e "process.exit(0)"')).toEqual({
|
|
100
|
+
command: 'node',
|
|
101
|
+
args: ['-e', 'process.exit(0)'],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('does not interpret shell operators', () => {
|
|
106
|
+
expect(parseCommandLine('tool first && second')).toEqual({
|
|
107
|
+
command: 'tool',
|
|
108
|
+
args: ['first', '&&', 'second'],
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('resolveExecutable', () => {
|
|
114
|
+
it('resolves an executable from PATH', async () => {
|
|
115
|
+
const resolved = await resolveExecutable(process.execPath);
|
|
116
|
+
expect(resolved).toBe(path.resolve(process.execPath));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('uses Windows PATHEXT semantics for command shims', async () => {
|
|
120
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex process '));
|
|
121
|
+
temporaryDirectories.push(directory);
|
|
122
|
+
const shimPath = path.join(directory, 'codex.CMD');
|
|
123
|
+
await fs.writeFile(shimPath, '@echo off\r\n');
|
|
124
|
+
|
|
125
|
+
const resolved = await resolveExecutable('codex', {
|
|
126
|
+
env: { PATH: directory, PATHEXT: '.EXE;.CMD' },
|
|
127
|
+
platform: 'win32',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(resolved?.toLowerCase()).toBe(shimPath.toLowerCase());
|
|
131
|
+
});
|
|
132
|
+
});
|