@vtxmacro/cli 2026.9.59 → 2026.9.61
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 +80 -6
- package/bin/exo-harness.ts +17 -13
- package/bin/vtx-managed-codex.js +3125 -0
- package/bin/vtx-service-bootstrap.js +5 -2
- package/bin/vtx-service-launcher.js +3 -0
- package/bin/vtx.js +17142 -15739
- package/package.json +3 -2
|
@@ -0,0 +1,3125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// lib/inference-host/managed-codex-wrapper.ts
|
|
4
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
6
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// lib/inference-host/codex-app-server.ts
|
|
9
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
10
|
+
import { chmod, lstat as lstat2, open as open2, readFile as readFile2, rename as rename2, rm as rm2, writeFile } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2, sep } from "node:path";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
|
|
15
|
+
// lib/inference-host/config.ts
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { lstat, mkdir, open, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
18
|
+
import { dirname, isAbsolute, join, relative, resolve, win32 as windowsPath } from "node:path";
|
|
19
|
+
var MAX_INFERENCE_PRIVATE_FILE_BYTES = 16 * 1024 * 1024;
|
|
20
|
+
var WINDOWS_PRIVATE_ACL_BROKER_SCRIPT = `$ErrorActionPreference='Stop'
|
|
21
|
+
$ProgressPreference='SilentlyContinue'
|
|
22
|
+
while(($line=[Console]::In.ReadLine()) -ne $null) {
|
|
23
|
+
$requestId=$null
|
|
24
|
+
$failureStage='request_validation'
|
|
25
|
+
try {
|
|
26
|
+
if($line.Length -lt 2 -or $line.Length -gt 65536) { throw 'invalid request size' }
|
|
27
|
+
$request=$line | ConvertFrom-Json
|
|
28
|
+
$names=@($request.PSObject.Properties.Name | Sort-Object)
|
|
29
|
+
if(($names -join ',') -ne 'id,kind,mode,path_b64') { throw 'invalid request shape' }
|
|
30
|
+
if($request.id -isnot [long] -and $request.id -isnot [int]) { throw 'invalid request id' }
|
|
31
|
+
$requestId=[long]$request.id
|
|
32
|
+
if($requestId -lt 1) { throw 'invalid request id' }
|
|
33
|
+
$kind=[string]$request.kind
|
|
34
|
+
$mode=[string]$request.mode
|
|
35
|
+
if($kind -ne 'directory' -and $kind -ne 'file') { throw 'invalid path kind' }
|
|
36
|
+
if($mode -ne 'harden' -and $mode -ne 'verify' -and $mode -ne 'verify-owner') { throw 'invalid ACL mode' }
|
|
37
|
+
$path=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([string]$request.path_b64))
|
|
38
|
+
if([string]::IsNullOrWhiteSpace($path) -or $path.Length -gt 32767 -or $path.Contains([char]0)) { throw 'invalid path' }
|
|
39
|
+
$failureStage='path_lookup'
|
|
40
|
+
if(-not (Test-Path -LiteralPath $path)) {
|
|
41
|
+
[Console]::Out.WriteLine((@{id=$requestId;status='missing'} | ConvertTo-Json -Compress))
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
$failureStage='principal_resolution'
|
|
45
|
+
$currentIdentity=[Security.Principal.WindowsIdentity]::GetCurrent()
|
|
46
|
+
$current=$currentIdentity.User
|
|
47
|
+
$system=New-Object Security.Principal.SecurityIdentifier('S-1-5-18')
|
|
48
|
+
$admins=New-Object Security.Principal.SecurityIdentifier('S-1-5-32-544')
|
|
49
|
+
$currentIsAdmin=(New-Object Security.Principal.WindowsPrincipal($currentIdentity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
50
|
+
$allowed=@($current.Value,$system.Value,$admins.Value)
|
|
51
|
+
if($mode -eq 'harden') {
|
|
52
|
+
$failureStage='acl_read'
|
|
53
|
+
$acl=Get-Acl -LiteralPath $path
|
|
54
|
+
$originalAccess=$acl.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access)
|
|
55
|
+
$failureStage='owner_validation'
|
|
56
|
+
$existingOwner=(New-Object Security.Principal.NTAccount($acl.Owner)).Translate([Security.Principal.SecurityIdentifier]).Value
|
|
57
|
+
if($existingOwner -ne $current.Value -and ($existingOwner -ne $admins.Value -or -not $currentIsAdmin)) { throw 'private ACL owner is invalid' }
|
|
58
|
+
$failureStage='acl_mutation'
|
|
59
|
+
$acl.SetAccessRuleProtection($true,$false)
|
|
60
|
+
foreach($existingRule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($existingRule) }
|
|
61
|
+
$inherit=if($kind -eq 'directory') {
|
|
62
|
+
[Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
|
|
63
|
+
} else { [Security.AccessControl.InheritanceFlags]::None }
|
|
64
|
+
foreach($sid in @($current,$system,$admins)) {
|
|
65
|
+
$rule=New-Object Security.AccessControl.FileSystemAccessRule(
|
|
66
|
+
$sid,
|
|
67
|
+
[Security.AccessControl.FileSystemRights]::FullControl,
|
|
68
|
+
$inherit,
|
|
69
|
+
[Security.AccessControl.PropagationFlags]::None,
|
|
70
|
+
[Security.AccessControl.AccessControlType]::Allow
|
|
71
|
+
)
|
|
72
|
+
[void]$acl.AddAccessRule($rule)
|
|
73
|
+
}
|
|
74
|
+
$failureStage='acl_write'
|
|
75
|
+
if($originalAccess -ne $acl.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access)) {
|
|
76
|
+
(Get-Item -LiteralPath $path).SetAccessControl($acl)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
$failureStage='acl_readback'
|
|
80
|
+
$actual=Get-Acl -LiteralPath $path
|
|
81
|
+
$failureStage='acl_verification'
|
|
82
|
+
$owner=(New-Object Security.Principal.NTAccount($actual.Owner)).Translate([Security.Principal.SecurityIdentifier]).Value
|
|
83
|
+
if($owner -ne $current.Value -and ($owner -ne $admins.Value -or -not $currentIsAdmin)) { throw 'private ACL owner is invalid' }
|
|
84
|
+
if($mode -ne 'verify-owner') {
|
|
85
|
+
if(-not $actual.AreAccessRulesProtected) { throw 'private ACL inheritance is invalid' }
|
|
86
|
+
$userFull=$false
|
|
87
|
+
foreach($rule in $actual.Access) {
|
|
88
|
+
$sid=$rule.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value
|
|
89
|
+
if($rule.IsInherited -or $allowed -notcontains $sid -or $rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow) {
|
|
90
|
+
throw 'private ACL contains an unauthorized access rule'
|
|
91
|
+
}
|
|
92
|
+
if($sid -eq $current.Value -and (($rule.FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -eq [Security.AccessControl.FileSystemRights]::FullControl)) { $userFull=$true }
|
|
93
|
+
}
|
|
94
|
+
if(-not $userFull) { throw 'private ACL does not grant the current user full control' }
|
|
95
|
+
}
|
|
96
|
+
[Console]::Out.WriteLine((@{id=$requestId;status='ok'} | ConvertTo-Json -Compress))
|
|
97
|
+
} catch {
|
|
98
|
+
[Console]::Out.WriteLine((@{id=$requestId;status='error';error_code=$failureStage} | ConvertTo-Json -Compress))
|
|
99
|
+
}
|
|
100
|
+
}`;
|
|
101
|
+
var WINDOWS_IDENTITY_COMMAND_TIMEOUT_MS = 15e3;
|
|
102
|
+
var WINDOWS_PRIVATE_ACL_BROKER_STARTUP_TIMEOUT_MS = 9e4;
|
|
103
|
+
var SMALL_IDENTITY_COMMAND_TIMEOUT_MS = 5e3;
|
|
104
|
+
var windowsPowerShellEnvironment = (extra = {}) => {
|
|
105
|
+
const sanitizedExtra = { ...extra };
|
|
106
|
+
delete sanitizedExtra.PSModulePath;
|
|
107
|
+
const windowsRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
108
|
+
return {
|
|
109
|
+
...sanitizedExtra,
|
|
110
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
111
|
+
SystemRoot: process.env.SystemRoot,
|
|
112
|
+
WINDIR: process.env.WINDIR,
|
|
113
|
+
ComSpec: process.env.ComSpec,
|
|
114
|
+
PATHEXT: process.env.PATHEXT,
|
|
115
|
+
PSModulePath: windowsRoot ? windowsPath.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "Modules") : void 0,
|
|
116
|
+
TEMP: process.env.TEMP,
|
|
117
|
+
TMP: process.env.TMP
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
var windowsPrivateAclBrokerInvocation = () => ({
|
|
121
|
+
args: [
|
|
122
|
+
"-NoLogo",
|
|
123
|
+
"-NoProfile",
|
|
124
|
+
"-NonInteractive",
|
|
125
|
+
"-EncodedCommand",
|
|
126
|
+
Buffer.from(WINDOWS_PRIVATE_ACL_BROKER_SCRIPT, "utf16le").toString("base64")
|
|
127
|
+
],
|
|
128
|
+
env: windowsPowerShellEnvironment()
|
|
129
|
+
});
|
|
130
|
+
var WindowsPrivateAclBroker = class {
|
|
131
|
+
constructor(dependencies = {}) {
|
|
132
|
+
this.child = null;
|
|
133
|
+
this.requestSequence = 0;
|
|
134
|
+
this.queue = Promise.resolve();
|
|
135
|
+
this.pending = null;
|
|
136
|
+
this.outputBuffer = "";
|
|
137
|
+
this.idleTimer = null;
|
|
138
|
+
this.run = async (path, kind, mode) => {
|
|
139
|
+
const operation = this.queue.then(
|
|
140
|
+
() => this.dispatch(path, kind, mode),
|
|
141
|
+
() => this.dispatch(path, kind, mode)
|
|
142
|
+
);
|
|
143
|
+
this.queue = operation.then(() => void 0, () => void 0);
|
|
144
|
+
await operation;
|
|
145
|
+
};
|
|
146
|
+
this.requestTimeoutMs = dependencies.requestTimeoutMs ?? 15e3;
|
|
147
|
+
this.startupRequestTimeoutMs = dependencies.startupRequestTimeoutMs ?? WINDOWS_PRIVATE_ACL_BROKER_STARTUP_TIMEOUT_MS;
|
|
148
|
+
this.idleShutdownMs = dependencies.idleShutdownMs ?? 3e4;
|
|
149
|
+
this.spawnProcess = dependencies.spawnProcess ?? (() => {
|
|
150
|
+
const invocation = windowsPrivateAclBrokerInvocation();
|
|
151
|
+
return spawn("powershell.exe", [...invocation.args], {
|
|
152
|
+
env: invocation.env,
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
scheduleIdleShutdown() {
|
|
159
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
160
|
+
this.idleTimer = setTimeout(() => {
|
|
161
|
+
this.idleTimer = null;
|
|
162
|
+
const child = this.child;
|
|
163
|
+
if (!child || this.pending) return;
|
|
164
|
+
this.child = null;
|
|
165
|
+
this.outputBuffer = "";
|
|
166
|
+
child.stdin.end();
|
|
167
|
+
const force = setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
168
|
+
force.unref();
|
|
169
|
+
child.once("exit", () => clearTimeout(force));
|
|
170
|
+
}, this.idleShutdownMs);
|
|
171
|
+
this.idleTimer.unref();
|
|
172
|
+
}
|
|
173
|
+
failChild(child, error) {
|
|
174
|
+
if (this.child !== child) return;
|
|
175
|
+
this.child = null;
|
|
176
|
+
this.outputBuffer = "";
|
|
177
|
+
if (this.idleTimer) {
|
|
178
|
+
clearTimeout(this.idleTimer);
|
|
179
|
+
this.idleTimer = null;
|
|
180
|
+
}
|
|
181
|
+
const pending = this.pending;
|
|
182
|
+
this.pending = null;
|
|
183
|
+
if (pending) {
|
|
184
|
+
clearTimeout(pending.timer);
|
|
185
|
+
pending.reject(error);
|
|
186
|
+
}
|
|
187
|
+
child.kill("SIGKILL");
|
|
188
|
+
}
|
|
189
|
+
acceptLine(child, line) {
|
|
190
|
+
const pending = this.pending;
|
|
191
|
+
if (!pending || !line || line.length > 4096) {
|
|
192
|
+
this.failChild(child, new Error("Windows private ACL broker returned malformed output."));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
let response;
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(line);
|
|
198
|
+
const status = String(parsed.status);
|
|
199
|
+
const expectedKeys = status === "error" ? "error_code\0id\0status" : "id\0status";
|
|
200
|
+
if (Object.keys(parsed).sort().join("\0") !== expectedKeys || parsed.id !== pending.id || !["ok", "missing", "error"].includes(status) || status === "error" && ![
|
|
201
|
+
"request_validation",
|
|
202
|
+
"path_lookup",
|
|
203
|
+
"principal_resolution",
|
|
204
|
+
"acl_read",
|
|
205
|
+
"owner_validation",
|
|
206
|
+
"acl_mutation",
|
|
207
|
+
"acl_write",
|
|
208
|
+
"acl_readback",
|
|
209
|
+
"acl_verification"
|
|
210
|
+
].includes(String(parsed.error_code))) throw new Error("invalid response");
|
|
211
|
+
response = parsed;
|
|
212
|
+
} catch {
|
|
213
|
+
this.failChild(child, new Error("Windows private ACL broker returned malformed output."));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.pending = null;
|
|
217
|
+
clearTimeout(pending.timer);
|
|
218
|
+
if (response.status === "ok") {
|
|
219
|
+
pending.resolve();
|
|
220
|
+
} else if (response.status === "missing") {
|
|
221
|
+
const error = new Error("Windows private path no longer exists.");
|
|
222
|
+
error.code = "ENOENT";
|
|
223
|
+
pending.reject(error);
|
|
224
|
+
} else {
|
|
225
|
+
pending.reject(new Error(
|
|
226
|
+
`Windows private ACL ${pending.mode} failed (${response.error_code}).`
|
|
227
|
+
));
|
|
228
|
+
}
|
|
229
|
+
this.scheduleIdleShutdown();
|
|
230
|
+
}
|
|
231
|
+
ensureChild() {
|
|
232
|
+
if (this.child && this.child.exitCode === null && !this.child.killed) {
|
|
233
|
+
return { child: this.child, startedNow: false };
|
|
234
|
+
}
|
|
235
|
+
const child = this.spawnProcess();
|
|
236
|
+
this.child = child;
|
|
237
|
+
this.outputBuffer = "";
|
|
238
|
+
child.stdout.setEncoding("utf8");
|
|
239
|
+
child.stdout.on("data", (chunk) => {
|
|
240
|
+
if (this.child !== child) return;
|
|
241
|
+
this.outputBuffer += chunk;
|
|
242
|
+
if (this.outputBuffer.length > 65536) {
|
|
243
|
+
this.failChild(child, new Error("Windows private ACL broker exceeded its output limit."));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
while (this.child === child) {
|
|
247
|
+
const newline = this.outputBuffer.indexOf("\n");
|
|
248
|
+
if (newline < 0) break;
|
|
249
|
+
const line = this.outputBuffer.slice(0, newline).trim();
|
|
250
|
+
this.outputBuffer = this.outputBuffer.slice(newline + 1);
|
|
251
|
+
this.acceptLine(child, line);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
child.stderr.on("data", () => void 0);
|
|
255
|
+
child.once("error", () => {
|
|
256
|
+
this.failChild(child, new Error("Windows private ACL broker failed to start."));
|
|
257
|
+
});
|
|
258
|
+
child.once("exit", () => {
|
|
259
|
+
this.failChild(child, new Error("Windows private ACL broker exited before confirmation."));
|
|
260
|
+
});
|
|
261
|
+
child.unref();
|
|
262
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) {
|
|
263
|
+
stream.unref?.();
|
|
264
|
+
}
|
|
265
|
+
return { child, startedNow: true };
|
|
266
|
+
}
|
|
267
|
+
async dispatch(path, kind, mode) {
|
|
268
|
+
if (this.idleTimer) {
|
|
269
|
+
clearTimeout(this.idleTimer);
|
|
270
|
+
this.idleTimer = null;
|
|
271
|
+
}
|
|
272
|
+
const id = ++this.requestSequence;
|
|
273
|
+
const request = `${JSON.stringify({
|
|
274
|
+
id,
|
|
275
|
+
kind,
|
|
276
|
+
mode,
|
|
277
|
+
path_b64: Buffer.from(path, "utf8").toString("base64")
|
|
278
|
+
})}
|
|
279
|
+
`;
|
|
280
|
+
if (Buffer.byteLength(request, "utf8") > 65536) {
|
|
281
|
+
throw new Error("Windows private ACL broker request exceeds its input limit.");
|
|
282
|
+
}
|
|
283
|
+
const { child, startedNow } = this.ensureChild();
|
|
284
|
+
const timeoutMs = startedNow ? this.startupRequestTimeoutMs : this.requestTimeoutMs;
|
|
285
|
+
await new Promise((resolvePromise, reject) => {
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
this.failChild(child, new Error("Windows private ACL broker request timed out."));
|
|
288
|
+
}, timeoutMs);
|
|
289
|
+
this.pending = { id, mode, resolve: resolvePromise, reject, timer };
|
|
290
|
+
child.stdin.write(request, "utf8", (error) => {
|
|
291
|
+
if (error) {
|
|
292
|
+
this.failChild(child, new Error("Windows private ACL broker request failed."));
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
close() {
|
|
298
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
299
|
+
this.idleTimer = null;
|
|
300
|
+
const child = this.child;
|
|
301
|
+
this.child = null;
|
|
302
|
+
this.outputBuffer = "";
|
|
303
|
+
if (child) child.stdin.end();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
var defaultWindowsPrivateAclBroker = new WindowsPrivateAclBroker();
|
|
307
|
+
var windowsBootIdentityInvocation = () => {
|
|
308
|
+
const script = `$ErrorActionPreference='Stop';$s=[System.Management.ManagementObjectSearcher]::new('SELECT LastBootUpTime FROM Win32_OperatingSystem');try{$c=$s.Get();$e=$c.GetEnumerator();if(-not $e.MoveNext()){throw 'boot identity unavailable'};$raw=[string]$e.Current['LastBootUpTime'];$v=[System.Management.ManagementDateTimeConverter]::ToDateTime($raw).ToUniversalTime().ToFileTimeUtc();[Console]::Out.Write($v)}finally{if($c){$c.Dispose()};$s.Dispose()}`;
|
|
309
|
+
return {
|
|
310
|
+
args: [
|
|
311
|
+
"-NoLogo",
|
|
312
|
+
"-NoProfile",
|
|
313
|
+
"-NonInteractive",
|
|
314
|
+
"-EncodedCommand",
|
|
315
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
316
|
+
],
|
|
317
|
+
env: windowsPowerShellEnvironment(),
|
|
318
|
+
timeoutMs: WINDOWS_IDENTITY_COMMAND_TIMEOUT_MS
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
var runSmallIdentityCommand = async (command, args, env, timeoutMs = SMALL_IDENTITY_COMMAND_TIMEOUT_MS) => await new Promise((resolvePromise) => {
|
|
322
|
+
const child = spawn(command, [...args], {
|
|
323
|
+
windowsHide: true,
|
|
324
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
325
|
+
env
|
|
326
|
+
});
|
|
327
|
+
const output = [];
|
|
328
|
+
let bytes = 0;
|
|
329
|
+
let settled = false;
|
|
330
|
+
const settle = (value) => {
|
|
331
|
+
if (settled) return;
|
|
332
|
+
settled = true;
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
resolvePromise(value);
|
|
335
|
+
};
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
child.kill("SIGKILL");
|
|
338
|
+
settle(null);
|
|
339
|
+
}, timeoutMs);
|
|
340
|
+
child.stdout.on("data", (chunk) => {
|
|
341
|
+
bytes += Buffer.byteLength(chunk);
|
|
342
|
+
if (bytes > 4096) {
|
|
343
|
+
child.kill("SIGKILL");
|
|
344
|
+
settle(null);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
output.push(Buffer.from(chunk));
|
|
348
|
+
});
|
|
349
|
+
child.once("error", () => settle(null));
|
|
350
|
+
child.once("exit", (code) => settle(
|
|
351
|
+
code === 0 ? Buffer.concat(output).toString("utf8").trim() || null : null
|
|
352
|
+
));
|
|
353
|
+
});
|
|
354
|
+
var cachedSystemBootIdentity = null;
|
|
355
|
+
var readInferenceSystemBootIdentity = async (platform = process.platform) => {
|
|
356
|
+
if (platform === process.platform && cachedSystemBootIdentity) {
|
|
357
|
+
return await cachedSystemBootIdentity;
|
|
358
|
+
}
|
|
359
|
+
const read = async () => {
|
|
360
|
+
let identity = null;
|
|
361
|
+
if (platform === "linux") {
|
|
362
|
+
try {
|
|
363
|
+
identity = (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim();
|
|
364
|
+
} catch {
|
|
365
|
+
identity = null;
|
|
366
|
+
}
|
|
367
|
+
} else if (platform === "win32") {
|
|
368
|
+
const invocation = windowsBootIdentityInvocation();
|
|
369
|
+
identity = await runSmallIdentityCommand(
|
|
370
|
+
"powershell.exe",
|
|
371
|
+
invocation.args,
|
|
372
|
+
invocation.env,
|
|
373
|
+
invocation.timeoutMs
|
|
374
|
+
);
|
|
375
|
+
} else if (platform === "darwin") {
|
|
376
|
+
identity = await runSmallIdentityCommand("sysctl", ["-n", "kern.boottime"], void 0);
|
|
377
|
+
}
|
|
378
|
+
if (!identity || /[\r\n\0]/u.test(identity)) {
|
|
379
|
+
throw new Error("Inference host could not establish the current system boot identity.");
|
|
380
|
+
}
|
|
381
|
+
return `${platform}:${identity}`;
|
|
382
|
+
};
|
|
383
|
+
const pending = read();
|
|
384
|
+
if (platform === process.platform) cachedSystemBootIdentity = pending;
|
|
385
|
+
try {
|
|
386
|
+
return await pending;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (platform === process.platform) cachedSystemBootIdentity = null;
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// lib/inference-host/codex-app-server.ts
|
|
394
|
+
var CODEX_INFERENCE_PERMISSION_PROFILE = "vtx_inference_readonly";
|
|
395
|
+
var CODEX_AGENT_PERMISSION_PROFILE = "vtx_inference_agent";
|
|
396
|
+
var CODEX_REASONING_CONTENT_MAX_UTF8_BYTES = 32768;
|
|
397
|
+
var CODEX_ACCOUNT_PLAN_TYPES = [
|
|
398
|
+
"free",
|
|
399
|
+
"go",
|
|
400
|
+
"plus",
|
|
401
|
+
"pro",
|
|
402
|
+
"prolite",
|
|
403
|
+
"team",
|
|
404
|
+
"self_serve_business_prolite",
|
|
405
|
+
"self_serve_business_usage_based",
|
|
406
|
+
"business",
|
|
407
|
+
"ent26",
|
|
408
|
+
"enterprise_cbp_automation",
|
|
409
|
+
"enterprise_cbp_usage_based",
|
|
410
|
+
"enterprise",
|
|
411
|
+
"edu",
|
|
412
|
+
"unknown"
|
|
413
|
+
];
|
|
414
|
+
var CODEX_AGENT_DYNAMIC_TOOLS = [
|
|
415
|
+
"vtx_set_schedule",
|
|
416
|
+
"vtx_get_data",
|
|
417
|
+
"vtx_submit_decision",
|
|
418
|
+
"vtx_decision_status"
|
|
419
|
+
];
|
|
420
|
+
var CodexAppServerError = class extends Error {
|
|
421
|
+
constructor(options) {
|
|
422
|
+
super(options.message, { cause: options.cause });
|
|
423
|
+
this.name = "CodexAppServerError";
|
|
424
|
+
this.category = options.category;
|
|
425
|
+
this.code = options.code;
|
|
426
|
+
this.retryable = options.retryable;
|
|
427
|
+
this.dispatchOutcome = options.dispatchOutcome ?? "not_dispatched";
|
|
428
|
+
this.httpStatusCode = options.httpStatusCode ?? null;
|
|
429
|
+
this.retryAtMs = options.retryAtMs ?? null;
|
|
430
|
+
this.usage = options.usage ?? null;
|
|
431
|
+
this.terminalEvidence = options.terminalEvidence ?? null;
|
|
432
|
+
this.rateLimits = options.rateLimits ?? null;
|
|
433
|
+
this.processExit = options.processExit ?? null;
|
|
434
|
+
this.grokToolValidation = options.grokToolValidation ?? null;
|
|
435
|
+
this.grokResponseIdentity = options.grokResponseIdentity ?? null;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
var objectOrNull = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
439
|
+
var finiteToken = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
440
|
+
var tokenUsageFromBreakdown = (value) => {
|
|
441
|
+
const candidate = objectOrNull(value);
|
|
442
|
+
if (!candidate) return null;
|
|
443
|
+
const inputTokens = finiteToken(candidate.inputTokens ?? candidate.input_tokens);
|
|
444
|
+
const cachedInputTokens = finiteToken(
|
|
445
|
+
candidate.cachedInputTokens ?? candidate.cached_input_tokens ?? objectOrNull(candidate.input_tokens_details)?.cached_tokens
|
|
446
|
+
);
|
|
447
|
+
const outputTokens = finiteToken(candidate.outputTokens ?? candidate.output_tokens);
|
|
448
|
+
const reasoningOutputTokens = finiteToken(
|
|
449
|
+
candidate.reasoningOutputTokens ?? candidate.reasoning_output_tokens ?? objectOrNull(candidate.output_tokens_details)?.reasoning_tokens
|
|
450
|
+
);
|
|
451
|
+
const totalTokens = finiteToken(candidate.totalTokens ?? candidate.total_tokens);
|
|
452
|
+
if (inputTokens === null || cachedInputTokens === null || outputTokens === null || reasoningOutputTokens === null || totalTokens === null || cachedInputTokens > inputTokens || reasoningOutputTokens > outputTokens || totalTokens !== inputTokens + outputTokens) return null;
|
|
453
|
+
const rawCacheWrite = candidate.cacheWriteInputTokens ?? candidate.cache_write_input_tokens;
|
|
454
|
+
const cacheWrite = rawCacheWrite === void 0 ? null : finiteToken(rawCacheWrite);
|
|
455
|
+
if (rawCacheWrite !== void 0 && cacheWrite === null) return null;
|
|
456
|
+
return {
|
|
457
|
+
inputTokens,
|
|
458
|
+
cachedInputTokens,
|
|
459
|
+
outputTokens,
|
|
460
|
+
reasoningOutputTokens,
|
|
461
|
+
totalTokens,
|
|
462
|
+
cacheWriteInputTokens: cacheWrite,
|
|
463
|
+
cacheWriteSupported: rawCacheWrite !== void 0
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
var usageFromNotification = (method, params) => {
|
|
467
|
+
if (method === "thread/tokenUsage/updated") {
|
|
468
|
+
const tokenUsage = objectOrNull(params.tokenUsage);
|
|
469
|
+
return tokenUsageFromBreakdown(tokenUsage?.last) ?? tokenUsageFromBreakdown(tokenUsage?.total);
|
|
470
|
+
}
|
|
471
|
+
if (method === "rawResponse/completed") {
|
|
472
|
+
return tokenUsageFromBreakdown(params.usage);
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
};
|
|
476
|
+
var validPlanType = (value) => typeof value === "string" && CODEX_ACCOUNT_PLAN_TYPES.includes(value);
|
|
477
|
+
var nonnegativeSafeIntegerOrNull = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
|
|
478
|
+
var nonnegativeFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
479
|
+
var parseRateLimitWindow = (value) => {
|
|
480
|
+
if (value === null) return null;
|
|
481
|
+
const candidate = objectOrNull(value);
|
|
482
|
+
if (!candidate) throw new Error("invalid rate-limit window");
|
|
483
|
+
const usedPercent = nonnegativeFiniteNumberOrNull(candidate.usedPercent);
|
|
484
|
+
const windowDurationMins = candidate.windowDurationMins == null ? null : nonnegativeSafeIntegerOrNull(candidate.windowDurationMins);
|
|
485
|
+
const resetsAt = candidate.resetsAt == null ? null : nonnegativeSafeIntegerOrNull(candidate.resetsAt);
|
|
486
|
+
if (usedPercent === null || windowDurationMins === null && candidate.windowDurationMins != null || resetsAt === null && candidate.resetsAt != null) {
|
|
487
|
+
throw new Error("invalid rate-limit window");
|
|
488
|
+
}
|
|
489
|
+
return { usedPercent, windowDurationMins, resetsAt };
|
|
490
|
+
};
|
|
491
|
+
var RATE_LIMIT_REACHED_TYPES = [
|
|
492
|
+
"rate_limit_reached",
|
|
493
|
+
"workspace_owner_credits_depleted",
|
|
494
|
+
"workspace_member_credits_depleted",
|
|
495
|
+
"workspace_owner_usage_limit_reached",
|
|
496
|
+
"workspace_member_usage_limit_reached"
|
|
497
|
+
];
|
|
498
|
+
var DEFAULT_CODEX_QUOTA_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
499
|
+
var MAX_CODEX_QUOTA_COOLDOWN_MS = 8 * 24 * 60 * 60 * 1e3;
|
|
500
|
+
var CODEX_TRANSIENT_RATE_LIMIT_COOLDOWN_MS = 30 * 1e3;
|
|
501
|
+
var parseRateLimitSnapshot = (value) => {
|
|
502
|
+
const candidate = objectOrNull(value);
|
|
503
|
+
if (!candidate) throw new Error("invalid rate-limit snapshot");
|
|
504
|
+
const limitId = candidate.limitId === null || candidate.limitId === void 0 ? null : typeof candidate.limitId === "string" && candidate.limitId.trim() ? candidate.limitId.trim() : void 0;
|
|
505
|
+
const limitName = candidate.limitName === null || candidate.limitName === void 0 ? null : typeof candidate.limitName === "string" && candidate.limitName.trim() ? candidate.limitName.trim() : void 0;
|
|
506
|
+
const planType = candidate.planType === null || candidate.planType === void 0 ? null : validPlanType(candidate.planType) ? candidate.planType : void 0;
|
|
507
|
+
const reached = candidate.rateLimitReachedType === null || candidate.rateLimitReachedType === void 0 ? null : typeof candidate.rateLimitReachedType === "string" && RATE_LIMIT_REACHED_TYPES.includes(candidate.rateLimitReachedType) ? candidate.rateLimitReachedType : void 0;
|
|
508
|
+
const spendControlReached = candidate.spendControlReached === null || candidate.spendControlReached === void 0 ? null : typeof candidate.spendControlReached === "boolean" ? candidate.spendControlReached : void 0;
|
|
509
|
+
if (limitId === void 0 || limitName === void 0 || planType === void 0 || reached === void 0 || spendControlReached === void 0) throw new Error("invalid rate-limit snapshot");
|
|
510
|
+
return {
|
|
511
|
+
limitId,
|
|
512
|
+
limitName,
|
|
513
|
+
planType,
|
|
514
|
+
primary: parseRateLimitWindow(candidate.primary ?? null),
|
|
515
|
+
secondary: parseRateLimitWindow(candidate.secondary ?? null),
|
|
516
|
+
rateLimitReachedType: reached,
|
|
517
|
+
spendControlReached
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
var codexRateLimitRetryAtMs = (accountRateLimits) => {
|
|
521
|
+
const snapshots = [accountRateLimits.rateLimits];
|
|
522
|
+
const exhaustedResets = [];
|
|
523
|
+
const reachedResets = [];
|
|
524
|
+
for (const snapshot of snapshots) {
|
|
525
|
+
const windows = [snapshot.primary, snapshot.secondary].filter(
|
|
526
|
+
(window) => window !== null
|
|
527
|
+
);
|
|
528
|
+
for (const window of windows) {
|
|
529
|
+
if (window.resetsAt === null) continue;
|
|
530
|
+
const resetAtMs = window.resetsAt * 1e3;
|
|
531
|
+
if (!Number.isSafeInteger(resetAtMs)) continue;
|
|
532
|
+
if (window.usedPercent >= 100) exhaustedResets.push(resetAtMs);
|
|
533
|
+
if (snapshot.rateLimitReachedType !== null || snapshot.spendControlReached === true) {
|
|
534
|
+
reachedResets.push(resetAtMs);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const candidates = exhaustedResets.length > 0 ? exhaustedResets : reachedResets.length > 0 ? reachedResets : [];
|
|
539
|
+
return candidates.length > 0 ? Math.max(...candidates) : null;
|
|
540
|
+
};
|
|
541
|
+
var codexAccountRateLimitReached = (accountRateLimits) => [accountRateLimits.rateLimits].some((snapshot) => snapshot.rateLimitReachedType !== null || snapshot.spendControlReached === true || snapshot.primary?.usedPercent !== void 0 && snapshot.primary.usedPercent >= 100 || snapshot.secondary?.usedPercent !== void 0 && snapshot.secondary.usedPercent >= 100);
|
|
542
|
+
var forbiddenMethod = (method) => method.startsWith("mcpServer/") || method.startsWith("serverRequest/") || method.startsWith("item/mcpToolCall") || method.startsWith("item/commandExecution") || method.startsWith("item/fileChange") || method.startsWith("item/dynamicToolCall") || method.startsWith("item/collabAgentToolCall");
|
|
543
|
+
var forbiddenTerminalItem = (item) => ![
|
|
544
|
+
"userMessage",
|
|
545
|
+
"agentMessage",
|
|
546
|
+
"reasoning"
|
|
547
|
+
].includes(String(item.type || ""));
|
|
548
|
+
var forbiddenAgentMethod = (method) => method.startsWith("mcpServer/") || method.startsWith("serverRequest/") || method.startsWith("item/mcpToolCall") || method.startsWith("item/commandExecution") || method.startsWith("item/fileChange") || method.startsWith("item/collabAgentToolCall");
|
|
549
|
+
var forbiddenAgentTerminalItem = (item) => ![
|
|
550
|
+
"userMessage",
|
|
551
|
+
"agentMessage",
|
|
552
|
+
"reasoning",
|
|
553
|
+
"dynamicToolCall",
|
|
554
|
+
"webSearch",
|
|
555
|
+
"contextCompaction"
|
|
556
|
+
].includes(String(item.type || ""));
|
|
557
|
+
var createCodexAgentToolSpecs = (dataContract, decisionSchema) => {
|
|
558
|
+
if (dataContract.length === 0 || Object.keys(decisionSchema).length === 0) {
|
|
559
|
+
throw new CodexAppServerError({
|
|
560
|
+
message: "Codex Agent data contract is unavailable.",
|
|
561
|
+
category: "schema",
|
|
562
|
+
code: "invalid_agent_data_contract",
|
|
563
|
+
retryable: false
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const dataRequestBranches = dataContract.map((descriptor) => ({
|
|
567
|
+
type: "object",
|
|
568
|
+
additionalProperties: false,
|
|
569
|
+
required: ["capability", "arguments"],
|
|
570
|
+
description: descriptor.description,
|
|
571
|
+
properties: {
|
|
572
|
+
capability: {
|
|
573
|
+
type: "string",
|
|
574
|
+
const: descriptor.id,
|
|
575
|
+
title: descriptor.title
|
|
576
|
+
},
|
|
577
|
+
arguments: descriptor.input_schema
|
|
578
|
+
}
|
|
579
|
+
}));
|
|
580
|
+
return [
|
|
581
|
+
{
|
|
582
|
+
type: "function",
|
|
583
|
+
name: "vtx_get_data",
|
|
584
|
+
description: "Request assignment-scoped VTX data using one exact canonical capability schema.",
|
|
585
|
+
inputSchema: {
|
|
586
|
+
oneOf: dataRequestBranches
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
type: "function",
|
|
591
|
+
name: "vtx_submit_decision",
|
|
592
|
+
description: "Submit one VTX structured trading decision candidate using the assignment decision schema.",
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: "object",
|
|
595
|
+
additionalProperties: false,
|
|
596
|
+
required: ["candidate"],
|
|
597
|
+
properties: { candidate: decisionSchema }
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
type: "function",
|
|
602
|
+
name: "vtx_decision_status",
|
|
603
|
+
description: "Resolve the durable status of a previously attempted decision operation.",
|
|
604
|
+
inputSchema: {
|
|
605
|
+
type: "object",
|
|
606
|
+
additionalProperties: false,
|
|
607
|
+
required: ["operation_id"],
|
|
608
|
+
properties: { operation_id: { type: "string", minLength: 1 } }
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
type: "function",
|
|
613
|
+
name: "vtx_set_schedule",
|
|
614
|
+
description: "Configure or cancel recurring market observations for this assignment. This never submits a trading decision.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: "object",
|
|
617
|
+
additionalProperties: false,
|
|
618
|
+
required: ["schedule"],
|
|
619
|
+
properties: { schedule: { anyOf: [{ type: "null" }, {
|
|
620
|
+
type: "object",
|
|
621
|
+
additionalProperties: false,
|
|
622
|
+
required: ["interval_seconds", "objective", "observation"],
|
|
623
|
+
properties: {
|
|
624
|
+
interval_seconds: { type: "integer", minimum: 1 },
|
|
625
|
+
objective: { type: "string", minLength: 1, maxLength: 4e3 },
|
|
626
|
+
observation: { type: "object", additionalProperties: false, required: ["capability", "arguments"], properties: {
|
|
627
|
+
capability: { const: "market.snapshot", type: "string" },
|
|
628
|
+
arguments: dataContract.find((descriptor) => descriptor.id === "market.snapshot")?.input_schema ?? { type: "object" }
|
|
629
|
+
} }
|
|
630
|
+
}
|
|
631
|
+
}] } }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
];
|
|
635
|
+
};
|
|
636
|
+
var classifyCodexTurnFailure = (codexErrorInfo, terminalStatus) => {
|
|
637
|
+
if (terminalStatus === "interrupted") {
|
|
638
|
+
return { code: "cancelled", category: "cancelled", retryable: false, httpStatusCode: null };
|
|
639
|
+
}
|
|
640
|
+
if (codexErrorInfo === "usageLimitExceeded") {
|
|
641
|
+
return { code: "quota_exceeded", category: "model", retryable: false, httpStatusCode: null };
|
|
642
|
+
}
|
|
643
|
+
if (codexErrorInfo === "unauthorized") {
|
|
644
|
+
return { code: "auth_expired", category: "auth", retryable: false, httpStatusCode: null };
|
|
645
|
+
}
|
|
646
|
+
const stableStringCodes = {
|
|
647
|
+
badRequest: "codex_bad_request",
|
|
648
|
+
contextWindowExceeded: "context_window_exceeded",
|
|
649
|
+
cyberPolicy: "codex_policy_rejected",
|
|
650
|
+
internalServerError: "codex_internal_error",
|
|
651
|
+
sandboxError: "codex_sandbox_error",
|
|
652
|
+
serverOverloaded: "codex_server_overloaded",
|
|
653
|
+
sessionBudgetExceeded: "session_budget_exceeded",
|
|
654
|
+
threadRollbackFailed: "codex_thread_rollback_failed"
|
|
655
|
+
};
|
|
656
|
+
if (typeof codexErrorInfo === "string" && stableStringCodes[codexErrorInfo]) {
|
|
657
|
+
const code = stableStringCodes[codexErrorInfo];
|
|
658
|
+
return {
|
|
659
|
+
code,
|
|
660
|
+
category: "adapter",
|
|
661
|
+
retryable: code === "codex_internal_error" || code === "codex_server_overloaded",
|
|
662
|
+
httpStatusCode: null
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const structured = objectOrNull(codexErrorInfo);
|
|
666
|
+
if (structured) {
|
|
667
|
+
const variants = [
|
|
668
|
+
["httpConnectionFailed", "codex_http_connection_failed"],
|
|
669
|
+
["responseStreamConnectionFailed", "codex_response_stream_connection_failed"],
|
|
670
|
+
["responseStreamDisconnected", "codex_response_stream_disconnected"],
|
|
671
|
+
["responseTooManyFailedAttempts", "codex_provider_retry_exhausted"]
|
|
672
|
+
];
|
|
673
|
+
for (const [variant, code] of variants) {
|
|
674
|
+
if (!(variant in structured)) continue;
|
|
675
|
+
const details = objectOrNull(structured[variant]);
|
|
676
|
+
const rawStatus = details?.httpStatusCode;
|
|
677
|
+
const httpStatusCode = Number.isSafeInteger(rawStatus) && Number(rawStatus) >= 100 && Number(rawStatus) <= 599 ? Number(rawStatus) : null;
|
|
678
|
+
if (httpStatusCode === 401) {
|
|
679
|
+
return {
|
|
680
|
+
code: "auth_expired",
|
|
681
|
+
category: "auth",
|
|
682
|
+
retryable: false,
|
|
683
|
+
httpStatusCode
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (httpStatusCode === 429) {
|
|
687
|
+
return {
|
|
688
|
+
code: "codex_rate_limited",
|
|
689
|
+
category: "adapter",
|
|
690
|
+
retryable: true,
|
|
691
|
+
httpStatusCode
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (httpStatusCode !== null && httpStatusCode >= 400 && httpStatusCode < 500 && httpStatusCode !== 408 && httpStatusCode !== 425) {
|
|
695
|
+
const semanticText = JSON.stringify(details || {}).toLowerCase();
|
|
696
|
+
if (httpStatusCode === 402 || /(quota|credit|spend|payment|subscription.*limit)/u.test(semanticText)) {
|
|
697
|
+
return {
|
|
698
|
+
code: "quota_exceeded",
|
|
699
|
+
category: "model",
|
|
700
|
+
retryable: false,
|
|
701
|
+
httpStatusCode
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (httpStatusCode === 403 && /(auth|account|workspace|billing|entitlement)/u.test(semanticText)) {
|
|
705
|
+
return {
|
|
706
|
+
code: "auth_expired",
|
|
707
|
+
category: "auth",
|
|
708
|
+
retryable: false,
|
|
709
|
+
httpStatusCode
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
if (httpStatusCode === 403 && /(policy|safety|cyber)/u.test(semanticText)) {
|
|
713
|
+
return {
|
|
714
|
+
code: "codex_policy_rejected",
|
|
715
|
+
category: "adapter",
|
|
716
|
+
retryable: false,
|
|
717
|
+
httpStatusCode
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
code: httpStatusCode === 400 ? "codex_bad_request" : "codex_http_client_error",
|
|
722
|
+
category: "adapter",
|
|
723
|
+
retryable: false,
|
|
724
|
+
httpStatusCode
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
code,
|
|
729
|
+
category: "adapter",
|
|
730
|
+
retryable: httpStatusCode === null ? true : httpStatusCode === 408 || httpStatusCode === 425 || httpStatusCode >= 500,
|
|
731
|
+
httpStatusCode
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return { code: "turn_failed", category: "adapter", retryable: false, httpStatusCode: null };
|
|
736
|
+
};
|
|
737
|
+
var scrubbedCodexEnvironment = (codexHome) => {
|
|
738
|
+
if (process.platform === "win32") {
|
|
739
|
+
const systemRoot = String(process.env.SystemRoot || "C:\\Windows");
|
|
740
|
+
return {
|
|
741
|
+
CODEX_HOME: codexHome,
|
|
742
|
+
HOME: codexHome,
|
|
743
|
+
USERPROFILE: codexHome,
|
|
744
|
+
NODE_ENV: "production",
|
|
745
|
+
LANG: "C.UTF-8",
|
|
746
|
+
PATH: `${systemRoot}\\System32`,
|
|
747
|
+
SystemRoot: systemRoot,
|
|
748
|
+
WINDIR: String(process.env.WINDIR || systemRoot),
|
|
749
|
+
TEMP: tmpdir(),
|
|
750
|
+
TMP: tmpdir(),
|
|
751
|
+
TZ: "America/New_York"
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
CODEX_HOME: codexHome,
|
|
756
|
+
HOME: codexHome,
|
|
757
|
+
NODE_ENV: "production",
|
|
758
|
+
LANG: "C.UTF-8",
|
|
759
|
+
LC_ALL: "C.UTF-8",
|
|
760
|
+
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
761
|
+
SHELL: "/bin/bash",
|
|
762
|
+
SSL_CERT_DIR: "/etc/ssl/certs",
|
|
763
|
+
TMPDIR: "/tmp",
|
|
764
|
+
TZ: "America/New_York",
|
|
765
|
+
XDG_RUNTIME_DIR: codexHome
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
var killWindowsProcessTree = async (pid) => {
|
|
769
|
+
await new Promise((resolve3, reject) => {
|
|
770
|
+
const child = spawn2(
|
|
771
|
+
"taskkill.exe",
|
|
772
|
+
["/PID", String(pid), "/T", "/F"],
|
|
773
|
+
{
|
|
774
|
+
windowsHide: true,
|
|
775
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
776
|
+
env: {
|
|
777
|
+
NODE_ENV: "production",
|
|
778
|
+
SystemRoot: String(process.env.SystemRoot || "C:\\Windows"),
|
|
779
|
+
WINDIR: String(process.env.WINDIR || process.env.SystemRoot || "C:\\Windows"),
|
|
780
|
+
PATH: `${String(process.env.SystemRoot || "C:\\Windows")}\\System32`
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
);
|
|
784
|
+
const timer = setTimeout(() => {
|
|
785
|
+
child.kill();
|
|
786
|
+
reject(new Error("Windows Codex process-tree cleanup timed out."));
|
|
787
|
+
}, 5e3);
|
|
788
|
+
child.once("error", (error) => {
|
|
789
|
+
clearTimeout(timer);
|
|
790
|
+
reject(error);
|
|
791
|
+
});
|
|
792
|
+
child.once("close", (code) => {
|
|
793
|
+
clearTimeout(timer);
|
|
794
|
+
if (code === 0 || code === 128) resolve3();
|
|
795
|
+
else reject(new Error("Windows Codex process-tree cleanup failed."));
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
var appServerArgs = (enableAgentCodeModeHost = false) => {
|
|
800
|
+
const args = [
|
|
801
|
+
"app-server",
|
|
802
|
+
"--stdio",
|
|
803
|
+
"--strict-config",
|
|
804
|
+
"-c",
|
|
805
|
+
'approval_policy="never"',
|
|
806
|
+
"-c",
|
|
807
|
+
'shell_environment_policy.inherit="none"',
|
|
808
|
+
"-c",
|
|
809
|
+
'shell_environment_policy.include_only=["PATH","LANG","LC_ALL","SHELL","TMPDIR","TZ","SSL_CERT_DIR"]'
|
|
810
|
+
];
|
|
811
|
+
for (const feature of [
|
|
812
|
+
"fast_mode",
|
|
813
|
+
"plugins",
|
|
814
|
+
"unified_exec",
|
|
815
|
+
"apps",
|
|
816
|
+
"browser_use",
|
|
817
|
+
"in_app_browser",
|
|
818
|
+
"computer_use",
|
|
819
|
+
"memories",
|
|
820
|
+
"image_generation",
|
|
821
|
+
"multi_agent",
|
|
822
|
+
...enableAgentCodeModeHost ? [] : ["code_mode_host"]
|
|
823
|
+
]) args.push("--disable", feature);
|
|
824
|
+
if (enableAgentCodeModeHost) args.push("--enable", "code_mode_host");
|
|
825
|
+
return args;
|
|
826
|
+
};
|
|
827
|
+
var GUARDIAN_SCRIPT = String.raw`
|
|
828
|
+
import { spawn } from 'node:child_process';
|
|
829
|
+
import { chmod, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
830
|
+
import { dirname } from 'node:path';
|
|
831
|
+
const input = JSON.parse(Buffer.from(process.argv[1], 'base64url').toString('utf8'));
|
|
832
|
+
// The guardian owns the process boundary for every attempt. Apply a private
|
|
833
|
+
// umask before writing receipts or starting Codex so a lost thread/start reply
|
|
834
|
+
// still leaves traversable 0700 directories and removable 0600 rollout files.
|
|
835
|
+
process.umask(0o077);
|
|
836
|
+
const replaceReceipt = async (temporary) => {
|
|
837
|
+
const retryableWindowsCodes = new Set(['EACCES', 'EBUSY', 'EEXIST', 'ENOTEMPTY', 'EPERM']);
|
|
838
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
839
|
+
try {
|
|
840
|
+
await rename(temporary, input.receiptPath);
|
|
841
|
+
return;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (
|
|
844
|
+
process.platform !== 'win32'
|
|
845
|
+
|| !retryableWindowsCodes.has(String(error?.code || ''))
|
|
846
|
+
|| attempt === 39
|
|
847
|
+
) throw error;
|
|
848
|
+
try {
|
|
849
|
+
await rm(input.receiptPath, { force: true });
|
|
850
|
+
} catch (removeError) {
|
|
851
|
+
if (!retryableWindowsCodes.has(String(removeError?.code || ''))) throw removeError;
|
|
852
|
+
}
|
|
853
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
let receiptWriteSequence = 0;
|
|
858
|
+
let receiptWriteChain = Promise.resolve();
|
|
859
|
+
const writeReceipt = (state, childPid, termination = null) => {
|
|
860
|
+
const write = async () => {
|
|
861
|
+
const body = JSON.stringify({
|
|
862
|
+
schema_version: 'vtx_codex_guardian_v1',
|
|
863
|
+
process_token: input.processToken,
|
|
864
|
+
state,
|
|
865
|
+
guardian_pid: process.pid,
|
|
866
|
+
child_pid: childPid ?? null,
|
|
867
|
+
boot_identity: input.bootIdentity,
|
|
868
|
+
termination,
|
|
869
|
+
updated_at: new Date().toISOString(),
|
|
870
|
+
}) + '\n';
|
|
871
|
+
receiptWriteSequence += 1;
|
|
872
|
+
const temporary = input.receiptPath + '.tmp-' + process.pid + '-' + receiptWriteSequence;
|
|
873
|
+
try {
|
|
874
|
+
await writeFile(temporary, body, { mode: 0o600 });
|
|
875
|
+
await chmod(temporary, 0o600).catch(() => undefined);
|
|
876
|
+
const file = await open(temporary, 'r+');
|
|
877
|
+
try { await file.sync(); } finally { await file.close(); }
|
|
878
|
+
await replaceReceipt(temporary);
|
|
879
|
+
if (process.platform !== 'win32') {
|
|
880
|
+
const directory = await open(dirname(input.receiptPath), 'r');
|
|
881
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
882
|
+
}
|
|
883
|
+
} finally {
|
|
884
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
const scheduled = receiptWriteChain.then(write, write);
|
|
888
|
+
receiptWriteChain = scheduled.catch(() => undefined);
|
|
889
|
+
return scheduled;
|
|
890
|
+
};
|
|
891
|
+
let child = null;
|
|
892
|
+
let shuttingDown = false;
|
|
893
|
+
let shutdownRequested = false;
|
|
894
|
+
let childStartedAt = null;
|
|
895
|
+
let forceTimer = null;
|
|
896
|
+
const stopChild = (expected) => {
|
|
897
|
+
if (expected) shutdownRequested = true;
|
|
898
|
+
if (shuttingDown) return;
|
|
899
|
+
shuttingDown = true;
|
|
900
|
+
if (!child) return;
|
|
901
|
+
child.stdin.end();
|
|
902
|
+
forceTimer = setTimeout(() => {
|
|
903
|
+
if (child.exitCode === null) child.kill('SIGKILL');
|
|
904
|
+
}, input.shutdownGraceMs);
|
|
905
|
+
forceTimer.unref();
|
|
906
|
+
};
|
|
907
|
+
let shutdownStarted = false;
|
|
908
|
+
const shutdown = async () => {
|
|
909
|
+
if (shutdownStarted) return;
|
|
910
|
+
shutdownStarted = true;
|
|
911
|
+
let expected = false;
|
|
912
|
+
try {
|
|
913
|
+
expected = (await readFile(input.shutdownIntentPath, 'utf8')) === input.processToken;
|
|
914
|
+
await rm(input.shutdownIntentPath, { force: true });
|
|
915
|
+
} catch {
|
|
916
|
+
expected = false;
|
|
917
|
+
}
|
|
918
|
+
stopChild(expected);
|
|
919
|
+
};
|
|
920
|
+
process.stdin.once('end', shutdown);
|
|
921
|
+
process.stdin.once('close', shutdown);
|
|
922
|
+
process.stdout.on('error', () => undefined);
|
|
923
|
+
process.stderr.on('error', () => undefined);
|
|
924
|
+
await writeReceipt('starting', null);
|
|
925
|
+
if (shuttingDown) {
|
|
926
|
+
await writeReceipt('terminated', null);
|
|
927
|
+
process.exit(0);
|
|
928
|
+
}
|
|
929
|
+
child = spawn(input.binary, input.args, {
|
|
930
|
+
env: process.env,
|
|
931
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
932
|
+
windowsHide: true,
|
|
933
|
+
detached: false,
|
|
934
|
+
});
|
|
935
|
+
childStartedAt = Date.now();
|
|
936
|
+
child.stdin.on('error', () => undefined);
|
|
937
|
+
process.stdin.pipe(child.stdin, { end: false });
|
|
938
|
+
child.stdout.pipe(process.stdout);
|
|
939
|
+
child.stderr.pipe(process.stderr);
|
|
940
|
+
child.once('spawn', async () => {
|
|
941
|
+
try {
|
|
942
|
+
await writeReceipt('running', child.pid ?? null);
|
|
943
|
+
} catch {
|
|
944
|
+
stopChild(false);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
child.once('error', () => stopChild(false));
|
|
948
|
+
child.once('close', async (code, signal) => {
|
|
949
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
950
|
+
// A pending promise alone does not keep Node's event loop alive after the
|
|
951
|
+
// child and parent stdin have both closed. Hold one local handle until the
|
|
952
|
+
// serialized terminal tombstone is durably replaced.
|
|
953
|
+
const receiptKeepAlive = setInterval(() => undefined, 1_000);
|
|
954
|
+
try {
|
|
955
|
+
const normalizedSignal = typeof signal === 'string' && /^SIG[A-Z0-9]{1,28}$/.test(signal)
|
|
956
|
+
? signal.toLowerCase()
|
|
957
|
+
: null;
|
|
958
|
+
const normalizedCode = normalizedSignal === null && Number.isInteger(code)
|
|
959
|
+
&& code >= -(2 ** 31) && code <= (2 ** 31) - 1
|
|
960
|
+
? code
|
|
961
|
+
: null;
|
|
962
|
+
await writeReceipt('terminated', child.pid ?? null, {
|
|
963
|
+
exit_code: normalizedCode,
|
|
964
|
+
signal: normalizedSignal,
|
|
965
|
+
expected: shutdownRequested,
|
|
966
|
+
uptime_ms: Math.min(
|
|
967
|
+
Math.max(0, Date.now() - (childStartedAt ?? Date.now())),
|
|
968
|
+
Number.MAX_SAFE_INTEGER,
|
|
969
|
+
),
|
|
970
|
+
});
|
|
971
|
+
} finally {
|
|
972
|
+
clearInterval(receiptKeepAlive);
|
|
973
|
+
process.exit(0);
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
`;
|
|
977
|
+
var WINDOWS_RECEIPT_REPLACE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
978
|
+
"EACCES",
|
|
979
|
+
"EBUSY",
|
|
980
|
+
"EEXIST",
|
|
981
|
+
"ENOTEMPTY",
|
|
982
|
+
"EPERM"
|
|
983
|
+
]);
|
|
984
|
+
var MAX_RETIRED_RESPONSE_IDS = 256;
|
|
985
|
+
var LATE_RESPONSE_SAFE_METHODS = /* @__PURE__ */ new Set([
|
|
986
|
+
"account/rateLimits/read",
|
|
987
|
+
"turn/start",
|
|
988
|
+
"turn/interrupt"
|
|
989
|
+
]);
|
|
990
|
+
var replaceCodexGuardianReceiptFile = async (temporaryPath, receiptPath, options = {}) => {
|
|
991
|
+
const platform = options.platform ?? process.platform;
|
|
992
|
+
const renameFile = options.renameFile ?? rename2;
|
|
993
|
+
const removeFile = options.removeFile ?? ((path) => rm2(path, { force: true }));
|
|
994
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
|
|
995
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 40);
|
|
996
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? 25);
|
|
997
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
998
|
+
try {
|
|
999
|
+
await renameFile(temporaryPath, receiptPath);
|
|
1000
|
+
return;
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
const code = String(error.code ?? "");
|
|
1003
|
+
if (platform !== "win32" || !WINDOWS_RECEIPT_REPLACE_ERROR_CODES.has(code) || attempt === maxAttempts - 1) {
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
await removeFile(receiptPath);
|
|
1008
|
+
} catch (removeError) {
|
|
1009
|
+
const removeCode = String(removeError.code ?? "");
|
|
1010
|
+
if (!WINDOWS_RECEIPT_REPLACE_ERROR_CODES.has(removeCode)) {
|
|
1011
|
+
throw removeError;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
await sleep(retryDelayMs);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
var writeCodexGuardianSpawnIntent = async (guardian) => {
|
|
1019
|
+
const body = `${JSON.stringify({
|
|
1020
|
+
schema_version: "vtx_codex_guardian_v1",
|
|
1021
|
+
process_token: guardian.processToken,
|
|
1022
|
+
state: "spawn_intent",
|
|
1023
|
+
guardian_pid: null,
|
|
1024
|
+
child_pid: null,
|
|
1025
|
+
boot_identity: guardian.bootIdentity ?? null,
|
|
1026
|
+
termination: null,
|
|
1027
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1028
|
+
})}
|
|
1029
|
+
`;
|
|
1030
|
+
const temporary = `${guardian.receiptPath}.intent-${process.pid}`;
|
|
1031
|
+
await writeFile(temporary, body, { mode: 384 });
|
|
1032
|
+
await chmod(temporary, 384).catch(() => void 0);
|
|
1033
|
+
const handle = await open2(temporary, "r+");
|
|
1034
|
+
try {
|
|
1035
|
+
await handle.sync();
|
|
1036
|
+
} finally {
|
|
1037
|
+
await handle.close();
|
|
1038
|
+
}
|
|
1039
|
+
await replaceCodexGuardianReceiptFile(temporary, guardian.receiptPath);
|
|
1040
|
+
if (process.platform !== "win32") {
|
|
1041
|
+
const directory = await open2(dirname2(guardian.receiptPath), "r");
|
|
1042
|
+
try {
|
|
1043
|
+
await directory.sync();
|
|
1044
|
+
} finally {
|
|
1045
|
+
await directory.close();
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
var parseGuardianReceipt = (raw) => {
|
|
1050
|
+
const value = JSON.parse(raw);
|
|
1051
|
+
const rawTermination = value.termination;
|
|
1052
|
+
const termination = rawTermination === null || rawTermination === void 0 ? null : objectOrNull(rawTermination);
|
|
1053
|
+
if (value.schema_version !== "vtx_codex_guardian_v1" || typeof value.process_token !== "string" || !/^[0-9a-f]{64}$/u.test(value.process_token) || !["spawn_intent", "starting", "running", "terminated"].includes(String(value.state)) || value.guardian_pid !== null && !Number.isSafeInteger(value.guardian_pid) || value.state === "spawn_intent" && value.guardian_pid !== null || value.state !== "spawn_intent" && !Number.isSafeInteger(value.guardian_pid) || value.child_pid !== null && !Number.isSafeInteger(value.child_pid) || value.boot_identity !== null && value.boot_identity !== void 0 && typeof value.boot_identity !== "string" || typeof value.updated_at !== "string" || !Number.isFinite(Date.parse(value.updated_at)) || rawTermination !== null && rawTermination !== void 0 && termination === null || termination !== null && (value.state !== "terminated" || termination.exit_code !== null && (!Number.isInteger(termination.exit_code) || Number(termination.exit_code) < -(2 ** 31) || Number(termination.exit_code) > 2 ** 31 - 1) || termination.signal !== null && (typeof termination.signal !== "string" || !/^sig[a-z0-9]{1,28}$/u.test(termination.signal)) || termination.signal !== null && termination.exit_code !== null || typeof termination.expected !== "boolean" || !Number.isSafeInteger(termination.uptime_ms) || Number(termination.uptime_ms) < 0)) {
|
|
1054
|
+
throw new Error("Codex guardian receipt is invalid.");
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
...value,
|
|
1058
|
+
boot_identity: typeof value.boot_identity === "string" ? value.boot_identity : null,
|
|
1059
|
+
termination
|
|
1060
|
+
};
|
|
1061
|
+
};
|
|
1062
|
+
var readCodexGuardianReceipt = async (path) => {
|
|
1063
|
+
try {
|
|
1064
|
+
const state = await lstat2(path);
|
|
1065
|
+
if (!state.isFile() || state.isSymbolicLink() || process.platform !== "win32" && ((state.mode & 63) !== 0 || typeof process.getuid === "function" && state.uid !== process.getuid())) {
|
|
1066
|
+
throw new Error("Codex guardian receipt is not private.");
|
|
1067
|
+
}
|
|
1068
|
+
return parseGuardianReceipt(await readFile2(path, "utf8"));
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
if (error.code === "ENOENT") return null;
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
var waitForCodexGuardianState = async (guardian, expectedState, deadlineAtMs) => {
|
|
1075
|
+
while (Date.now() < deadlineAtMs) {
|
|
1076
|
+
const receipt = await readCodexGuardianReceipt(guardian.receiptPath);
|
|
1077
|
+
if (receipt) {
|
|
1078
|
+
if (receipt.process_token !== guardian.processToken) {
|
|
1079
|
+
throw new Error("Codex guardian receipt crossed process scope.");
|
|
1080
|
+
}
|
|
1081
|
+
if (receipt.state === expectedState) return receipt;
|
|
1082
|
+
if (receipt.state === "terminated" && expectedState === "running") {
|
|
1083
|
+
throw new Error("Codex guardian terminated before app-server initialization.");
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
await new Promise((resolve3) => setTimeout(resolve3, 25));
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error(`Codex guardian ${expectedState} state was not confirmed.`);
|
|
1089
|
+
};
|
|
1090
|
+
var defaultSpawn = (binary, args, options) => {
|
|
1091
|
+
if (!options.guardian) {
|
|
1092
|
+
const { guardian: _guardian, ...spawnOptions } = options;
|
|
1093
|
+
return spawn2(binary, args, {
|
|
1094
|
+
...spawnOptions,
|
|
1095
|
+
detached: options.detached
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
const payload = Buffer.from(JSON.stringify({
|
|
1099
|
+
binary,
|
|
1100
|
+
args,
|
|
1101
|
+
processToken: options.guardian.processToken,
|
|
1102
|
+
receiptPath: options.guardian.receiptPath,
|
|
1103
|
+
shutdownIntentPath: `${options.guardian.receiptPath}.shutdown-intent`,
|
|
1104
|
+
shutdownGraceMs: Math.max(25, options.guardian.shutdownGraceMs ?? 2e3),
|
|
1105
|
+
bootIdentity: options.guardian.bootIdentity
|
|
1106
|
+
}), "utf8").toString("base64url");
|
|
1107
|
+
return spawn2(process.execPath, ["--input-type=module", "--eval", GUARDIAN_SCRIPT, payload], {
|
|
1108
|
+
env: options.env,
|
|
1109
|
+
stdio: options.stdio,
|
|
1110
|
+
windowsHide: options.windowsHide,
|
|
1111
|
+
detached: true
|
|
1112
|
+
});
|
|
1113
|
+
};
|
|
1114
|
+
var CodexAppServerSession = class _CodexAppServerSession {
|
|
1115
|
+
constructor(options) {
|
|
1116
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
1117
|
+
this.retiredResponseIds = /* @__PURE__ */ new Map();
|
|
1118
|
+
this.notificationListeners = /* @__PURE__ */ new Set();
|
|
1119
|
+
this.fatalListeners = /* @__PURE__ */ new Set();
|
|
1120
|
+
this.agentToolHandlers = /* @__PURE__ */ new Map();
|
|
1121
|
+
this.accountLoginCompletions = /* @__PURE__ */ new Map();
|
|
1122
|
+
this.threads = /* @__PURE__ */ new Map();
|
|
1123
|
+
this.nextRequestId = 1;
|
|
1124
|
+
this.fatalError = null;
|
|
1125
|
+
this.closed = false;
|
|
1126
|
+
this.processCloseObserved = false;
|
|
1127
|
+
this.shutdownRequested = false;
|
|
1128
|
+
this.stderrBytesObserved = 0;
|
|
1129
|
+
this.stderrNonempty = false;
|
|
1130
|
+
this.stderrTruncated = false;
|
|
1131
|
+
this.stderrTail = "";
|
|
1132
|
+
this.stderrClass = "none";
|
|
1133
|
+
this.process = options.process;
|
|
1134
|
+
this.codexHome = options.codexHome;
|
|
1135
|
+
this.sessionGeneration = options.sessionGeneration;
|
|
1136
|
+
this.startedAtMs = Date.now();
|
|
1137
|
+
this.guardian = options.guardian;
|
|
1138
|
+
this.onProcessExit = options.onProcessExit;
|
|
1139
|
+
this.onLateResponse = options.onLateResponse;
|
|
1140
|
+
this.process.stderr.on("data", (chunk) => this.observeStderr(chunk));
|
|
1141
|
+
const lines = createInterface({ input: this.process.stdout, crlfDelay: Infinity });
|
|
1142
|
+
lines.on("line", (line) => this.handleLine(line));
|
|
1143
|
+
this.process.once("error", () => {
|
|
1144
|
+
});
|
|
1145
|
+
this.process.once("close", (code, signal) => {
|
|
1146
|
+
void this.handleProcessClose(code, signal);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
static {
|
|
1150
|
+
this.STDERR_OBSERVATION_CAP_BYTES = 65536;
|
|
1151
|
+
}
|
|
1152
|
+
static {
|
|
1153
|
+
this.STDERR_CLASSIFICATION_TAIL_CHARS = 1024;
|
|
1154
|
+
}
|
|
1155
|
+
static async start(options) {
|
|
1156
|
+
const spawnProcess = options.spawnProcess ?? defaultSpawn;
|
|
1157
|
+
const guardian = options.guardian ? {
|
|
1158
|
+
...options.guardian,
|
|
1159
|
+
bootIdentity: options.guardian.bootIdentity ?? await readInferenceSystemBootIdentity()
|
|
1160
|
+
} : void 0;
|
|
1161
|
+
if (guardian) {
|
|
1162
|
+
try {
|
|
1163
|
+
await writeCodexGuardianSpawnIntent(guardian);
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
throw new CodexAppServerError({
|
|
1166
|
+
message: "Codex guardian spawn intent could not be persisted.",
|
|
1167
|
+
category: "transport",
|
|
1168
|
+
code: "codex_guardian_intent_unconfirmed",
|
|
1169
|
+
retryable: false,
|
|
1170
|
+
cause: error
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const child = spawnProcess(options.binary.path, appServerArgs(
|
|
1175
|
+
options.enableAgentCodeModeHost === true
|
|
1176
|
+
), {
|
|
1177
|
+
env: scrubbedCodexEnvironment(options.codexHome),
|
|
1178
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1179
|
+
windowsHide: true,
|
|
1180
|
+
detached: true,
|
|
1181
|
+
guardian
|
|
1182
|
+
});
|
|
1183
|
+
if (guardian) {
|
|
1184
|
+
try {
|
|
1185
|
+
await waitForCodexGuardianState(
|
|
1186
|
+
guardian,
|
|
1187
|
+
"running",
|
|
1188
|
+
Math.min(options.deadlineAtMs, Date.now() + 1e4)
|
|
1189
|
+
);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
child.stdin.end();
|
|
1192
|
+
try {
|
|
1193
|
+
await waitForCodexGuardianState(
|
|
1194
|
+
guardian,
|
|
1195
|
+
"terminated",
|
|
1196
|
+
Date.now() + 5e3
|
|
1197
|
+
);
|
|
1198
|
+
} catch (terminationError) {
|
|
1199
|
+
throw new CodexAppServerError({
|
|
1200
|
+
message: "Codex guardian startup failed and termination was not confirmed.",
|
|
1201
|
+
category: "transport",
|
|
1202
|
+
code: "codex_guardian_termination_unconfirmed",
|
|
1203
|
+
retryable: false,
|
|
1204
|
+
cause: terminationError
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
throw new CodexAppServerError({
|
|
1208
|
+
message: "Codex guardian startup could not be confirmed.",
|
|
1209
|
+
category: "transport",
|
|
1210
|
+
code: "codex_guardian_start_unconfirmed",
|
|
1211
|
+
retryable: false,
|
|
1212
|
+
cause: error
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const session = new _CodexAppServerSession({
|
|
1217
|
+
process: child,
|
|
1218
|
+
codexHome: options.codexHome,
|
|
1219
|
+
sessionGeneration: options.sessionGeneration ?? 1,
|
|
1220
|
+
guardian: guardian ?? null,
|
|
1221
|
+
onProcessExit: options.onProcessExit ?? null,
|
|
1222
|
+
onLateResponse: options.onLateResponse ?? null
|
|
1223
|
+
});
|
|
1224
|
+
try {
|
|
1225
|
+
const initialized = await session.request("initialize", {
|
|
1226
|
+
clientInfo: {
|
|
1227
|
+
name: "vtx_external_inference_host",
|
|
1228
|
+
title: "VTX External Inference Host",
|
|
1229
|
+
version: "external_inference.v1"
|
|
1230
|
+
},
|
|
1231
|
+
capabilities: {
|
|
1232
|
+
experimentalApi: true,
|
|
1233
|
+
optOutNotificationMethods: ["thread/started"]
|
|
1234
|
+
}
|
|
1235
|
+
}, {
|
|
1236
|
+
timeoutMs: Math.max(1, options.deadlineAtMs - Date.now()),
|
|
1237
|
+
signal: options.signal
|
|
1238
|
+
});
|
|
1239
|
+
if (String(initialized.codexHome || "") !== options.codexHome || !String(initialized.userAgent || "").trim()) {
|
|
1240
|
+
throw new CodexAppServerError({
|
|
1241
|
+
message: "Codex initialization provenance was invalid.",
|
|
1242
|
+
category: "adapter",
|
|
1243
|
+
code: "invalid_initialize_provenance",
|
|
1244
|
+
retryable: false
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
session.notify("initialized", {});
|
|
1248
|
+
return session;
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
await session.close().catch(() => void 0);
|
|
1251
|
+
if (guardian) {
|
|
1252
|
+
try {
|
|
1253
|
+
await waitForCodexGuardianState(
|
|
1254
|
+
guardian,
|
|
1255
|
+
"terminated",
|
|
1256
|
+
Date.now() + 5e3
|
|
1257
|
+
);
|
|
1258
|
+
} catch (terminationError) {
|
|
1259
|
+
throw new CodexAppServerError({
|
|
1260
|
+
message: "Codex app-server failed and guardian termination was not confirmed.",
|
|
1261
|
+
category: "transport",
|
|
1262
|
+
code: "codex_guardian_termination_unconfirmed",
|
|
1263
|
+
retryable: false,
|
|
1264
|
+
cause: terminationError
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
throw error;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
handleLine(line) {
|
|
1272
|
+
let message;
|
|
1273
|
+
try {
|
|
1274
|
+
const parsed = JSON.parse(line);
|
|
1275
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1276
|
+
throw new Error("protocol message is not an object");
|
|
1277
|
+
}
|
|
1278
|
+
message = parsed;
|
|
1279
|
+
} catch {
|
|
1280
|
+
this.failTransport("invalid_protocol_json");
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const id = message.id;
|
|
1284
|
+
if (typeof id === "number" && Number.isSafeInteger(id) && !("method" in message)) {
|
|
1285
|
+
const pending = this.pending.get(id);
|
|
1286
|
+
if (!pending) {
|
|
1287
|
+
const retiredMethod = this.retiredResponseIds.get(id);
|
|
1288
|
+
this.retiredResponseIds.delete(id);
|
|
1289
|
+
if (retiredMethod && LATE_RESPONSE_SAFE_METHODS.has(retiredMethod)) {
|
|
1290
|
+
try {
|
|
1291
|
+
this.onLateResponse?.(retiredMethod);
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
this.failTransport("unexpected_response_id");
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
this.pending.delete(id);
|
|
1300
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1301
|
+
pending.abortCleanup?.();
|
|
1302
|
+
const error = objectOrNull(message.error);
|
|
1303
|
+
if (error) {
|
|
1304
|
+
pending.reject(new CodexAppServerError({
|
|
1305
|
+
message: "Codex app-server rejected a request.",
|
|
1306
|
+
category: "adapter",
|
|
1307
|
+
code: "rpc_rejected",
|
|
1308
|
+
retryable: false
|
|
1309
|
+
}));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const result = objectOrNull(message.result);
|
|
1313
|
+
if (!result) {
|
|
1314
|
+
pending.reject(new CodexAppServerError({
|
|
1315
|
+
message: "Codex app-server returned an invalid result.",
|
|
1316
|
+
category: "adapter",
|
|
1317
|
+
code: "invalid_rpc_result",
|
|
1318
|
+
retryable: false
|
|
1319
|
+
}));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
pending.resolve(result);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const method = typeof message.method === "string" ? message.method : "";
|
|
1326
|
+
const params = objectOrNull(message.params);
|
|
1327
|
+
if (method === "item/tool/call" && params && "id" in message) {
|
|
1328
|
+
const requestId = message.id;
|
|
1329
|
+
if (!(typeof requestId === "string" || typeof requestId === "number" && Number.isSafeInteger(requestId))) {
|
|
1330
|
+
this.failTransport("invalid_dynamic_tool_request_id");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
void this.handleAgentToolRequest(requestId, params);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (!method || !params || "id" in message) {
|
|
1337
|
+
this.failTransport("unexpected_server_request");
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (method === "account/login/completed") {
|
|
1341
|
+
const loginId = typeof params.loginId === "string" ? params.loginId.trim() : "";
|
|
1342
|
+
if (!loginId || typeof params.success !== "boolean") {
|
|
1343
|
+
this.failTransport("invalid_account_login_completion");
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
this.accountLoginCompletions.set(loginId, {
|
|
1347
|
+
success: params.success,
|
|
1348
|
+
error: typeof params.error === "string" ? params.error : null
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
for (const listener of this.notificationListeners) listener({ method, params });
|
|
1352
|
+
}
|
|
1353
|
+
async handleAgentToolRequest(requestId, params) {
|
|
1354
|
+
const threadId = typeof params.threadId === "string" ? params.threadId.trim() : "";
|
|
1355
|
+
const requestTurnId = typeof params.turnId === "string" ? params.turnId.trim() : "";
|
|
1356
|
+
const callId = typeof params.callId === "string" ? params.callId.trim() : "";
|
|
1357
|
+
const tool = typeof params.tool === "string" ? params.tool : "";
|
|
1358
|
+
const args = objectOrNull(params.arguments);
|
|
1359
|
+
const handler = this.agentToolHandlers.get(threadId);
|
|
1360
|
+
if (!threadId || !requestTurnId || !callId || params.namespace !== null || !CODEX_AGENT_DYNAMIC_TOOLS.includes(tool) || !args || !handler || !handler.acceptTurnId(requestTurnId)) {
|
|
1361
|
+
this.failTransport("invalid_dynamic_tool_request");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
const signature = JSON.stringify({ tool, arguments: args });
|
|
1366
|
+
const existing = handler.calls.get(callId);
|
|
1367
|
+
if (existing && existing.signature !== signature) {
|
|
1368
|
+
this.failTransport("dynamic_tool_call_identity_reused");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const promise = existing?.promise ?? handler.execute({
|
|
1372
|
+
callId,
|
|
1373
|
+
tool,
|
|
1374
|
+
arguments: args
|
|
1375
|
+
});
|
|
1376
|
+
if (!existing) handler.calls.set(callId, { signature, promise });
|
|
1377
|
+
const result = await promise;
|
|
1378
|
+
this.write({
|
|
1379
|
+
id: requestId,
|
|
1380
|
+
result: {
|
|
1381
|
+
contentItems: [{ type: "inputText", text: JSON.stringify(result.value ?? null) }],
|
|
1382
|
+
success: result.success
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
} catch {
|
|
1386
|
+
this.write({
|
|
1387
|
+
id: requestId,
|
|
1388
|
+
result: {
|
|
1389
|
+
contentItems: [{
|
|
1390
|
+
type: "inputText",
|
|
1391
|
+
text: JSON.stringify({ error: "vtx_agent_tool_failed" })
|
|
1392
|
+
}],
|
|
1393
|
+
success: false
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
failTransport(code, processExit = null) {
|
|
1399
|
+
if (this.fatalError) return;
|
|
1400
|
+
this.fatalError = new CodexAppServerError({
|
|
1401
|
+
message: "Codex app-server transport became unavailable.",
|
|
1402
|
+
category: "transport",
|
|
1403
|
+
code,
|
|
1404
|
+
retryable: true,
|
|
1405
|
+
processExit
|
|
1406
|
+
});
|
|
1407
|
+
for (const pending of this.pending.values()) {
|
|
1408
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1409
|
+
pending.abortCleanup?.();
|
|
1410
|
+
pending.reject(this.fatalError);
|
|
1411
|
+
}
|
|
1412
|
+
this.pending.clear();
|
|
1413
|
+
for (const listener of this.fatalListeners) listener(this.fatalError);
|
|
1414
|
+
}
|
|
1415
|
+
retireResponseId(id, method) {
|
|
1416
|
+
this.retiredResponseIds.set(id, method);
|
|
1417
|
+
while (this.retiredResponseIds.size > MAX_RETIRED_RESPONSE_IDS) {
|
|
1418
|
+
const oldest = this.retiredResponseIds.keys().next().value;
|
|
1419
|
+
if (typeof oldest !== "number") break;
|
|
1420
|
+
this.retiredResponseIds.delete(oldest);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
write(message) {
|
|
1424
|
+
if (this.closed || this.fatalError || this.process.exitCode !== null) {
|
|
1425
|
+
throw this.fatalError ?? new CodexAppServerError({
|
|
1426
|
+
message: "Codex app-server is closed.",
|
|
1427
|
+
category: "transport",
|
|
1428
|
+
code: "transport_closed",
|
|
1429
|
+
retryable: true
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
this.process.stdin.write(`${JSON.stringify(message)}
|
|
1433
|
+
`);
|
|
1434
|
+
}
|
|
1435
|
+
notify(method, params) {
|
|
1436
|
+
this.write({ method, params });
|
|
1437
|
+
}
|
|
1438
|
+
async request(method, params, options = { timeoutMs: 3e4 }) {
|
|
1439
|
+
if (options.signal?.aborted) {
|
|
1440
|
+
throw new CodexAppServerError({
|
|
1441
|
+
message: "Codex request was cancelled before dispatch.",
|
|
1442
|
+
category: "cancelled",
|
|
1443
|
+
code: "rpc_cancelled_before_write",
|
|
1444
|
+
retryable: false,
|
|
1445
|
+
dispatchOutcome: "not_dispatched"
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
const id = this.nextRequestId++;
|
|
1449
|
+
await options.beforeWrite?.(id);
|
|
1450
|
+
if (options.signal?.aborted) {
|
|
1451
|
+
throw new CodexAppServerError({
|
|
1452
|
+
message: "Codex request was cancelled before dispatch.",
|
|
1453
|
+
category: "cancelled",
|
|
1454
|
+
code: "rpc_cancelled_before_write",
|
|
1455
|
+
retryable: false,
|
|
1456
|
+
dispatchOutcome: "not_dispatched"
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
if (options.notAfterMs !== void 0 && Date.now() > options.notAfterMs) {
|
|
1460
|
+
throw new CodexAppServerError({
|
|
1461
|
+
message: "Server account context expired before provider dispatch.",
|
|
1462
|
+
category: "adapter",
|
|
1463
|
+
code: "server_account_context_expired_before_provider_dispatch",
|
|
1464
|
+
retryable: false,
|
|
1465
|
+
dispatchOutcome: "not_dispatched"
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
return await new Promise((resolve3, reject) => {
|
|
1469
|
+
const pending = { resolve: resolve3, reject, timer: null, abortCleanup: null };
|
|
1470
|
+
pending.timer = setTimeout(() => {
|
|
1471
|
+
this.pending.delete(id);
|
|
1472
|
+
this.retireResponseId(id, method);
|
|
1473
|
+
pending.abortCleanup?.();
|
|
1474
|
+
reject(new CodexAppServerError({
|
|
1475
|
+
message: "Codex app-server request timed out.",
|
|
1476
|
+
category: "timeout",
|
|
1477
|
+
code: "rpc_timeout",
|
|
1478
|
+
retryable: true
|
|
1479
|
+
}));
|
|
1480
|
+
}, Math.max(1, options.timeoutMs));
|
|
1481
|
+
if (options.signal && !options.retainResponseOnAbort) {
|
|
1482
|
+
const onAbort = () => {
|
|
1483
|
+
this.pending.delete(id);
|
|
1484
|
+
this.retireResponseId(id, method);
|
|
1485
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1486
|
+
pending.abortCleanup?.();
|
|
1487
|
+
reject(new CodexAppServerError({
|
|
1488
|
+
message: "Codex request was cancelled.",
|
|
1489
|
+
category: "cancelled",
|
|
1490
|
+
code: "cancelled",
|
|
1491
|
+
retryable: false
|
|
1492
|
+
}));
|
|
1493
|
+
};
|
|
1494
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1495
|
+
pending.abortCleanup = () => options.signal?.removeEventListener("abort", onAbort);
|
|
1496
|
+
}
|
|
1497
|
+
this.pending.set(id, pending);
|
|
1498
|
+
try {
|
|
1499
|
+
this.write(params === void 0 ? { id, method } : { id, method, params });
|
|
1500
|
+
options.onWritten?.(id);
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
this.pending.delete(id);
|
|
1503
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1504
|
+
pending.abortCleanup?.();
|
|
1505
|
+
reject(error);
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
subscribe(listener) {
|
|
1510
|
+
this.notificationListeners.add(listener);
|
|
1511
|
+
return () => this.notificationListeners.delete(listener);
|
|
1512
|
+
}
|
|
1513
|
+
subscribeFatal(listener) {
|
|
1514
|
+
this.fatalListeners.add(listener);
|
|
1515
|
+
return () => this.fatalListeners.delete(listener);
|
|
1516
|
+
}
|
|
1517
|
+
observeStderr(chunk) {
|
|
1518
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8");
|
|
1519
|
+
if (bytes.length === 0) return;
|
|
1520
|
+
this.stderrNonempty = true;
|
|
1521
|
+
const remaining = Math.max(
|
|
1522
|
+
0,
|
|
1523
|
+
_CodexAppServerSession.STDERR_OBSERVATION_CAP_BYTES - this.stderrBytesObserved
|
|
1524
|
+
);
|
|
1525
|
+
if (remaining === 0) {
|
|
1526
|
+
this.stderrTruncated = true;
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const observed = bytes.subarray(0, remaining);
|
|
1530
|
+
this.stderrBytesObserved += observed.length;
|
|
1531
|
+
if (bytes.length > remaining) this.stderrTruncated = true;
|
|
1532
|
+
this.stderrTail = `${this.stderrTail}${observed.toString("utf8")}`.slice(-_CodexAppServerSession.STDERR_CLASSIFICATION_TAIL_CHARS).toLowerCase();
|
|
1533
|
+
const classified = /heap out of memory|allocation failed|out of memory/u.test(this.stderrTail) ? "out_of_memory" : /fatal runtime error|fatal error|\bpanic(?:ked)?\b/u.test(this.stderrTail) ? "runtime_fatal" : /access violation|segmentation fault|\bsegfault\b/u.test(this.stderrTail) ? "memory_fault" : /broken pipe|econnreset|connection reset/u.test(this.stderrTail) ? "pipe_or_connection" : null;
|
|
1534
|
+
if (classified) this.stderrClass = classified;
|
|
1535
|
+
else if (this.stderrClass === "none") this.stderrClass = "unclassified";
|
|
1536
|
+
}
|
|
1537
|
+
async handleProcessClose(rawCode, rawSignal) {
|
|
1538
|
+
this.processCloseObserved = true;
|
|
1539
|
+
let termination = null;
|
|
1540
|
+
if (this.guardian) {
|
|
1541
|
+
try {
|
|
1542
|
+
const receipt = await readCodexGuardianReceipt(this.guardian.receiptPath);
|
|
1543
|
+
if (receipt?.process_token === this.guardian.processToken && receipt.state === "terminated") {
|
|
1544
|
+
termination = receipt.termination;
|
|
1545
|
+
}
|
|
1546
|
+
} catch {
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const diagnostics = this.processExitDiagnostics(rawCode, rawSignal, termination);
|
|
1550
|
+
if (this.guardian) {
|
|
1551
|
+
await rm2(`${this.guardian.receiptPath}.shutdown-intent`, { force: true }).catch(() => void 0);
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
this.onProcessExit?.(diagnostics);
|
|
1555
|
+
} catch {
|
|
1556
|
+
}
|
|
1557
|
+
this.failTransport("codex_process_closed", diagnostics);
|
|
1558
|
+
}
|
|
1559
|
+
processExitDiagnostics(rawCode, rawSignal, termination) {
|
|
1560
|
+
const source = termination ? "child_receipt" : this.guardian ? "guardian_process" : "direct_process";
|
|
1561
|
+
const reportedCode = termination?.exit_code ?? rawCode;
|
|
1562
|
+
const reportedSignal = termination?.signal ?? rawSignal;
|
|
1563
|
+
const signal = typeof reportedSignal === "string" && /^(?:SIG[A-Z0-9]{1,28}|sig[a-z0-9]{1,28})$/u.test(reportedSignal) ? reportedSignal.toLowerCase() : null;
|
|
1564
|
+
const exitCode = signal === null && typeof reportedCode === "number" && Number.isInteger(reportedCode) && reportedCode >= -(2 ** 31) && reportedCode <= 2 ** 31 - 1 ? reportedCode : null;
|
|
1565
|
+
const diagnostics = {
|
|
1566
|
+
schema_version: "external_inference_process_exit_v1",
|
|
1567
|
+
runtime: "codex_app_server",
|
|
1568
|
+
source,
|
|
1569
|
+
session_generation: this.sessionGeneration,
|
|
1570
|
+
expected_shutdown: termination?.expected ?? this.shutdownRequested,
|
|
1571
|
+
exit_class: signal !== null ? "signal" : exitCode === null ? "unknown" : exitCode === 0 ? "zero" : "nonzero",
|
|
1572
|
+
exit_code: exitCode,
|
|
1573
|
+
signal,
|
|
1574
|
+
uptime_ms: termination?.uptime_ms ?? Math.min(
|
|
1575
|
+
Math.max(0, Date.now() - this.startedAtMs),
|
|
1576
|
+
Number.MAX_SAFE_INTEGER
|
|
1577
|
+
),
|
|
1578
|
+
pending_request_count: this.pending.size,
|
|
1579
|
+
active_thread_count: this.threads.size,
|
|
1580
|
+
stderr_nonempty: this.stderrNonempty,
|
|
1581
|
+
stderr_bytes_observed: this.stderrBytesObserved,
|
|
1582
|
+
stderr_truncated: this.stderrTruncated,
|
|
1583
|
+
stderr_class: this.stderrNonempty ? this.stderrClass : "none"
|
|
1584
|
+
};
|
|
1585
|
+
this.stderrTail = "";
|
|
1586
|
+
return diagnostics;
|
|
1587
|
+
}
|
|
1588
|
+
isUsable() {
|
|
1589
|
+
return !this.closed && this.fatalError === null && this.process.exitCode === null;
|
|
1590
|
+
}
|
|
1591
|
+
async readAccountSnapshot(deadlineAtMs, signal, refreshToken = false) {
|
|
1592
|
+
const result = await this.request("account/read", { refreshToken }, {
|
|
1593
|
+
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
|
|
1594
|
+
signal
|
|
1595
|
+
});
|
|
1596
|
+
const account = objectOrNull(result.account);
|
|
1597
|
+
if (typeof result.requiresOpenaiAuth !== "boolean") {
|
|
1598
|
+
throw new CodexAppServerError({
|
|
1599
|
+
message: "Codex returned invalid account metadata.",
|
|
1600
|
+
category: "auth",
|
|
1601
|
+
code: "invalid_account_metadata",
|
|
1602
|
+
retryable: false
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
if (account?.type === "chatgpt") {
|
|
1606
|
+
const email = account.email === null ? null : typeof account.email === "string" && account.email.trim() ? account.email.trim() : void 0;
|
|
1607
|
+
if (email === void 0 || !validPlanType(account.planType)) {
|
|
1608
|
+
throw new CodexAppServerError({
|
|
1609
|
+
message: "Codex returned invalid managed ChatGPT account metadata.",
|
|
1610
|
+
category: "auth",
|
|
1611
|
+
code: "invalid_account_metadata",
|
|
1612
|
+
retryable: false
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
requiresOpenaiAuth: result.requiresOpenaiAuth,
|
|
1617
|
+
authenticatedChatGptAccount: { email, planType: account.planType }
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
if (account !== null && account?.type !== "apiKey" && account?.type !== "amazonBedrock") {
|
|
1621
|
+
throw new CodexAppServerError({
|
|
1622
|
+
message: "Codex returned an unsupported account type.",
|
|
1623
|
+
category: "auth",
|
|
1624
|
+
code: "invalid_account_metadata",
|
|
1625
|
+
retryable: false
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
requiresOpenaiAuth: result.requiresOpenaiAuth,
|
|
1630
|
+
authenticatedChatGptAccount: null
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
async hasManagedChatGptAuth(deadlineAtMs, signal, refreshToken = false) {
|
|
1634
|
+
const snapshot = await this.readAccountSnapshot(deadlineAtMs, signal, refreshToken);
|
|
1635
|
+
return snapshot.authenticatedChatGptAccount !== null && snapshot.requiresOpenaiAuth;
|
|
1636
|
+
}
|
|
1637
|
+
async readAccountRateLimits(deadlineAtMs, signal) {
|
|
1638
|
+
const result = await this.request("account/rateLimits/read", void 0, {
|
|
1639
|
+
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
|
|
1640
|
+
signal
|
|
1641
|
+
});
|
|
1642
|
+
try {
|
|
1643
|
+
const rateLimits = parseRateLimitSnapshot(result.rateLimits);
|
|
1644
|
+
const rawByLimitId = result.rateLimitsByLimitId;
|
|
1645
|
+
let rateLimitsByLimitId = null;
|
|
1646
|
+
if (rawByLimitId !== null && rawByLimitId !== void 0) {
|
|
1647
|
+
const byLimitId = objectOrNull(rawByLimitId);
|
|
1648
|
+
if (!byLimitId) throw new Error("invalid rate-limit bucket map");
|
|
1649
|
+
rateLimitsByLimitId = {};
|
|
1650
|
+
for (const [limitId, snapshot] of Object.entries(byLimitId)) {
|
|
1651
|
+
if (!limitId.trim()) throw new Error("invalid rate-limit bucket identity");
|
|
1652
|
+
rateLimitsByLimitId[limitId] = parseRateLimitSnapshot(snapshot);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return { rateLimits, rateLimitsByLimitId };
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
throw new CodexAppServerError({
|
|
1658
|
+
message: "Codex returned invalid account rate-limit metadata.",
|
|
1659
|
+
category: "adapter",
|
|
1660
|
+
code: "invalid_rate_limit_metadata",
|
|
1661
|
+
retryable: false,
|
|
1662
|
+
cause: error
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
async requireManagedChatGptAuth(deadlineAtMs, signal, refreshToken = false) {
|
|
1667
|
+
if (!await this.hasManagedChatGptAuth(deadlineAtMs, signal, refreshToken)) {
|
|
1668
|
+
throw new CodexAppServerError({
|
|
1669
|
+
message: "Codex managed ChatGPT authentication is required.",
|
|
1670
|
+
category: "auth",
|
|
1671
|
+
code: "managed_chatgpt_auth_required",
|
|
1672
|
+
retryable: false
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async startChatGptDeviceCodeLogin(deadlineAtMs, signal) {
|
|
1677
|
+
const result = await this.request("account/login/start", { type: "chatgptDeviceCode" }, {
|
|
1678
|
+
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
|
|
1679
|
+
signal
|
|
1680
|
+
});
|
|
1681
|
+
const loginId = typeof result.loginId === "string" ? result.loginId.trim() : "";
|
|
1682
|
+
const verificationUrl = typeof result.verificationUrl === "string" ? result.verificationUrl.trim() : "";
|
|
1683
|
+
const userCode = typeof result.userCode === "string" ? result.userCode.trim() : "";
|
|
1684
|
+
if (result.type !== "chatgptDeviceCode" || !loginId || !verificationUrl.startsWith("https://") || !userCode) {
|
|
1685
|
+
throw new CodexAppServerError({
|
|
1686
|
+
message: "Codex returned an invalid device-code login challenge.",
|
|
1687
|
+
category: "auth",
|
|
1688
|
+
code: "invalid_device_code_challenge",
|
|
1689
|
+
retryable: false
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
return { loginId, verificationUrl, userCode };
|
|
1693
|
+
}
|
|
1694
|
+
async waitForChatGptLogin(loginId, deadlineAtMs, signal) {
|
|
1695
|
+
const settle = (completion, resolveCompletion, rejectCompletion) => {
|
|
1696
|
+
this.accountLoginCompletions.delete(loginId);
|
|
1697
|
+
if (completion.success) {
|
|
1698
|
+
resolveCompletion();
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
rejectCompletion(new CodexAppServerError({
|
|
1702
|
+
message: "Codex ChatGPT device-code login failed.",
|
|
1703
|
+
category: "auth",
|
|
1704
|
+
code: "chatgpt_login_failed",
|
|
1705
|
+
retryable: false
|
|
1706
|
+
}));
|
|
1707
|
+
};
|
|
1708
|
+
const existing = this.accountLoginCompletions.get(loginId);
|
|
1709
|
+
if (existing) {
|
|
1710
|
+
await new Promise((resolveCompletion, rejectCompletion) => {
|
|
1711
|
+
settle(existing, resolveCompletion, rejectCompletion);
|
|
1712
|
+
});
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
await new Promise((resolveCompletion, rejectCompletion) => {
|
|
1716
|
+
let timer = null;
|
|
1717
|
+
let abortCleanup = null;
|
|
1718
|
+
let unsubscribe = null;
|
|
1719
|
+
let unsubscribeFatal = null;
|
|
1720
|
+
const cleanup = () => {
|
|
1721
|
+
if (timer) clearTimeout(timer);
|
|
1722
|
+
abortCleanup?.();
|
|
1723
|
+
unsubscribe?.();
|
|
1724
|
+
unsubscribeFatal?.();
|
|
1725
|
+
};
|
|
1726
|
+
const fail = (error) => {
|
|
1727
|
+
cleanup();
|
|
1728
|
+
rejectCompletion(error);
|
|
1729
|
+
};
|
|
1730
|
+
unsubscribe = this.subscribe(({ method, params }) => {
|
|
1731
|
+
if (method !== "account/login/completed" || params.loginId !== loginId) return;
|
|
1732
|
+
const completion = this.accountLoginCompletions.get(loginId);
|
|
1733
|
+
if (!completion) {
|
|
1734
|
+
fail(new CodexAppServerError({
|
|
1735
|
+
message: "Codex returned an invalid device-code login completion.",
|
|
1736
|
+
category: "auth",
|
|
1737
|
+
code: "invalid_device_code_completion",
|
|
1738
|
+
retryable: false
|
|
1739
|
+
}));
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
cleanup();
|
|
1743
|
+
settle(completion, resolveCompletion, rejectCompletion);
|
|
1744
|
+
});
|
|
1745
|
+
unsubscribeFatal = this.subscribeFatal(fail);
|
|
1746
|
+
timer = setTimeout(() => fail(new CodexAppServerError({
|
|
1747
|
+
message: "Codex ChatGPT device-code login timed out.",
|
|
1748
|
+
category: "timeout",
|
|
1749
|
+
code: "chatgpt_login_timeout",
|
|
1750
|
+
retryable: false
|
|
1751
|
+
})), Math.max(1, deadlineAtMs - Date.now()));
|
|
1752
|
+
if (signal) {
|
|
1753
|
+
const onAbort = () => fail(new CodexAppServerError({
|
|
1754
|
+
message: "Codex ChatGPT device-code login was cancelled.",
|
|
1755
|
+
category: "cancelled",
|
|
1756
|
+
code: "cancelled",
|
|
1757
|
+
retryable: false
|
|
1758
|
+
}));
|
|
1759
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1760
|
+
abortCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1761
|
+
if (signal.aborted) onAbort();
|
|
1762
|
+
}
|
|
1763
|
+
const racedCompletion = this.accountLoginCompletions.get(loginId);
|
|
1764
|
+
if (racedCompletion) {
|
|
1765
|
+
cleanup();
|
|
1766
|
+
settle(racedCompletion, resolveCompletion, rejectCompletion);
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
async cancelChatGptLogin(loginId, deadlineAtMs, signal) {
|
|
1771
|
+
const result = await this.request("account/login/cancel", { loginId }, {
|
|
1772
|
+
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
|
|
1773
|
+
signal
|
|
1774
|
+
});
|
|
1775
|
+
if (result.status !== "canceled" && result.status !== "notFound") {
|
|
1776
|
+
throw new CodexAppServerError({
|
|
1777
|
+
message: "Codex device-code login cancellation was not confirmed.",
|
|
1778
|
+
category: "auth",
|
|
1779
|
+
code: "device_code_cancel_unconfirmed",
|
|
1780
|
+
retryable: false
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
async logoutManagedChatGptAuth(deadlineAtMs, signal) {
|
|
1785
|
+
const result = await this.request("account/logout", void 0, {
|
|
1786
|
+
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
|
|
1787
|
+
signal
|
|
1788
|
+
});
|
|
1789
|
+
if (Object.keys(result).length !== 0) {
|
|
1790
|
+
throw new CodexAppServerError({
|
|
1791
|
+
message: "Codex ChatGPT logout was not confirmed.",
|
|
1792
|
+
category: "auth",
|
|
1793
|
+
code: "chatgpt_logout_unconfirmed",
|
|
1794
|
+
retryable: false
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async readModelCapabilities(deadlineAtMs, signal) {
|
|
1799
|
+
let cursor = null;
|
|
1800
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1801
|
+
const capabilities = [];
|
|
1802
|
+
do {
|
|
1803
|
+
if (signal?.aborted) {
|
|
1804
|
+
throw new CodexAppServerError({
|
|
1805
|
+
message: "Codex model catalog request was cancelled before dispatch.",
|
|
1806
|
+
category: "cancelled",
|
|
1807
|
+
code: "cancelled",
|
|
1808
|
+
retryable: false
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
if (Date.now() >= deadlineAtMs) {
|
|
1812
|
+
throw new CodexAppServerError({
|
|
1813
|
+
message: "Codex model catalog request exceeded the immutable deadline before dispatch.",
|
|
1814
|
+
category: "timeout",
|
|
1815
|
+
code: "deadline_exceeded",
|
|
1816
|
+
retryable: false
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
const result = await this.request("model/list", {
|
|
1820
|
+
includeHidden: false,
|
|
1821
|
+
...cursor ? { cursor } : {}
|
|
1822
|
+
}, {
|
|
1823
|
+
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
|
|
1824
|
+
signal
|
|
1825
|
+
});
|
|
1826
|
+
const data = Array.isArray(result.data) ? result.data : [];
|
|
1827
|
+
for (const raw of data) {
|
|
1828
|
+
const candidate = objectOrNull(raw);
|
|
1829
|
+
if (!candidate) continue;
|
|
1830
|
+
const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
|
|
1831
|
+
const model = typeof candidate.model === "string" ? candidate.model.trim() : "";
|
|
1832
|
+
const displayName = typeof candidate.displayName === "string" ? candidate.displayName.trim() : "";
|
|
1833
|
+
const hidden = candidate.hidden === true;
|
|
1834
|
+
const supportedReasoningEfforts = Array.isArray(candidate.supportedReasoningEfforts) ? candidate.supportedReasoningEfforts.flatMap((item) => {
|
|
1835
|
+
const reasoningEffort = objectOrNull(item)?.reasoningEffort;
|
|
1836
|
+
return typeof reasoningEffort === "string" && reasoningEffort.trim() ? [reasoningEffort.trim()] : [];
|
|
1837
|
+
}) : [];
|
|
1838
|
+
const defaultReasoningEffort = typeof candidate.defaultReasoningEffort === "string" ? candidate.defaultReasoningEffort.trim() : "";
|
|
1839
|
+
if (!id || !model || !displayName || hidden || supportedReasoningEfforts.length === 0 || new Set(supportedReasoningEfforts).size !== supportedReasoningEfforts.length || !supportedReasoningEfforts.includes(defaultReasoningEffort)) continue;
|
|
1840
|
+
capabilities.push(Object.freeze({
|
|
1841
|
+
id,
|
|
1842
|
+
model,
|
|
1843
|
+
displayName,
|
|
1844
|
+
hidden,
|
|
1845
|
+
supportedReasoningEfforts: Object.freeze(supportedReasoningEfforts),
|
|
1846
|
+
defaultReasoningEffort,
|
|
1847
|
+
isDefault: candidate.isDefault === true
|
|
1848
|
+
}));
|
|
1849
|
+
}
|
|
1850
|
+
cursor = typeof result.nextCursor === "string" && result.nextCursor ? result.nextCursor : null;
|
|
1851
|
+
if (cursor && seen.has(cursor)) {
|
|
1852
|
+
throw new CodexAppServerError({
|
|
1853
|
+
message: "Codex model catalog repeated a cursor.",
|
|
1854
|
+
category: "adapter",
|
|
1855
|
+
code: "model_catalog_cursor_repeated",
|
|
1856
|
+
retryable: false
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
if (cursor) seen.add(cursor);
|
|
1860
|
+
} while (cursor);
|
|
1861
|
+
return Object.freeze(capabilities);
|
|
1862
|
+
}
|
|
1863
|
+
requireModelCapability(capabilities, modelName, reasoningEffort) {
|
|
1864
|
+
const match = capabilities.find((candidate) => candidate.model === modelName || candidate.id === modelName);
|
|
1865
|
+
if (!match) {
|
|
1866
|
+
throw new CodexAppServerError({
|
|
1867
|
+
message: "The requested Codex model is unavailable.",
|
|
1868
|
+
category: "model",
|
|
1869
|
+
code: "model_unavailable",
|
|
1870
|
+
retryable: false
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
if (!match.supportedReasoningEfforts.includes(reasoningEffort)) {
|
|
1874
|
+
throw new CodexAppServerError({
|
|
1875
|
+
message: "The requested Codex reasoning effort is unavailable.",
|
|
1876
|
+
category: "model",
|
|
1877
|
+
code: "reasoning_effort_unavailable",
|
|
1878
|
+
retryable: false
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
async requireModel(modelName, reasoningEffort, deadlineAtMs, signal) {
|
|
1883
|
+
this.requireModelCapability(
|
|
1884
|
+
await this.readModelCapabilities(deadlineAtMs, signal),
|
|
1885
|
+
modelName,
|
|
1886
|
+
reasoningEffort
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
async startThread(options) {
|
|
1890
|
+
if (options.signal?.aborted) {
|
|
1891
|
+
throw new CodexAppServerError({
|
|
1892
|
+
message: "Codex thread request was cancelled before dispatch.",
|
|
1893
|
+
category: "cancelled",
|
|
1894
|
+
code: "cancelled",
|
|
1895
|
+
retryable: false
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
if (Date.now() >= options.deadlineAtMs) {
|
|
1899
|
+
throw new CodexAppServerError({
|
|
1900
|
+
message: "Codex thread request exceeded the immutable deadline before dispatch.",
|
|
1901
|
+
category: "timeout",
|
|
1902
|
+
code: "deadline_exceeded",
|
|
1903
|
+
retryable: false
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
const result = await this.request("thread/start", {
|
|
1907
|
+
model: options.requestedModel,
|
|
1908
|
+
modelProvider: "openai",
|
|
1909
|
+
allowProviderModelFallback: false,
|
|
1910
|
+
serviceTier: "default",
|
|
1911
|
+
cwd: options.workspacePath,
|
|
1912
|
+
runtimeWorkspaceRoots: [options.workspacePath],
|
|
1913
|
+
approvalPolicy: "never",
|
|
1914
|
+
permissions: CODEX_INFERENCE_PERMISSION_PROFILE,
|
|
1915
|
+
ephemeral: false,
|
|
1916
|
+
historyMode: "legacy",
|
|
1917
|
+
environments: [],
|
|
1918
|
+
dynamicTools: [],
|
|
1919
|
+
selectedCapabilityRoots: [],
|
|
1920
|
+
experimentalRawEvents: false,
|
|
1921
|
+
developerInstructions: options.systemPrompt,
|
|
1922
|
+
config: {
|
|
1923
|
+
model_reasoning_effort: options.requestedReasoningEffort,
|
|
1924
|
+
model_reasoning_summary: "none",
|
|
1925
|
+
show_raw_agent_reasoning: true,
|
|
1926
|
+
hide_agent_reasoning: false,
|
|
1927
|
+
web_search: "disabled",
|
|
1928
|
+
features: {
|
|
1929
|
+
apps: false,
|
|
1930
|
+
code_mode: false,
|
|
1931
|
+
code_mode_buffered_exec: false,
|
|
1932
|
+
code_mode_host: false,
|
|
1933
|
+
code_mode_only: false,
|
|
1934
|
+
in_app_browser: false,
|
|
1935
|
+
js_repl: false,
|
|
1936
|
+
js_repl_tools_only: false,
|
|
1937
|
+
multi_agent: false,
|
|
1938
|
+
multi_agent_v2: false,
|
|
1939
|
+
plugin_sharing: false,
|
|
1940
|
+
plugins: false,
|
|
1941
|
+
remote_plugin: false,
|
|
1942
|
+
shell_tool: false,
|
|
1943
|
+
skill_mcp_dependency_install: false,
|
|
1944
|
+
standalone_web_search: false,
|
|
1945
|
+
web_search_cached: false,
|
|
1946
|
+
web_search_request: false
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}, {
|
|
1950
|
+
timeoutMs: Math.max(1, options.deadlineAtMs - Date.now()),
|
|
1951
|
+
signal: options.signal,
|
|
1952
|
+
// Thread creation does not dispatch inference, but it can persist prompt
|
|
1953
|
+
// bytes. Keep its exact reply correlated after cancellation so we can
|
|
1954
|
+
// validate and delete the thread before reporting cleanup. The original
|
|
1955
|
+
// RPC deadline still bounds setup; turn/start keeps normal cancellation.
|
|
1956
|
+
retainResponseOnAbort: true
|
|
1957
|
+
});
|
|
1958
|
+
const thread = objectOrNull(result.thread);
|
|
1959
|
+
const permission = objectOrNull(result.activePermissionProfile);
|
|
1960
|
+
const sandbox = objectOrNull(result.sandbox);
|
|
1961
|
+
const threadId = String(thread?.id || "");
|
|
1962
|
+
const threadPath = String(thread?.path || "");
|
|
1963
|
+
const resolvedHome = resolve2(this.codexHome);
|
|
1964
|
+
const resolvedThreadPath = resolve2(threadPath);
|
|
1965
|
+
const resolvedSessionsRoot = join2(resolvedHome, "sessions");
|
|
1966
|
+
if (!threadId || !threadPath || !isAbsolute2(threadPath) || !resolvedThreadPath.startsWith(`${resolvedSessionsRoot}${sep}`) || !resolvedThreadPath.endsWith(".jsonl") || thread?.ephemeral !== false || thread?.modelProvider !== "openai" || !Array.isArray(thread?.turns) || thread.turns.length !== 0 || result.model !== options.requestedModel || result.modelProvider !== "openai" || result.serviceTier !== "default" || result.reasoningEffort !== options.requestedReasoningEffort || String(result.cwd || "") !== options.workspacePath || !Array.isArray(result.runtimeWorkspaceRoots) || result.runtimeWorkspaceRoots.length !== 0 || !Array.isArray(result.instructionSources) || result.instructionSources.length !== 0 || result.approvalPolicy !== "never" || result.approvalsReviewer !== "user" || sandbox?.type !== "readOnly" || sandbox.networkAccess !== false || permission?.id !== CODEX_INFERENCE_PERMISSION_PROFILE || permission.extends !== null) {
|
|
1967
|
+
throw new CodexAppServerError({
|
|
1968
|
+
message: "Codex thread isolation or effective settings were invalid.",
|
|
1969
|
+
category: "adapter",
|
|
1970
|
+
code: "invalid_thread_provenance",
|
|
1971
|
+
retryable: false
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
this.threads.set(threadId, resolvedThreadPath);
|
|
1975
|
+
if (options.signal?.aborted || Date.now() >= options.deadlineAtMs) {
|
|
1976
|
+
await this.deleteThread(threadId);
|
|
1977
|
+
throw new CodexAppServerError({
|
|
1978
|
+
message: "Codex thread setup ended before inference dispatch.",
|
|
1979
|
+
category: options.signal?.aborted ? "cancelled" : "timeout",
|
|
1980
|
+
code: options.signal?.aborted ? "cancelled" : "deadline_exceeded",
|
|
1981
|
+
retryable: false
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
threadId,
|
|
1986
|
+
threadPath: resolvedThreadPath,
|
|
1987
|
+
effectiveModel: options.requestedModel,
|
|
1988
|
+
effectiveReasoningEffort: options.requestedReasoningEffort
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
async startAgentThread(options) {
|
|
1992
|
+
if (options.signal?.aborted || Date.now() >= options.deadlineAtMs) {
|
|
1993
|
+
throw new CodexAppServerError({
|
|
1994
|
+
message: "Codex Agent thread request was cancelled before dispatch.",
|
|
1995
|
+
category: options.signal?.aborted ? "cancelled" : "timeout",
|
|
1996
|
+
code: options.signal?.aborted ? "cancelled" : "deadline_exceeded",
|
|
1997
|
+
retryable: false
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
const result = await this.request("thread/start", {
|
|
2001
|
+
model: options.requestedModel,
|
|
2002
|
+
modelProvider: "openai",
|
|
2003
|
+
allowProviderModelFallback: false,
|
|
2004
|
+
serviceTier: "default",
|
|
2005
|
+
cwd: options.workspacePath,
|
|
2006
|
+
runtimeWorkspaceRoots: [options.workspacePath],
|
|
2007
|
+
approvalPolicy: "never",
|
|
2008
|
+
permissions: CODEX_AGENT_PERMISSION_PROFILE,
|
|
2009
|
+
ephemeral: false,
|
|
2010
|
+
historyMode: "legacy",
|
|
2011
|
+
environments: [],
|
|
2012
|
+
dynamicTools: createCodexAgentToolSpecs(options.dataContract, options.decisionSchema),
|
|
2013
|
+
selectedCapabilityRoots: [],
|
|
2014
|
+
experimentalRawEvents: false,
|
|
2015
|
+
developerInstructions: options.systemPrompt,
|
|
2016
|
+
config: {
|
|
2017
|
+
model_reasoning_effort: options.requestedReasoningEffort,
|
|
2018
|
+
model_reasoning_summary: "none",
|
|
2019
|
+
show_raw_agent_reasoning: true,
|
|
2020
|
+
hide_agent_reasoning: false,
|
|
2021
|
+
web_search: "live",
|
|
2022
|
+
features: {
|
|
2023
|
+
apps: false,
|
|
2024
|
+
code_mode: true,
|
|
2025
|
+
code_mode_buffered_exec: false,
|
|
2026
|
+
code_mode_host: true,
|
|
2027
|
+
code_mode_only: false,
|
|
2028
|
+
in_app_browser: false,
|
|
2029
|
+
js_repl: false,
|
|
2030
|
+
js_repl_tools_only: false,
|
|
2031
|
+
multi_agent: false,
|
|
2032
|
+
multi_agent_v2: false,
|
|
2033
|
+
plugin_sharing: false,
|
|
2034
|
+
plugins: false,
|
|
2035
|
+
remote_plugin: false,
|
|
2036
|
+
shell_tool: false,
|
|
2037
|
+
skill_mcp_dependency_install: false,
|
|
2038
|
+
standalone_web_search: true,
|
|
2039
|
+
web_search_cached: false,
|
|
2040
|
+
web_search_request: true
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}, {
|
|
2044
|
+
timeoutMs: Math.max(1, options.deadlineAtMs - Date.now()),
|
|
2045
|
+
signal: options.signal
|
|
2046
|
+
});
|
|
2047
|
+
return this.validateAgentThreadReceipt(result, options, false);
|
|
2048
|
+
}
|
|
2049
|
+
async resumeAgentThread(options) {
|
|
2050
|
+
const expectedPath = resolve2(options.threadPath);
|
|
2051
|
+
const sessionsRoot = join2(resolve2(this.codexHome), "sessions");
|
|
2052
|
+
if (!options.threadId.trim() || !isAbsolute2(options.threadPath) || !expectedPath.startsWith(`${sessionsRoot}${sep}`) || !expectedPath.endsWith(".jsonl")) {
|
|
2053
|
+
throw new CodexAppServerError({
|
|
2054
|
+
message: "Codex Agent durable thread receipt was invalid.",
|
|
2055
|
+
category: "adapter",
|
|
2056
|
+
code: "invalid_agent_thread_receipt",
|
|
2057
|
+
retryable: false
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
const result = await this.request("thread/resume", {
|
|
2061
|
+
threadId: options.threadId,
|
|
2062
|
+
model: options.requestedModel,
|
|
2063
|
+
modelProvider: "openai",
|
|
2064
|
+
serviceTier: "default",
|
|
2065
|
+
cwd: options.workspacePath,
|
|
2066
|
+
approvalPolicy: "never",
|
|
2067
|
+
permissions: CODEX_AGENT_PERMISSION_PROFILE,
|
|
2068
|
+
developerInstructions: options.systemPrompt,
|
|
2069
|
+
config: {
|
|
2070
|
+
model_reasoning_effort: options.requestedReasoningEffort,
|
|
2071
|
+
model_reasoning_summary: "none",
|
|
2072
|
+
web_search: "live"
|
|
2073
|
+
}
|
|
2074
|
+
}, {
|
|
2075
|
+
timeoutMs: Math.max(1, options.deadlineAtMs - Date.now()),
|
|
2076
|
+
signal: options.signal
|
|
2077
|
+
});
|
|
2078
|
+
const receipt = this.validateAgentThreadReceipt(result, options, true);
|
|
2079
|
+
if (receipt.threadId !== options.threadId || receipt.threadPath !== expectedPath) {
|
|
2080
|
+
throw new CodexAppServerError({
|
|
2081
|
+
message: "Codex Agent resumed a different durable thread.",
|
|
2082
|
+
category: "adapter",
|
|
2083
|
+
code: "agent_thread_identity_changed",
|
|
2084
|
+
retryable: false
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
return receipt;
|
|
2088
|
+
}
|
|
2089
|
+
validateAgentThreadReceipt(result, options, resumed) {
|
|
2090
|
+
const thread = objectOrNull(result.thread);
|
|
2091
|
+
const permission = objectOrNull(result.activePermissionProfile);
|
|
2092
|
+
const sandbox = objectOrNull(result.sandbox);
|
|
2093
|
+
const threadId = String(thread?.id || "");
|
|
2094
|
+
const threadPath = String(thread?.path || "");
|
|
2095
|
+
const resolvedThreadPath = resolve2(threadPath);
|
|
2096
|
+
const sessionsRoot = join2(resolve2(this.codexHome), "sessions");
|
|
2097
|
+
if (!threadId || !threadPath || !isAbsolute2(threadPath) || !resolvedThreadPath.startsWith(`${sessionsRoot}${sep}`) || !resolvedThreadPath.endsWith(".jsonl") || thread?.ephemeral !== false || thread?.modelProvider !== "openai" || !Array.isArray(thread?.turns) || !resumed && thread.turns.length !== 0 || result.model !== options.requestedModel || result.modelProvider !== "openai" || result.serviceTier !== "default" || result.reasoningEffort !== options.requestedReasoningEffort || String(result.cwd || "") !== options.workspacePath || !Array.isArray(result.instructionSources) || result.instructionSources.length !== 0 || result.approvalPolicy !== "never" || result.approvalsReviewer !== "user" || sandbox?.type !== "readOnly" || sandbox.networkAccess !== false || permission?.id !== CODEX_AGENT_PERMISSION_PROFILE || permission.extends !== null) {
|
|
2098
|
+
throw new CodexAppServerError({
|
|
2099
|
+
message: "Codex Agent thread isolation or effective settings were invalid.",
|
|
2100
|
+
category: "adapter",
|
|
2101
|
+
code: "invalid_agent_thread_provenance",
|
|
2102
|
+
retryable: false
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
this.threads.set(threadId, resolvedThreadPath);
|
|
2106
|
+
return {
|
|
2107
|
+
threadId,
|
|
2108
|
+
threadPath: resolvedThreadPath,
|
|
2109
|
+
effectiveModel: options.requestedModel,
|
|
2110
|
+
effectiveReasoningEffort: options.requestedReasoningEffort
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
async executeTurn(request) {
|
|
2114
|
+
return await this.executeTurnWithPolicy(request, false);
|
|
2115
|
+
}
|
|
2116
|
+
async executeAgentTurn(request) {
|
|
2117
|
+
return await this.executeTurnWithPolicy(request, true);
|
|
2118
|
+
}
|
|
2119
|
+
async executeTurnWithPolicy(request, agentMode) {
|
|
2120
|
+
if (request.signal?.aborted || request.deadlineAtMs <= Date.now()) {
|
|
2121
|
+
throw new CodexAppServerError({
|
|
2122
|
+
message: "Codex turn was cancelled before dispatch.",
|
|
2123
|
+
category: request.signal?.aborted ? "cancelled" : "timeout",
|
|
2124
|
+
code: request.signal?.aborted ? "cancelled" : "deadline_exceeded",
|
|
2125
|
+
retryable: false
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
let requestWritten = false;
|
|
2129
|
+
let requestId = null;
|
|
2130
|
+
let turnId = null;
|
|
2131
|
+
let rerouted = false;
|
|
2132
|
+
let rawResponseUsage = null;
|
|
2133
|
+
let threadUsage = null;
|
|
2134
|
+
let adapterResponseId = null;
|
|
2135
|
+
let firstTokenAt = null;
|
|
2136
|
+
let terminal = null;
|
|
2137
|
+
let completedReasoningObserved = false;
|
|
2138
|
+
let completedReasoningSequence = 0;
|
|
2139
|
+
const completedReasoningContent = /* @__PURE__ */ new Map();
|
|
2140
|
+
const agentToolCalls = [];
|
|
2141
|
+
const agentWebSearches = [];
|
|
2142
|
+
let terminalResolve;
|
|
2143
|
+
let terminalReject;
|
|
2144
|
+
const terminalPromise = new Promise((resolve3, reject) => {
|
|
2145
|
+
terminalResolve = resolve3;
|
|
2146
|
+
terminalReject = reject;
|
|
2147
|
+
});
|
|
2148
|
+
void terminalPromise.catch(() => void 0);
|
|
2149
|
+
const unsubscribeFatal = this.subscribeFatal((error) => {
|
|
2150
|
+
terminalReject(new CodexAppServerError({
|
|
2151
|
+
message: error.message,
|
|
2152
|
+
category: error.category,
|
|
2153
|
+
code: error.code,
|
|
2154
|
+
retryable: error.retryable,
|
|
2155
|
+
dispatchOutcome: requestWritten ? "outcome_unknown" : "not_dispatched",
|
|
2156
|
+
processExit: error.processExit,
|
|
2157
|
+
cause: error
|
|
2158
|
+
}));
|
|
2159
|
+
});
|
|
2160
|
+
const startedAt = Date.now();
|
|
2161
|
+
const unsubscribe = this.subscribe(({ method, params }) => {
|
|
2162
|
+
if (method === "thread/started") {
|
|
2163
|
+
const startedThread = objectOrNull(params.thread);
|
|
2164
|
+
if (!String(startedThread?.id || "")) {
|
|
2165
|
+
terminalReject(new CodexAppServerError({
|
|
2166
|
+
message: "Codex returned an invalid thread lifecycle notification.",
|
|
2167
|
+
category: "adapter",
|
|
2168
|
+
code: "invalid_thread_started_notification",
|
|
2169
|
+
retryable: false,
|
|
2170
|
+
dispatchOutcome: requestWritten ? turnId ? "confirmed_dispatched" : "outcome_unknown" : "not_dispatched"
|
|
2171
|
+
}));
|
|
2172
|
+
}
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const eventThreadId = String(params.threadId || "");
|
|
2176
|
+
const scopedMethod = method.startsWith("thread/") || method.startsWith("turn/") || method.startsWith("item/") || method.startsWith("rawResponse") || method === "model/rerouted";
|
|
2177
|
+
if (eventThreadId && eventThreadId !== request.thread.threadId) {
|
|
2178
|
+
if (!this.threads.has(eventThreadId)) {
|
|
2179
|
+
terminalReject(new CodexAppServerError({
|
|
2180
|
+
message: "Codex notification crossed thread scope.",
|
|
2181
|
+
category: "adapter",
|
|
2182
|
+
code: "cross_thread_notification",
|
|
2183
|
+
retryable: false,
|
|
2184
|
+
dispatchOutcome: turnId ? "confirmed_dispatched" : "outcome_unknown"
|
|
2185
|
+
}));
|
|
2186
|
+
}
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (scopedMethod && !eventThreadId) {
|
|
2190
|
+
terminalReject(new CodexAppServerError({
|
|
2191
|
+
message: "Codex notification omitted its thread scope.",
|
|
2192
|
+
category: "adapter",
|
|
2193
|
+
code: "missing_thread_notification_scope",
|
|
2194
|
+
retryable: false,
|
|
2195
|
+
dispatchOutcome: requestWritten ? turnId ? "confirmed_dispatched" : "outcome_unknown" : "not_dispatched"
|
|
2196
|
+
}));
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
if (agentMode ? forbiddenAgentMethod(method) : forbiddenMethod(method)) {
|
|
2200
|
+
terminalReject(new CodexAppServerError({
|
|
2201
|
+
message: "Codex attempted to use forbidden child authority.",
|
|
2202
|
+
category: "adapter",
|
|
2203
|
+
code: "forbidden_child_authority",
|
|
2204
|
+
retryable: false,
|
|
2205
|
+
dispatchOutcome: turnId ? "confirmed_dispatched" : "outcome_unknown"
|
|
2206
|
+
}));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (method === "model/rerouted") {
|
|
2210
|
+
rerouted = true;
|
|
2211
|
+
terminalReject(new CodexAppServerError({
|
|
2212
|
+
message: "Codex rerouted the requested model.",
|
|
2213
|
+
category: "model",
|
|
2214
|
+
code: "model_rerouted",
|
|
2215
|
+
retryable: false,
|
|
2216
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2217
|
+
}));
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (method === "thread/settings/updated") {
|
|
2221
|
+
const settings = objectOrNull(params.threadSettings);
|
|
2222
|
+
const permission = objectOrNull(settings?.activePermissionProfile);
|
|
2223
|
+
if (!settings || settings.model !== request.requestedModel || settings.modelProvider !== "openai" || settings.effort !== request.requestedReasoningEffort || settings.summary !== "none" || String(settings.cwd || "") !== request.workspacePath || settings.approvalPolicy !== "never" || permission?.id !== (agentMode ? CODEX_AGENT_PERMISSION_PROFILE : CODEX_INFERENCE_PERMISSION_PROFILE) || permission.extends !== null) {
|
|
2224
|
+
terminalReject(new CodexAppServerError({
|
|
2225
|
+
message: "Codex effective turn settings changed after dispatch.",
|
|
2226
|
+
category: "adapter",
|
|
2227
|
+
code: "effective_settings_changed",
|
|
2228
|
+
retryable: false,
|
|
2229
|
+
dispatchOutcome: turnId ? "confirmed_dispatched" : "outcome_unknown"
|
|
2230
|
+
}));
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const eventTurn = objectOrNull(params.turn);
|
|
2235
|
+
const eventTurnId = String(params.turnId || eventTurn?.id || "");
|
|
2236
|
+
if (eventTurnId && !eventThreadId) {
|
|
2237
|
+
terminalReject(new CodexAppServerError({
|
|
2238
|
+
message: "Codex turn notification omitted its thread scope.",
|
|
2239
|
+
category: "adapter",
|
|
2240
|
+
code: "missing_thread_notification_scope",
|
|
2241
|
+
retryable: false,
|
|
2242
|
+
dispatchOutcome: requestWritten ? turnId ? "confirmed_dispatched" : "outcome_unknown" : "not_dispatched"
|
|
2243
|
+
}));
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const turnScopedMethod = method.startsWith("turn/") || method.startsWith("item/") || method.startsWith("rawResponse") || method === "model/rerouted";
|
|
2247
|
+
if (turnScopedMethod && !eventTurnId) {
|
|
2248
|
+
terminalReject(new CodexAppServerError({
|
|
2249
|
+
message: "Codex notification omitted its turn scope.",
|
|
2250
|
+
category: "adapter",
|
|
2251
|
+
code: "missing_turn_notification_scope",
|
|
2252
|
+
retryable: false,
|
|
2253
|
+
dispatchOutcome: requestWritten ? turnId ? "confirmed_dispatched" : "outcome_unknown" : "not_dispatched"
|
|
2254
|
+
}));
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
if (eventTurnId) {
|
|
2258
|
+
if (turnId && turnId !== eventTurnId) {
|
|
2259
|
+
terminalReject(new CodexAppServerError({
|
|
2260
|
+
message: "Codex notification crossed turn scope.",
|
|
2261
|
+
category: "adapter",
|
|
2262
|
+
code: "cross_turn_notification",
|
|
2263
|
+
retryable: false,
|
|
2264
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2265
|
+
}));
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
turnId = eventTurnId;
|
|
2269
|
+
}
|
|
2270
|
+
if (method === "item/agentMessage/delta" && String(params.delta || "").length > 0 && firstTokenAt === null) {
|
|
2271
|
+
firstTokenAt = Date.now();
|
|
2272
|
+
}
|
|
2273
|
+
if (method === "item/started" || method === "item/completed") {
|
|
2274
|
+
const item = objectOrNull(params.item);
|
|
2275
|
+
if (!item || (agentMode ? forbiddenAgentTerminalItem(item) : forbiddenTerminalItem(item))) {
|
|
2276
|
+
terminalReject(new CodexAppServerError({
|
|
2277
|
+
message: "Codex attempted forbidden tool activity.",
|
|
2278
|
+
category: "adapter",
|
|
2279
|
+
code: "forbidden_child_authority",
|
|
2280
|
+
retryable: false,
|
|
2281
|
+
dispatchOutcome: turnId ? "confirmed_dispatched" : "outcome_unknown"
|
|
2282
|
+
}));
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (method === "item/completed" && item.type === "reasoning") {
|
|
2286
|
+
completedReasoningObserved = true;
|
|
2287
|
+
const itemId = String(item.id || params.itemId || "").trim();
|
|
2288
|
+
const itemKey = itemId ? `id:${itemId}` : `sequence:${completedReasoningSequence++}`;
|
|
2289
|
+
completedReasoningContent.set(
|
|
2290
|
+
itemKey,
|
|
2291
|
+
Array.isArray(item.content) ? item.content : []
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
if (agentMode && method === "item/completed" && item.type === "dynamicToolCall") {
|
|
2295
|
+
const tool = String(item.tool || "");
|
|
2296
|
+
const callArguments = objectOrNull(item.arguments);
|
|
2297
|
+
const callId = String(item.id || "").trim();
|
|
2298
|
+
if (!callId || !CODEX_AGENT_DYNAMIC_TOOLS.includes(tool) || !callArguments || typeof item.success !== "boolean") {
|
|
2299
|
+
terminalReject(new CodexAppServerError({
|
|
2300
|
+
message: "Codex Agent returned invalid dynamic-tool evidence.",
|
|
2301
|
+
category: "adapter",
|
|
2302
|
+
code: "invalid_dynamic_tool_evidence",
|
|
2303
|
+
retryable: false,
|
|
2304
|
+
dispatchOutcome: turnId ? "confirmed_dispatched" : "outcome_unknown"
|
|
2305
|
+
}));
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
agentToolCalls.push({
|
|
2309
|
+
callId,
|
|
2310
|
+
tool,
|
|
2311
|
+
arguments: callArguments,
|
|
2312
|
+
success: item.success
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
if (agentMode && method === "item/completed" && item.type === "webSearch") {
|
|
2316
|
+
const query = typeof item.query === "string" ? item.query.trim() : "";
|
|
2317
|
+
if (!query) {
|
|
2318
|
+
terminalReject(new CodexAppServerError({
|
|
2319
|
+
message: "Codex Agent returned invalid web-search evidence.",
|
|
2320
|
+
category: "adapter",
|
|
2321
|
+
code: "invalid_web_search_evidence",
|
|
2322
|
+
retryable: false,
|
|
2323
|
+
dispatchOutcome: turnId ? "confirmed_dispatched" : "outcome_unknown"
|
|
2324
|
+
}));
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
agentWebSearches.push({ query, action: objectOrNull(item.action) });
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
const observedUsage = usageFromNotification(method, params);
|
|
2331
|
+
if (method === "rawResponse/completed") {
|
|
2332
|
+
const responseId = String(params.responseId || "").trim();
|
|
2333
|
+
if (!responseId || !observedUsage || adapterResponseId !== null) {
|
|
2334
|
+
terminalReject(new CodexAppServerError({
|
|
2335
|
+
message: "Codex raw response evidence was invalid or ambiguous.",
|
|
2336
|
+
category: "adapter",
|
|
2337
|
+
code: "invalid_raw_response_evidence",
|
|
2338
|
+
retryable: false,
|
|
2339
|
+
dispatchOutcome: turnId ? "confirmed_dispatched" : "outcome_unknown"
|
|
2340
|
+
}));
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
adapterResponseId = responseId;
|
|
2344
|
+
rawResponseUsage = observedUsage;
|
|
2345
|
+
} else if (method === "thread/tokenUsage/updated" && observedUsage) {
|
|
2346
|
+
threadUsage = observedUsage;
|
|
2347
|
+
}
|
|
2348
|
+
if (method === "turn/completed" && eventTurn) {
|
|
2349
|
+
terminal = eventTurn;
|
|
2350
|
+
terminalResolve(eventTurn);
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
if (agentMode) {
|
|
2354
|
+
if (this.agentToolHandlers.has(request.thread.threadId)) {
|
|
2355
|
+
unsubscribe();
|
|
2356
|
+
unsubscribeFatal();
|
|
2357
|
+
throw new CodexAppServerError({
|
|
2358
|
+
message: "Codex Agent thread already has an active turn.",
|
|
2359
|
+
category: "adapter",
|
|
2360
|
+
code: "agent_thread_turn_already_active",
|
|
2361
|
+
retryable: false
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
this.agentToolHandlers.set(
|
|
2365
|
+
request.thread.threadId,
|
|
2366
|
+
{
|
|
2367
|
+
execute: request.executeTool,
|
|
2368
|
+
acceptTurnId: (candidate) => {
|
|
2369
|
+
if (turnId && turnId !== candidate) return false;
|
|
2370
|
+
turnId = candidate;
|
|
2371
|
+
return true;
|
|
2372
|
+
},
|
|
2373
|
+
calls: /* @__PURE__ */ new Map()
|
|
2374
|
+
}
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
try {
|
|
2378
|
+
const remaining = () => Math.max(1, request.deadlineAtMs - Date.now());
|
|
2379
|
+
let startResult;
|
|
2380
|
+
try {
|
|
2381
|
+
startResult = await this.request("turn/start", {
|
|
2382
|
+
threadId: request.thread.threadId,
|
|
2383
|
+
input: [{ type: "text", text: request.userPrompt, text_elements: [] }],
|
|
2384
|
+
environments: [],
|
|
2385
|
+
cwd: request.workspacePath,
|
|
2386
|
+
runtimeWorkspaceRoots: [request.workspacePath],
|
|
2387
|
+
approvalPolicy: "never",
|
|
2388
|
+
permissions: agentMode ? CODEX_AGENT_PERMISSION_PROFILE : CODEX_INFERENCE_PERMISSION_PROFILE,
|
|
2389
|
+
model: request.requestedModel,
|
|
2390
|
+
serviceTier: "default",
|
|
2391
|
+
effort: request.requestedReasoningEffort,
|
|
2392
|
+
summary: "none",
|
|
2393
|
+
outputSchema: request.outputSchema
|
|
2394
|
+
}, {
|
|
2395
|
+
timeoutMs: remaining(),
|
|
2396
|
+
signal: request.signal,
|
|
2397
|
+
notAfterMs: "providerDispatchNotAfterMs" in request ? request.providerDispatchNotAfterMs : void 0,
|
|
2398
|
+
beforeWrite: async (id) => {
|
|
2399
|
+
requestId = id;
|
|
2400
|
+
try {
|
|
2401
|
+
await request.onDispatchState?.({
|
|
2402
|
+
requestId: String(id),
|
|
2403
|
+
turnId,
|
|
2404
|
+
dispatchOutcome: "outcome_unknown"
|
|
2405
|
+
});
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
throw new CodexAppServerError({
|
|
2408
|
+
message: "Codex pre-dispatch recovery evidence could not be persisted.",
|
|
2409
|
+
category: "adapter",
|
|
2410
|
+
code: "recovery_checkpoint_failed",
|
|
2411
|
+
retryable: false,
|
|
2412
|
+
dispatchOutcome: "not_dispatched",
|
|
2413
|
+
cause: error
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
},
|
|
2417
|
+
onWritten: (id) => {
|
|
2418
|
+
requestWritten = true;
|
|
2419
|
+
requestId = id;
|
|
2420
|
+
if ("onProviderDispatch" in request) request.onProviderDispatch?.();
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
if (error instanceof CodexAppServerError && requestWritten) {
|
|
2425
|
+
throw new CodexAppServerError({
|
|
2426
|
+
message: error.message,
|
|
2427
|
+
category: error.category,
|
|
2428
|
+
code: error.code,
|
|
2429
|
+
retryable: error.retryable,
|
|
2430
|
+
dispatchOutcome: "outcome_unknown",
|
|
2431
|
+
processExit: error.processExit,
|
|
2432
|
+
cause: error
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
throw error;
|
|
2436
|
+
}
|
|
2437
|
+
const startedTurn = objectOrNull(startResult.turn);
|
|
2438
|
+
const responseTurnId = String(startedTurn?.id || "");
|
|
2439
|
+
if (!responseTurnId || turnId && turnId !== responseTurnId) {
|
|
2440
|
+
throw new CodexAppServerError({
|
|
2441
|
+
message: "Codex turn start response was invalid.",
|
|
2442
|
+
category: "adapter",
|
|
2443
|
+
code: "invalid_turn_start",
|
|
2444
|
+
retryable: false,
|
|
2445
|
+
dispatchOutcome: "outcome_unknown"
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
turnId = responseTurnId;
|
|
2449
|
+
try {
|
|
2450
|
+
await request.onDispatchState?.({
|
|
2451
|
+
requestId: String(requestId),
|
|
2452
|
+
turnId,
|
|
2453
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2454
|
+
});
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
try {
|
|
2457
|
+
await this.interrupt(request.thread.threadId, turnId, Math.min(5e3, remaining()));
|
|
2458
|
+
} catch {
|
|
2459
|
+
}
|
|
2460
|
+
throw new CodexAppServerError({
|
|
2461
|
+
message: "Codex confirmed-dispatch evidence could not be persisted.",
|
|
2462
|
+
category: "adapter",
|
|
2463
|
+
code: "recovery_checkpoint_failed",
|
|
2464
|
+
retryable: false,
|
|
2465
|
+
dispatchOutcome: "confirmed_dispatched",
|
|
2466
|
+
cause: error
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
if (rerouted) {
|
|
2470
|
+
throw new CodexAppServerError({
|
|
2471
|
+
message: "Codex rerouted the requested model.",
|
|
2472
|
+
category: "model",
|
|
2473
|
+
code: "model_rerouted",
|
|
2474
|
+
retryable: false,
|
|
2475
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
let deadlineTimer = null;
|
|
2479
|
+
let abortHandler = null;
|
|
2480
|
+
const abortPromise = new Promise((_, reject) => {
|
|
2481
|
+
const abort = () => reject(new CodexAppServerError({
|
|
2482
|
+
message: "Codex turn was cancelled.",
|
|
2483
|
+
category: "cancelled",
|
|
2484
|
+
code: "cancelled",
|
|
2485
|
+
retryable: false,
|
|
2486
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2487
|
+
}));
|
|
2488
|
+
if (request.signal?.aborted) {
|
|
2489
|
+
abort();
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
abortHandler = abort;
|
|
2493
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
2494
|
+
});
|
|
2495
|
+
const deadlinePromise = new Promise((_, reject) => {
|
|
2496
|
+
deadlineTimer = setTimeout(() => reject(new CodexAppServerError({
|
|
2497
|
+
message: "Codex turn exceeded its immutable deadline.",
|
|
2498
|
+
category: "timeout",
|
|
2499
|
+
code: "deadline_exceeded",
|
|
2500
|
+
retryable: false,
|
|
2501
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2502
|
+
})), remaining());
|
|
2503
|
+
});
|
|
2504
|
+
try {
|
|
2505
|
+
try {
|
|
2506
|
+
terminal = terminal ?? await Promise.race([terminalPromise, abortPromise, deadlinePromise]);
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
try {
|
|
2509
|
+
await this.interrupt(request.thread.threadId, turnId, Math.min(5e3, remaining()));
|
|
2510
|
+
} catch {
|
|
2511
|
+
}
|
|
2512
|
+
throw error;
|
|
2513
|
+
}
|
|
2514
|
+
} finally {
|
|
2515
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
2516
|
+
if (request.signal && abortHandler) request.signal.removeEventListener("abort", abortHandler);
|
|
2517
|
+
}
|
|
2518
|
+
if (terminal.status !== "completed") {
|
|
2519
|
+
const turnError = objectOrNull(terminal.error);
|
|
2520
|
+
let failure = classifyCodexTurnFailure(
|
|
2521
|
+
turnError?.codexErrorInfo,
|
|
2522
|
+
String(terminal.status)
|
|
2523
|
+
);
|
|
2524
|
+
const observedUsage = threadUsage ?? rawResponseUsage;
|
|
2525
|
+
let retryAtMs = null;
|
|
2526
|
+
let observedRateLimits = null;
|
|
2527
|
+
if (failure.code === "quota_exceeded" || failure.httpStatusCode === 429) {
|
|
2528
|
+
const observedAtMs = Date.now();
|
|
2529
|
+
let accountLimitReached = failure.code === "quota_exceeded";
|
|
2530
|
+
try {
|
|
2531
|
+
const metadataDeadlineAtMs = Math.min(
|
|
2532
|
+
request.deadlineAtMs,
|
|
2533
|
+
observedAtMs + 5e3
|
|
2534
|
+
);
|
|
2535
|
+
if (metadataDeadlineAtMs <= observedAtMs) throw new Error("rate-limit deadline elapsed");
|
|
2536
|
+
observedRateLimits = await this.readAccountRateLimits(
|
|
2537
|
+
metadataDeadlineAtMs,
|
|
2538
|
+
request.signal
|
|
2539
|
+
);
|
|
2540
|
+
accountLimitReached = accountLimitReached || codexAccountRateLimitReached(observedRateLimits);
|
|
2541
|
+
retryAtMs = codexRateLimitRetryAtMs(observedRateLimits);
|
|
2542
|
+
} catch {
|
|
2543
|
+
}
|
|
2544
|
+
if (accountLimitReached) {
|
|
2545
|
+
failure = {
|
|
2546
|
+
...failure,
|
|
2547
|
+
code: "quota_exceeded",
|
|
2548
|
+
category: "model",
|
|
2549
|
+
retryable: false
|
|
2550
|
+
};
|
|
2551
|
+
if (retryAtMs === null || retryAtMs <= observedAtMs) {
|
|
2552
|
+
retryAtMs = observedAtMs + DEFAULT_CODEX_QUOTA_COOLDOWN_MS;
|
|
2553
|
+
} else {
|
|
2554
|
+
retryAtMs = Math.min(
|
|
2555
|
+
retryAtMs,
|
|
2556
|
+
observedAtMs + MAX_CODEX_QUOTA_COOLDOWN_MS
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
} else {
|
|
2560
|
+
retryAtMs = observedAtMs + CODEX_TRANSIENT_RATE_LIMIT_COOLDOWN_MS;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
throw new CodexAppServerError({
|
|
2564
|
+
message: "Codex turn did not complete successfully.",
|
|
2565
|
+
category: failure.category,
|
|
2566
|
+
code: failure.code,
|
|
2567
|
+
retryable: failure.retryable,
|
|
2568
|
+
dispatchOutcome: "confirmed_dispatched",
|
|
2569
|
+
httpStatusCode: failure.httpStatusCode,
|
|
2570
|
+
retryAtMs,
|
|
2571
|
+
usage: observedUsage,
|
|
2572
|
+
rateLimits: observedRateLimits
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
const items = Array.isArray(terminal.items) ? terminal.items.map(objectOrNull) : [];
|
|
2576
|
+
if (items.some((item) => !item || (agentMode ? forbiddenAgentTerminalItem(item) : forbiddenTerminalItem(item)))) {
|
|
2577
|
+
throw new CodexAppServerError({
|
|
2578
|
+
message: "Codex terminal output contained forbidden tool activity.",
|
|
2579
|
+
category: "adapter",
|
|
2580
|
+
code: "forbidden_child_authority",
|
|
2581
|
+
retryable: false,
|
|
2582
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
const finalMessages = items.filter((item) => item?.type === "agentMessage" && item.phase === "final_answer");
|
|
2586
|
+
const fallbackMessages = items.filter((item) => item?.type === "agentMessage" && item.phase == null);
|
|
2587
|
+
const answerItem = finalMessages.at(-1) ?? fallbackMessages.at(-1);
|
|
2588
|
+
const text = typeof answerItem?.text === "string" ? answerItem.text : "";
|
|
2589
|
+
const reasoningContentSections = completedReasoningObserved ? [...completedReasoningContent.values()] : items.flatMap((item) => item?.type === "reasoning" && Array.isArray(item.content) ? [item.content] : []);
|
|
2590
|
+
const reasoningContentParts = reasoningContentSections.flatMap((content) => content.filter((part) => typeof part === "string" && part.trim().length > 0));
|
|
2591
|
+
const reasoningContentCandidate = reasoningContentParts.length > 0 ? reasoningContentParts.join("\n\n") : null;
|
|
2592
|
+
const reasoningContent = reasoningContentCandidate !== null && Buffer.byteLength(reasoningContentCandidate, "utf8") <= CODEX_REASONING_CONTENT_MAX_UTF8_BYTES ? reasoningContentCandidate : null;
|
|
2593
|
+
const usage = threadUsage ?? rawResponseUsage;
|
|
2594
|
+
const responseIdentity = adapterResponseId ?? turnId;
|
|
2595
|
+
if (!text || !usage || !responseIdentity) {
|
|
2596
|
+
throw new CodexAppServerError({
|
|
2597
|
+
message: !text ? "Codex returned no terminal answer." : !usage ? "Codex returned no valid token usage." : "Codex returned no response identity.",
|
|
2598
|
+
category: "adapter",
|
|
2599
|
+
code: !text ? "missing_terminal_answer" : !usage ? "missing_token_usage" : "missing_response_identity",
|
|
2600
|
+
retryable: false,
|
|
2601
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
const result = {
|
|
2605
|
+
text,
|
|
2606
|
+
reasoningContent,
|
|
2607
|
+
reasoningSummary: null,
|
|
2608
|
+
requestedModel: request.requestedModel,
|
|
2609
|
+
effectiveModel: request.thread.effectiveModel,
|
|
2610
|
+
requestedReasoningEffort: request.requestedReasoningEffort,
|
|
2611
|
+
effectiveReasoningEffort: request.thread.effectiveReasoningEffort,
|
|
2612
|
+
adapterRequestId: String(requestId),
|
|
2613
|
+
adapterResponseId: responseIdentity,
|
|
2614
|
+
usage,
|
|
2615
|
+
latencyMs: Math.max(0, Date.now() - startedAt),
|
|
2616
|
+
timeToFirstTokenMs: firstTokenAt === null ? null : Math.max(0, firstTokenAt - startedAt),
|
|
2617
|
+
terminalStatus: "completed"
|
|
2618
|
+
};
|
|
2619
|
+
return agentMode ? {
|
|
2620
|
+
...result,
|
|
2621
|
+
toolCalls: agentToolCalls,
|
|
2622
|
+
webSearches: agentWebSearches
|
|
2623
|
+
} : result;
|
|
2624
|
+
} finally {
|
|
2625
|
+
if (agentMode) this.agentToolHandlers.delete(request.thread.threadId);
|
|
2626
|
+
unsubscribe();
|
|
2627
|
+
unsubscribeFatal();
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
async interrupt(threadId, turnId, timeoutMs = 5e3) {
|
|
2631
|
+
const result = await this.request("turn/interrupt", { threadId, turnId }, { timeoutMs });
|
|
2632
|
+
if (Object.keys(result).length !== 0) {
|
|
2633
|
+
throw new CodexAppServerError({
|
|
2634
|
+
message: "Codex interrupt response was invalid.",
|
|
2635
|
+
category: "adapter",
|
|
2636
|
+
code: "invalid_interrupt_response",
|
|
2637
|
+
retryable: false,
|
|
2638
|
+
dispatchOutcome: "confirmed_dispatched"
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
async deleteThread(threadId) {
|
|
2643
|
+
const threadPath = this.threads.get(threadId);
|
|
2644
|
+
if (!threadPath) {
|
|
2645
|
+
throw new CodexAppServerError({
|
|
2646
|
+
message: "Codex thread cleanup identity was unavailable.",
|
|
2647
|
+
category: "adapter",
|
|
2648
|
+
code: "thread_cleanup_identity_missing",
|
|
2649
|
+
retryable: false
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
const result = await this.request("thread/delete", { threadId }, { timeoutMs: 3e4 });
|
|
2653
|
+
if (Object.keys(result).length !== 0) {
|
|
2654
|
+
throw new CodexAppServerError({
|
|
2655
|
+
message: "Codex thread deletion was not confirmed.",
|
|
2656
|
+
category: "adapter",
|
|
2657
|
+
code: "thread_delete_unconfirmed",
|
|
2658
|
+
retryable: false
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
const loaded = await this.request("thread/loaded/list", {}, { timeoutMs: 3e4 });
|
|
2662
|
+
const data = Array.isArray(loaded.data) ? loaded.data : null;
|
|
2663
|
+
if (data === null || loaded.nextCursor != null || data.some((item) => String(objectOrNull(item)?.id ?? item) === threadId)) {
|
|
2664
|
+
throw new CodexAppServerError({
|
|
2665
|
+
message: "Codex thread remained loaded after deletion.",
|
|
2666
|
+
category: "adapter",
|
|
2667
|
+
code: "thread_delete_unconfirmed",
|
|
2668
|
+
retryable: false
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
try {
|
|
2672
|
+
await lstat2(threadPath);
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
if (error.code === "ENOENT") {
|
|
2675
|
+
this.threads.delete(threadId);
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
throw error;
|
|
2679
|
+
}
|
|
2680
|
+
throw new CodexAppServerError({
|
|
2681
|
+
message: "Codex persisted thread remained after deletion.",
|
|
2682
|
+
category: "adapter",
|
|
2683
|
+
code: "thread_delete_unconfirmed",
|
|
2684
|
+
retryable: false
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
async close() {
|
|
2688
|
+
if (this.closed) return;
|
|
2689
|
+
this.closed = true;
|
|
2690
|
+
if (this.processCloseObserved || this.process.exitCode !== null) {
|
|
2691
|
+
if (this.guardian) {
|
|
2692
|
+
await rm2(`${this.guardian.receiptPath}.shutdown-intent`, { force: true }).catch(() => void 0);
|
|
2693
|
+
}
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
this.shutdownRequested = true;
|
|
2697
|
+
if (this.guardian) {
|
|
2698
|
+
await writeFile(
|
|
2699
|
+
`${this.guardian.receiptPath}.shutdown-intent`,
|
|
2700
|
+
this.guardian.processToken,
|
|
2701
|
+
{ mode: 384 }
|
|
2702
|
+
).catch(() => void 0);
|
|
2703
|
+
}
|
|
2704
|
+
this.process.stdin.end();
|
|
2705
|
+
const exited = await new Promise((resolve3) => {
|
|
2706
|
+
if (this.process.exitCode !== null) {
|
|
2707
|
+
resolve3(true);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
const timer = setTimeout(() => resolve3(false), 5e3);
|
|
2711
|
+
this.process.once("close", () => {
|
|
2712
|
+
clearTimeout(timer);
|
|
2713
|
+
resolve3(true);
|
|
2714
|
+
});
|
|
2715
|
+
});
|
|
2716
|
+
if (exited) return;
|
|
2717
|
+
const pid = this.process.pid;
|
|
2718
|
+
try {
|
|
2719
|
+
if (pid && process.platform === "win32") await killWindowsProcessTree(pid);
|
|
2720
|
+
else if (pid) process.kill(-pid, "SIGTERM");
|
|
2721
|
+
else this.process.kill("SIGTERM");
|
|
2722
|
+
} catch {
|
|
2723
|
+
}
|
|
2724
|
+
const terminated = await new Promise((resolve3) => {
|
|
2725
|
+
const timer = setTimeout(() => resolve3(false), 1e3);
|
|
2726
|
+
this.process.once("close", () => {
|
|
2727
|
+
clearTimeout(timer);
|
|
2728
|
+
resolve3(true);
|
|
2729
|
+
});
|
|
2730
|
+
});
|
|
2731
|
+
if (!terminated) {
|
|
2732
|
+
try {
|
|
2733
|
+
if (pid && process.platform === "win32") await killWindowsProcessTree(pid);
|
|
2734
|
+
else if (pid) process.kill(-pid, "SIGKILL");
|
|
2735
|
+
else this.process.kill("SIGKILL");
|
|
2736
|
+
} catch {
|
|
2737
|
+
}
|
|
2738
|
+
await new Promise((resolve3) => {
|
|
2739
|
+
if (this.process.exitCode !== null) {
|
|
2740
|
+
resolve3();
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
const timer = setTimeout(resolve3, 1e3);
|
|
2744
|
+
this.process.once("close", () => {
|
|
2745
|
+
clearTimeout(timer);
|
|
2746
|
+
resolve3();
|
|
2747
|
+
});
|
|
2748
|
+
});
|
|
2749
|
+
if (this.process.exitCode === null) {
|
|
2750
|
+
throw new CodexAppServerError({
|
|
2751
|
+
message: "Codex process-tree cleanup could not be confirmed.",
|
|
2752
|
+
category: "adapter",
|
|
2753
|
+
code: "process_tree_cleanup_unconfirmed",
|
|
2754
|
+
retryable: false
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
|
|
2761
|
+
// lib/inference-host/managed-codex-wrapper.ts
|
|
2762
|
+
var object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2763
|
+
var nonnegative = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
2764
|
+
var ManagedCodexWrapperFailure = class extends Error {
|
|
2765
|
+
constructor(code, dispatchOutcome) {
|
|
2766
|
+
super(code);
|
|
2767
|
+
this.dispatchOutcome = dispatchOutcome;
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
async function runManagedCodexWrapper(binary) {
|
|
2771
|
+
if (!isAbsolute3(binary) || binary.includes("\0")) throw new ManagedCodexWrapperFailure("codex_binary_must_be_absolute", "not_dispatched");
|
|
2772
|
+
const host = createInterface2({ input: process.stdin, crlfDelay: Infinity });
|
|
2773
|
+
const frames = host[Symbol.asyncIterator]();
|
|
2774
|
+
const first = await frames.next();
|
|
2775
|
+
if (first.done) throw new ManagedCodexWrapperFailure("missing_turn", "not_dispatched");
|
|
2776
|
+
let turn;
|
|
2777
|
+
try {
|
|
2778
|
+
turn = object(JSON.parse(first.value));
|
|
2779
|
+
} catch {
|
|
2780
|
+
host.close();
|
|
2781
|
+
throw new ManagedCodexWrapperFailure("invalid_turn", "not_dispatched");
|
|
2782
|
+
}
|
|
2783
|
+
if (turn.schema_version !== "vtx_managed_harness_turn_v1" || turn.type !== "turn" || typeof turn.requested_model !== "string" || typeof turn.requested_reasoning_effort !== "string" || typeof turn.system_prompt !== "string" || typeof turn.user_prompt !== "string" || !Array.isArray(turn.data_contract)) {
|
|
2784
|
+
host.close();
|
|
2785
|
+
throw new ManagedCodexWrapperFailure("invalid_turn", "not_dispatched");
|
|
2786
|
+
}
|
|
2787
|
+
const deadline = Date.parse(String(turn.deadline_at));
|
|
2788
|
+
if (!Number.isFinite(deadline) || deadline <= Date.now()) {
|
|
2789
|
+
host.close();
|
|
2790
|
+
throw new ManagedCodexWrapperFailure("expired_turn", "not_dispatched");
|
|
2791
|
+
}
|
|
2792
|
+
const child = spawn3(binary, ["app-server", "--stdio"], {
|
|
2793
|
+
cwd: process.cwd(),
|
|
2794
|
+
env: process.env,
|
|
2795
|
+
shell: false,
|
|
2796
|
+
windowsHide: true,
|
|
2797
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2798
|
+
});
|
|
2799
|
+
const nativeClosed = new Promise((resolve3) => {
|
|
2800
|
+
child.once("close", () => resolve3());
|
|
2801
|
+
});
|
|
2802
|
+
let nativeDispatchAttempted = false;
|
|
2803
|
+
let terminalError = null;
|
|
2804
|
+
const native = createInterface2({ input: child.stdout, crlfDelay: Infinity });
|
|
2805
|
+
child.stderr.on("data", () => void 0);
|
|
2806
|
+
const sendHost = (frame) => {
|
|
2807
|
+
process.stdout.write(`${JSON.stringify(frame)}
|
|
2808
|
+
`);
|
|
2809
|
+
};
|
|
2810
|
+
const sendNative = (frame) => {
|
|
2811
|
+
child.stdin.write(`${JSON.stringify(frame)}
|
|
2812
|
+
`);
|
|
2813
|
+
};
|
|
2814
|
+
let requestId = 0;
|
|
2815
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2816
|
+
const tools = /* @__PURE__ */ new Map();
|
|
2817
|
+
let failed = null;
|
|
2818
|
+
let failTurn;
|
|
2819
|
+
const failure = new Promise((_resolve, reject) => {
|
|
2820
|
+
failTurn = reject;
|
|
2821
|
+
});
|
|
2822
|
+
void failure.catch(() => void 0);
|
|
2823
|
+
const fail = (code) => {
|
|
2824
|
+
if (failed) return;
|
|
2825
|
+
failed = new Error(code);
|
|
2826
|
+
for (const item of pending.values()) item.reject(failed);
|
|
2827
|
+
pending.clear();
|
|
2828
|
+
failTurn(failed);
|
|
2829
|
+
};
|
|
2830
|
+
const request = (method, params) => {
|
|
2831
|
+
if (failed) return Promise.reject(failed);
|
|
2832
|
+
const id = ++requestId;
|
|
2833
|
+
return new Promise((resolve3, reject) => {
|
|
2834
|
+
pending.set(id, { resolve: resolve3, reject });
|
|
2835
|
+
sendNative({ id, method, params });
|
|
2836
|
+
});
|
|
2837
|
+
};
|
|
2838
|
+
let threadId = null;
|
|
2839
|
+
let nativeTurnId = null;
|
|
2840
|
+
let nativeTurnRequestId = null;
|
|
2841
|
+
let nativeResponseId = null;
|
|
2842
|
+
let acknowledged = false;
|
|
2843
|
+
let finalText = "";
|
|
2844
|
+
let usage = null;
|
|
2845
|
+
let completed;
|
|
2846
|
+
const terminal = new Promise((resolve3) => {
|
|
2847
|
+
completed = resolve3;
|
|
2848
|
+
});
|
|
2849
|
+
const timer = setTimeout(() => fail("codex_deadline_exceeded"), deadline - Date.now());
|
|
2850
|
+
child.on("error", () => fail("codex_process_start_failed"));
|
|
2851
|
+
child.stdin.on("error", () => fail("codex_input_failed"));
|
|
2852
|
+
child.on("close", () => {
|
|
2853
|
+
if (!finalText) fail("codex_process_exited");
|
|
2854
|
+
});
|
|
2855
|
+
const scoped = (params) => !params.threadId || threadId === null || params.threadId === threadId;
|
|
2856
|
+
native.on("line", (line) => {
|
|
2857
|
+
try {
|
|
2858
|
+
const message = object(JSON.parse(line));
|
|
2859
|
+
if (typeof message.id === "number" && !message.method) {
|
|
2860
|
+
const waiter = pending.get(message.id);
|
|
2861
|
+
if (!waiter) {
|
|
2862
|
+
fail("codex_unknown_response");
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
pending.delete(message.id);
|
|
2866
|
+
if (message.error) waiter.reject(new Error("codex_request_failed"));
|
|
2867
|
+
else waiter.resolve(object(message.result));
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const params = object(message.params);
|
|
2871
|
+
if (!scoped(params)) {
|
|
2872
|
+
fail("codex_thread_scope_changed");
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (message.method === "item/tool/call" && message.id !== void 0) {
|
|
2876
|
+
if (!acknowledged || !threadId || params.threadId !== threadId || typeof params.callId !== "string" || tools.has(params.callId) || !CODEX_AGENT_DYNAMIC_TOOLS.includes(params.tool) || nativeTurnId && params.turnId !== nativeTurnId || !(typeof message.id === "number" || typeof message.id === "string")) {
|
|
2877
|
+
fail("codex_invalid_tool_request");
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
tools.set(params.callId, message.id);
|
|
2881
|
+
sendHost({ type: "tool_call", call_id: params.callId, tool: params.tool, arguments: params.arguments });
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
if (message.id !== void 0) {
|
|
2885
|
+
fail("codex_interactive_request_required");
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (message.method === "model/rerouted") {
|
|
2889
|
+
fail("codex_model_rerouted");
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (message.method === "thread/settings/updated") {
|
|
2893
|
+
const settings = object(params.threadSettings);
|
|
2894
|
+
if (settings.model !== turn.requested_model || settings.effort !== turn.requested_reasoning_effort || settings.modelProvider !== "openai") {
|
|
2895
|
+
fail("codex_effective_selection_changed");
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (message.method === "turn/started") {
|
|
2900
|
+
const started = object(params.turn);
|
|
2901
|
+
if (typeof started.id === "string") nativeTurnId = started.id;
|
|
2902
|
+
}
|
|
2903
|
+
if (message.method === "item/completed") {
|
|
2904
|
+
const item = object(params.item);
|
|
2905
|
+
if (item.type === "agentMessage" && typeof item.text === "string") finalText = item.text;
|
|
2906
|
+
}
|
|
2907
|
+
if (message.method === "thread/tokenUsage/updated") {
|
|
2908
|
+
const tokens = object(object(params.tokenUsage).last);
|
|
2909
|
+
const names = ["inputTokens", "cachedInputTokens", "outputTokens", "reasoningOutputTokens", "totalTokens"];
|
|
2910
|
+
if (!names.every((key) => nonnegative(tokens[key])) || Number(tokens.cachedInputTokens) > Number(tokens.inputTokens) || Number(tokens.reasoningOutputTokens) > Number(tokens.outputTokens) || Number(tokens.totalTokens) !== Number(tokens.inputTokens) + Number(tokens.outputTokens)) {
|
|
2911
|
+
fail("codex_usage_unavailable");
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
usage = Object.fromEntries(names.map((key) => [key, tokens[key]]));
|
|
2915
|
+
usage.cacheWriteInputTokens = null;
|
|
2916
|
+
usage.cacheWriteSupported = false;
|
|
2917
|
+
}
|
|
2918
|
+
if (message.method === "rawResponse/completed" && typeof params.responseId === "string") nativeResponseId = params.responseId;
|
|
2919
|
+
if (message.method === "turn/completed") {
|
|
2920
|
+
const result = object(params.turn);
|
|
2921
|
+
if (result.status !== "completed") {
|
|
2922
|
+
const info = object(result.error).codexErrorInfo;
|
|
2923
|
+
const categories = {
|
|
2924
|
+
contextWindowExceeded: "context_window_exceeded",
|
|
2925
|
+
sessionBudgetExceeded: "session_budget_exceeded",
|
|
2926
|
+
usageLimitExceeded: "usage_limit_exceeded",
|
|
2927
|
+
serverOverloaded: "server_overloaded",
|
|
2928
|
+
cyberPolicy: "policy_rejected",
|
|
2929
|
+
internalServerError: "internal_server_error",
|
|
2930
|
+
unauthorized: "unauthorized",
|
|
2931
|
+
badRequest: "bad_request",
|
|
2932
|
+
sandboxError: "sandbox_error",
|
|
2933
|
+
other: "other"
|
|
2934
|
+
};
|
|
2935
|
+
const category = typeof info === "string" ? categories[info] : [
|
|
2936
|
+
"httpConnectionFailed",
|
|
2937
|
+
"responseStreamConnectionFailed",
|
|
2938
|
+
"responseStreamDisconnected",
|
|
2939
|
+
"responseTooManyFailedAttempts"
|
|
2940
|
+
].find((key) => key in object(info))?.replace(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
|
|
2941
|
+
fail(`codex_turn_${category ?? "failed"}`);
|
|
2942
|
+
} else if (!nativeTurnId || result.id !== nativeTurnId) fail("codex_turn_identity_unverified");
|
|
2943
|
+
else completed();
|
|
2944
|
+
}
|
|
2945
|
+
} catch {
|
|
2946
|
+
fail("codex_protocol_invalid");
|
|
2947
|
+
}
|
|
2948
|
+
});
|
|
2949
|
+
try {
|
|
2950
|
+
await request("initialize", {
|
|
2951
|
+
clientInfo: { name: "vtx_managed_agent", version: "1" },
|
|
2952
|
+
capabilities: { experimentalApi: true }
|
|
2953
|
+
});
|
|
2954
|
+
sendNative({ method: "initialized", params: {} });
|
|
2955
|
+
let selected;
|
|
2956
|
+
let cursor;
|
|
2957
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
2958
|
+
do {
|
|
2959
|
+
const catalog = await request("model/list", { ...cursor ? { cursor } : {} });
|
|
2960
|
+
selected = Array.isArray(catalog.data) ? catalog.data.map(object).find((model) => model.model === turn.requested_model || model.id === turn.requested_model) : void 0;
|
|
2961
|
+
cursor = typeof catalog.nextCursor === "string" && catalog.nextCursor ? catalog.nextCursor : void 0;
|
|
2962
|
+
if (cursor && seenCursors.has(cursor)) throw new Error("codex_catalog_cursor_repeated");
|
|
2963
|
+
if (cursor) seenCursors.add(cursor);
|
|
2964
|
+
} while (!selected && cursor);
|
|
2965
|
+
if (!selected || !Array.isArray(selected.supportedReasoningEfforts) || !selected.supportedReasoningEfforts.some((entry) => object(entry).reasoningEffort === turn.requested_reasoning_effort)) {
|
|
2966
|
+
throw new Error("codex_selection_not_available");
|
|
2967
|
+
}
|
|
2968
|
+
const threadParams = {
|
|
2969
|
+
model: turn.requested_model,
|
|
2970
|
+
modelProvider: "openai",
|
|
2971
|
+
allowProviderModelFallback: false,
|
|
2972
|
+
cwd: process.cwd(),
|
|
2973
|
+
developerInstructions: turn.system_prompt,
|
|
2974
|
+
config: { model_reasoning_effort: turn.requested_reasoning_effort }
|
|
2975
|
+
};
|
|
2976
|
+
const started = turn.resume_id ? await request("thread/resume", { ...threadParams, threadId: turn.resume_id }) : await request("thread/start", {
|
|
2977
|
+
...threadParams,
|
|
2978
|
+
experimentalRawEvents: false,
|
|
2979
|
+
dynamicTools: createCodexAgentToolSpecs(turn.data_contract, object(turn.decision_schema))
|
|
2980
|
+
});
|
|
2981
|
+
const nativeThread = object(started.thread);
|
|
2982
|
+
if (started.model !== turn.requested_model || started.reasoningEffort !== turn.requested_reasoning_effort || started.modelProvider !== "openai" || typeof nativeThread.id !== "string" || turn.resume_id && nativeThread.id !== turn.resume_id) throw new Error("codex_thread_selection_unverified");
|
|
2983
|
+
threadId = nativeThread.id;
|
|
2984
|
+
sendHost({
|
|
2985
|
+
type: "thread_ready",
|
|
2986
|
+
resume_id: threadId,
|
|
2987
|
+
effective_model: started.model,
|
|
2988
|
+
effective_reasoning_effort: started.reasoningEffort
|
|
2989
|
+
});
|
|
2990
|
+
const ack = await Promise.race([frames.next(), failure]);
|
|
2991
|
+
if (ack.done) throw new Error("host_disconnected");
|
|
2992
|
+
const acknowledgement = object(JSON.parse(ack.value));
|
|
2993
|
+
if (acknowledgement.type !== "thread_ready" || acknowledgement.acknowledged !== true || acknowledgement.resume_id !== threadId) throw new Error("thread_not_checkpointed");
|
|
2994
|
+
acknowledged = true;
|
|
2995
|
+
const hostReplies = (async () => {
|
|
2996
|
+
while (true) {
|
|
2997
|
+
const next = await frames.next();
|
|
2998
|
+
if (next.done) return;
|
|
2999
|
+
const result = object(JSON.parse(next.value));
|
|
3000
|
+
const id = tools.get(String(result.call_id));
|
|
3001
|
+
if (result.type !== "tool_result" || id === void 0) {
|
|
3002
|
+
fail("host_tool_result_invalid");
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
tools.delete(String(result.call_id));
|
|
3006
|
+
sendNative({ id, result: {
|
|
3007
|
+
contentItems: [{ type: "inputText", text: JSON.stringify(result.value ?? null) }],
|
|
3008
|
+
success: result.success === true
|
|
3009
|
+
} });
|
|
3010
|
+
}
|
|
3011
|
+
})();
|
|
3012
|
+
void hostReplies.catch(() => fail("host_protocol_failed"));
|
|
3013
|
+
nativeTurnRequestId = requestId + 1;
|
|
3014
|
+
nativeDispatchAttempted = true;
|
|
3015
|
+
const begun = await request("turn/start", {
|
|
3016
|
+
threadId,
|
|
3017
|
+
input: [{ type: "text", text: turn.user_prompt, text_elements: [] }],
|
|
3018
|
+
model: turn.requested_model,
|
|
3019
|
+
effort: turn.requested_reasoning_effort,
|
|
3020
|
+
outputSchema: turn.output_schema
|
|
3021
|
+
});
|
|
3022
|
+
const returnedTurnId = object(begun.turn).id;
|
|
3023
|
+
if (typeof returnedTurnId !== "string" || nativeTurnId && returnedTurnId !== nativeTurnId) throw new Error("codex_turn_identity_unverified");
|
|
3024
|
+
nativeTurnId = returnedTurnId;
|
|
3025
|
+
await Promise.race([terminal, failure]);
|
|
3026
|
+
if (!usage) throw new Error("codex_usage_unavailable");
|
|
3027
|
+
if (!finalText) throw new Error("codex_final_text_unavailable");
|
|
3028
|
+
if (tools.size) throw new Error("codex_tool_outcome_unresolved");
|
|
3029
|
+
sendHost({
|
|
3030
|
+
type: "complete",
|
|
3031
|
+
text: finalText,
|
|
3032
|
+
requested_model: turn.requested_model,
|
|
3033
|
+
effective_model: started.model,
|
|
3034
|
+
requested_reasoning_effort: turn.requested_reasoning_effort,
|
|
3035
|
+
effective_reasoning_effort: started.reasoningEffort,
|
|
3036
|
+
adapter_request_id: String(nativeTurnRequestId),
|
|
3037
|
+
adapter_response_id: nativeResponseId ?? nativeTurnId,
|
|
3038
|
+
usage
|
|
3039
|
+
});
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
terminalError = error;
|
|
3042
|
+
} finally {
|
|
3043
|
+
clearTimeout(timer);
|
|
3044
|
+
host.close();
|
|
3045
|
+
native.close();
|
|
3046
|
+
child.stdin.end();
|
|
3047
|
+
child.kill("SIGTERM");
|
|
3048
|
+
const waitForClose = async () => {
|
|
3049
|
+
let timeout;
|
|
3050
|
+
try {
|
|
3051
|
+
return await Promise.race([
|
|
3052
|
+
nativeClosed.then(() => true),
|
|
3053
|
+
new Promise((resolve3) => {
|
|
3054
|
+
timeout = setTimeout(() => resolve3(false), 1e3);
|
|
3055
|
+
})
|
|
3056
|
+
]);
|
|
3057
|
+
} finally {
|
|
3058
|
+
clearTimeout(timeout);
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
let closed = await waitForClose();
|
|
3062
|
+
if (!closed) {
|
|
3063
|
+
child.kill("SIGKILL");
|
|
3064
|
+
closed = await waitForClose();
|
|
3065
|
+
}
|
|
3066
|
+
if (!closed) throw new ManagedCodexWrapperFailure("codex_teardown_unconfirmed", "outcome_unknown");
|
|
3067
|
+
}
|
|
3068
|
+
if (terminalError !== null) {
|
|
3069
|
+
const code = terminalError instanceof Error && /^[a-z_]{1,80}$/u.test(terminalError.message) ? terminalError.message : "native_terminal_evidence_unconfirmed";
|
|
3070
|
+
throw new ManagedCodexWrapperFailure(code, nativeDispatchAttempted ? "outcome_unknown" : "not_dispatched");
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// bin/vtx-managed-codex.ts
|
|
3075
|
+
runManagedCodexWrapper(process.argv[2] ?? "").catch((error) => {
|
|
3076
|
+
const code = error instanceof Error && /^[a-z_]{1,80}$/u.test(error.message) ? error.message : "native_terminal_evidence_unconfirmed";
|
|
3077
|
+
process.stdout.write(`${JSON.stringify({
|
|
3078
|
+
type: "failed",
|
|
3079
|
+
code,
|
|
3080
|
+
...error instanceof ManagedCodexWrapperFailure ? { dispatch_outcome: error.dispatchOutcome } : {}
|
|
3081
|
+
})}
|
|
3082
|
+
`);
|
|
3083
|
+
process.stderr.write(`VTX managed Codex turn failed: ${code}.
|
|
3084
|
+
`);
|
|
3085
|
+
process.exitCode = 1;
|
|
3086
|
+
});
|
|
3087
|
+
/*! Bundled license information:
|
|
3088
|
+
|
|
3089
|
+
smol-toml/dist/date.js:
|
|
3090
|
+
smol-toml/dist/error.js:
|
|
3091
|
+
smol-toml/dist/util.js:
|
|
3092
|
+
smol-toml/dist/primitive.js:
|
|
3093
|
+
smol-toml/dist/extract.js:
|
|
3094
|
+
smol-toml/dist/struct.js:
|
|
3095
|
+
smol-toml/dist/parse.js:
|
|
3096
|
+
smol-toml/dist/stringify.js:
|
|
3097
|
+
smol-toml/dist/index.js:
|
|
3098
|
+
(*!
|
|
3099
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
3100
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
3101
|
+
*
|
|
3102
|
+
* Redistribution and use in source and binary forms, with or without
|
|
3103
|
+
* modification, are permitted provided that the following conditions are met:
|
|
3104
|
+
*
|
|
3105
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
3106
|
+
* list of conditions and the following disclaimer.
|
|
3107
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
3108
|
+
* this list of conditions and the following disclaimer in the
|
|
3109
|
+
* documentation and/or other materials provided with the distribution.
|
|
3110
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
3111
|
+
* may be used to endorse or promote products derived from this software without
|
|
3112
|
+
* specific prior written permission.
|
|
3113
|
+
*
|
|
3114
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
3115
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
3116
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
3117
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
3118
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
3119
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
3120
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
3121
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
3122
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
3123
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
3124
|
+
*)
|
|
3125
|
+
*/
|