remote-codex 0.1.5 → 0.1.7
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/apps/supervisor-api/dist/index.js +7749 -5501
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-D-RjOTTL.js → highlighted-body-OFNGDK62-0cYcfOfd.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +32 -0
- package/apps/supervisor-web/dist/assets/index-nH6a8Wwn.js +377 -0
- package/apps/supervisor-web/dist/assets/{xterm-D8iZbRww.js → xterm-DisVWgDR.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +5 -1
- package/packages/agent-runtime/src/index.ts +2 -0
- package/packages/agent-runtime/src/registry.ts +44 -0
- package/packages/agent-runtime/src/types.ts +531 -0
- package/packages/codex/src/appServerManager.test.ts +328 -0
- package/packages/codex/src/appServerManager.ts +656 -0
- package/packages/codex/src/historyItems.ts +1185 -0
- package/packages/codex/src/hookHistory.ts +224 -0
- package/packages/codex/src/index.ts +6 -0
- package/packages/codex/src/jsonrpc.test.ts +58 -0
- package/packages/codex/src/jsonrpc.ts +198 -0
- package/packages/codex/src/requestMapper.test.ts +127 -0
- package/packages/codex/src/requestMapper.ts +511 -0
- package/packages/codex/src/runtimeAdapter.ts +692 -0
- package/packages/codex/src/types.ts +403 -0
- package/packages/db/migrations/0014_thread_history_items.sql +12 -0
- package/packages/db/migrations/0015_agent_provider_fields.sql +14 -0
- package/packages/db/migrations/0016_remove_codex_thread_goal_id.sql +46 -0
- package/packages/db/migrations/0017_remove_codex_thread_columns.sql +85 -0
- package/packages/db/src/client.ts +53 -0
- package/packages/db/src/index.ts +5 -0
- package/packages/db/src/migrate.test.ts +36 -0
- package/packages/db/src/migrate.ts +84 -0
- package/packages/db/src/repositories.ts +893 -0
- package/packages/db/src/schema.ts +177 -0
- package/packages/db/src/seed.ts +51 -0
- package/packages/shared/src/index.ts +878 -0
- package/scripts/service-manager.mjs +6 -4
- package/apps/supervisor-web/dist/assets/index-CdG3ogmZ.js +0 -376
- package/apps/supervisor-web/dist/assets/index-QM8NQf3e.css +0 -32
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { PassThrough, Writable } from 'node:stream';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { CodexAppServerManager } from './appServerManager';
|
|
8
|
+
|
|
9
|
+
async function waitForCondition(
|
|
10
|
+
condition: () => boolean,
|
|
11
|
+
timeoutMs = 1000,
|
|
12
|
+
) {
|
|
13
|
+
const startedAt = Date.now();
|
|
14
|
+
while (!condition()) {
|
|
15
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
16
|
+
throw new Error('Timed out waiting for condition.');
|
|
17
|
+
}
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class ScriptedChild extends EventEmitter {
|
|
23
|
+
stdout = new PassThrough();
|
|
24
|
+
stderr = new PassThrough();
|
|
25
|
+
stdin: Writable;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly options: {
|
|
29
|
+
initializeDelayMs?: number;
|
|
30
|
+
exitOnKillDelayMs?: number;
|
|
31
|
+
} = {},
|
|
32
|
+
) {
|
|
33
|
+
super();
|
|
34
|
+
|
|
35
|
+
let buffer = '';
|
|
36
|
+
this.stdin = new Writable({
|
|
37
|
+
write: (chunk, _encoding, callback) => {
|
|
38
|
+
buffer += chunk.toString();
|
|
39
|
+
const lines = buffer.split('\n');
|
|
40
|
+
buffer = lines.pop() ?? '';
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const message = JSON.parse(trimmed);
|
|
48
|
+
if (message.method === 'initialize') {
|
|
49
|
+
const delay = this.options.initializeDelayMs ?? 0;
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
this.stdout.write(
|
|
52
|
+
`${JSON.stringify({
|
|
53
|
+
id: message.id,
|
|
54
|
+
result: {
|
|
55
|
+
userAgent: 'fake',
|
|
56
|
+
codexHome: '/tmp',
|
|
57
|
+
platformFamily: 'unix',
|
|
58
|
+
platformOs: 'linux',
|
|
59
|
+
},
|
|
60
|
+
})}\n`,
|
|
61
|
+
);
|
|
62
|
+
}, delay);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
callback();
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
kill() {
|
|
72
|
+
const delay = this.options.exitOnKillDelayMs ?? 0;
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
this.stdout.end();
|
|
75
|
+
this.emit('exit', 0, 'SIGTERM');
|
|
76
|
+
}, delay);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('CodexAppServerManager', () => {
|
|
82
|
+
it('starts against a newline-delimited JSON-RPC process', async () => {
|
|
83
|
+
const script = [
|
|
84
|
+
"const readline=require('node:readline');",
|
|
85
|
+
"const rl=readline.createInterface({input:process.stdin,crlfDelay:Infinity});",
|
|
86
|
+
"rl.on('line',(line)=>{",
|
|
87
|
+
" const msg=JSON.parse(line);",
|
|
88
|
+
" if(msg.method==='initialize'){",
|
|
89
|
+
" if(!msg.params?.capabilities?.experimentalApi){ process.stderr.write('missing experimentalApi\\n'); process.exit(1); }",
|
|
90
|
+
" process.stdout.write(JSON.stringify({id:msg.id,result:{userAgent:'fake',codexHome:'/tmp',platformFamily:'unix',platformOs:'macos'}})+'\\n');",
|
|
91
|
+
" } else if(msg.method==='model/list'){",
|
|
92
|
+
" process.stdout.write(JSON.stringify({id:msg.id,result:{data:[{id:'m1',model:'gpt-5',displayName:'GPT-5',description:'desc',hidden:false,isDefault:true}],nextCursor:null}})+'\\n');",
|
|
93
|
+
" }",
|
|
94
|
+
"});"
|
|
95
|
+
].join('');
|
|
96
|
+
|
|
97
|
+
const manager = new CodexAppServerManager({
|
|
98
|
+
command: process.execPath,
|
|
99
|
+
startupTimeoutMs: 1000,
|
|
100
|
+
clientInfo: {
|
|
101
|
+
name: 'test',
|
|
102
|
+
title: 'test',
|
|
103
|
+
version: '0.1.0'
|
|
104
|
+
},
|
|
105
|
+
spawnProcess: (command) => {
|
|
106
|
+
return spawn(command, ['-e', script], { stdio: 'pipe' });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await manager.start();
|
|
111
|
+
const models = await manager.listModels();
|
|
112
|
+
|
|
113
|
+
expect(manager.getStatus().state).toBe('ready');
|
|
114
|
+
expect(models[0]?.model).toBe('gpt-5');
|
|
115
|
+
|
|
116
|
+
await manager.stop();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('surfaces spawn failures as a failed status instead of crashing', async () => {
|
|
120
|
+
class FailingChild extends EventEmitter {
|
|
121
|
+
stdout = new PassThrough();
|
|
122
|
+
stdin = new PassThrough();
|
|
123
|
+
stderr = new PassThrough();
|
|
124
|
+
kill() {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const manager = new CodexAppServerManager({
|
|
130
|
+
command: 'missing-codex',
|
|
131
|
+
startupTimeoutMs: 1000,
|
|
132
|
+
clientInfo: {
|
|
133
|
+
name: 'test',
|
|
134
|
+
title: 'test',
|
|
135
|
+
version: '0.1.0'
|
|
136
|
+
},
|
|
137
|
+
spawnProcess: () => {
|
|
138
|
+
const child = new FailingChild();
|
|
139
|
+
queueMicrotask(() => {
|
|
140
|
+
child.emit('error', new Error('spawn missing-codex ENOENT'));
|
|
141
|
+
});
|
|
142
|
+
return child as any;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await expect(manager.start()).rejects.toMatchObject({
|
|
147
|
+
code: 'spawn_failed'
|
|
148
|
+
});
|
|
149
|
+
expect(manager.getStatus()).toMatchObject({
|
|
150
|
+
state: 'failed'
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('uses expectedTurnId when steering a running turn', async () => {
|
|
155
|
+
const script = [
|
|
156
|
+
"const readline=require('node:readline');",
|
|
157
|
+
"const rl=readline.createInterface({input:process.stdin,crlfDelay:Infinity});",
|
|
158
|
+
"rl.on('line',(line)=>{",
|
|
159
|
+
" const msg=JSON.parse(line);",
|
|
160
|
+
" if(msg.method==='initialize'){",
|
|
161
|
+
" process.stdout.write(JSON.stringify({id:msg.id,result:{userAgent:'fake',codexHome:'/tmp',platformFamily:'unix',platformOs:'linux'}})+'\\n');",
|
|
162
|
+
" } else if(msg.method==='turn/steer'){",
|
|
163
|
+
" if(msg.params?.threadId==='thread-1' && msg.params?.expectedTurnId==='turn-1' && !('turnId' in msg.params)){",
|
|
164
|
+
" process.stdout.write(JSON.stringify({id:msg.id,result:{turn:{id:'turn-1',status:'inProgress',items:[]}}})+'\\n');",
|
|
165
|
+
" } else {",
|
|
166
|
+
" process.stdout.write(JSON.stringify({id:msg.id,error:{code:-32600,message:'bad steer params'}})+'\\n');",
|
|
167
|
+
" }",
|
|
168
|
+
" }",
|
|
169
|
+
"});"
|
|
170
|
+
].join('');
|
|
171
|
+
|
|
172
|
+
const manager = new CodexAppServerManager({
|
|
173
|
+
command: process.execPath,
|
|
174
|
+
startupTimeoutMs: 1000,
|
|
175
|
+
clientInfo: {
|
|
176
|
+
name: 'test',
|
|
177
|
+
title: 'test',
|
|
178
|
+
version: '0.1.0'
|
|
179
|
+
},
|
|
180
|
+
spawnProcess: (command) => {
|
|
181
|
+
return spawn(command, ['-e', script], { stdio: 'pipe' });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await manager.start();
|
|
186
|
+
const turn = await manager.steerTurn({
|
|
187
|
+
threadId: 'thread-1',
|
|
188
|
+
turnId: 'turn-1',
|
|
189
|
+
prompt: 'Follow up',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(turn).toMatchObject({
|
|
193
|
+
id: 'turn-1',
|
|
194
|
+
status: 'inProgress',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await manager.stop();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('writes hook trust state through config batch writes', async () => {
|
|
201
|
+
const requests: any[] = [];
|
|
202
|
+
const script = [
|
|
203
|
+
"const readline=require('node:readline');",
|
|
204
|
+
"const rl=readline.createInterface({input:process.stdin,crlfDelay:Infinity});",
|
|
205
|
+
"rl.on('line',(line)=>{",
|
|
206
|
+
" const msg=JSON.parse(line);",
|
|
207
|
+
" if(msg.method==='initialize'){",
|
|
208
|
+
" process.stdout.write(JSON.stringify({id:msg.id,result:{userAgent:'fake',codexHome:'/tmp',platformFamily:'unix',platformOs:'linux'}})+'\\n');",
|
|
209
|
+
" } else if(msg.method==='config/batchWrite'){",
|
|
210
|
+
" process.stderr.write(JSON.stringify(msg)+'\\n');",
|
|
211
|
+
" process.stdout.write(JSON.stringify({id:msg.id,result:{status:'ok',version:'v1',filePath:'/tmp/config.toml',overriddenMetadata:null}})+'\\n');",
|
|
212
|
+
" }",
|
|
213
|
+
"});"
|
|
214
|
+
].join('');
|
|
215
|
+
|
|
216
|
+
const manager = new CodexAppServerManager({
|
|
217
|
+
command: process.execPath,
|
|
218
|
+
startupTimeoutMs: 1000,
|
|
219
|
+
clientInfo: {
|
|
220
|
+
name: 'test',
|
|
221
|
+
title: 'test',
|
|
222
|
+
version: '0.1.0'
|
|
223
|
+
},
|
|
224
|
+
spawnProcess: (command) => {
|
|
225
|
+
const child = spawn(command, ['-e', script], { stdio: 'pipe' });
|
|
226
|
+
let stderr = '';
|
|
227
|
+
child.stderr.on('data', (chunk) => {
|
|
228
|
+
stderr += chunk.toString();
|
|
229
|
+
const lines = stderr.split('\n');
|
|
230
|
+
stderr = lines.pop() ?? '';
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
if (line.trim()) {
|
|
233
|
+
requests.push(JSON.parse(line));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
return child;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await manager.start();
|
|
242
|
+
await manager.setHookTrust({
|
|
243
|
+
key: '/tmp/repo/.codex/hooks.json:stop:0:0',
|
|
244
|
+
trustedHash: 'sha256:abc',
|
|
245
|
+
});
|
|
246
|
+
await manager.setHookTrust({
|
|
247
|
+
key: '/tmp/repo/.codex/hooks.json:stop:0:0',
|
|
248
|
+
trustedHash: null,
|
|
249
|
+
});
|
|
250
|
+
await waitForCondition(() => requests.length === 2);
|
|
251
|
+
|
|
252
|
+
expect(requests).toEqual([
|
|
253
|
+
expect.objectContaining({
|
|
254
|
+
method: 'config/batchWrite',
|
|
255
|
+
params: {
|
|
256
|
+
edits: [
|
|
257
|
+
{
|
|
258
|
+
keyPath: 'hooks.state',
|
|
259
|
+
mergeStrategy: 'upsert',
|
|
260
|
+
value: {
|
|
261
|
+
'/tmp/repo/.codex/hooks.json:stop:0:0': {
|
|
262
|
+
enabled: true,
|
|
263
|
+
trusted_hash: 'sha256:abc',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
reloadUserConfig: true,
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
expect.objectContaining({
|
|
272
|
+
method: 'config/batchWrite',
|
|
273
|
+
params: {
|
|
274
|
+
edits: [
|
|
275
|
+
{
|
|
276
|
+
keyPath: 'hooks.state',
|
|
277
|
+
mergeStrategy: 'upsert',
|
|
278
|
+
value: {
|
|
279
|
+
'/tmp/repo/.codex/hooks.json:stop:0:0': {
|
|
280
|
+
trusted_hash: '',
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
reloadUserConfig: true,
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
await manager.stop();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('does not let a stale child exit close the replacement app-server client during restart', async () => {
|
|
294
|
+
const firstChild = new ScriptedChild({
|
|
295
|
+
initializeDelayMs: 0,
|
|
296
|
+
exitOnKillDelayMs: 5,
|
|
297
|
+
});
|
|
298
|
+
const secondChild = new ScriptedChild({
|
|
299
|
+
initializeDelayMs: 15,
|
|
300
|
+
exitOnKillDelayMs: 0,
|
|
301
|
+
});
|
|
302
|
+
const spawnedChildren = [firstChild, secondChild];
|
|
303
|
+
|
|
304
|
+
const manager = new CodexAppServerManager({
|
|
305
|
+
command: 'fake-codex',
|
|
306
|
+
startupTimeoutMs: 1000,
|
|
307
|
+
clientInfo: {
|
|
308
|
+
name: 'test',
|
|
309
|
+
title: 'test',
|
|
310
|
+
version: '0.1.0',
|
|
311
|
+
},
|
|
312
|
+
spawnProcess: () => {
|
|
313
|
+
const child = spawnedChildren.shift();
|
|
314
|
+
if (!child) {
|
|
315
|
+
throw new Error('No scripted child available');
|
|
316
|
+
}
|
|
317
|
+
return child as any;
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await manager.start();
|
|
322
|
+
await manager.stop();
|
|
323
|
+
await expect(manager.start()).resolves.toBeUndefined();
|
|
324
|
+
expect(manager.getStatus().state).toBe('ready');
|
|
325
|
+
|
|
326
|
+
await manager.stop();
|
|
327
|
+
});
|
|
328
|
+
});
|