@hraness/xcb 0.9.1
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/LICENSE +21 -0
- package/MANAGED-CODEX.md +213 -0
- package/README.md +555 -0
- package/dist/accounts.d.ts +46 -0
- package/dist/broker-descriptors.d.ts +5 -0
- package/dist/broker.d.ts +62 -0
- package/dist/browser-session.d.ts +161 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/capabilities.d.ts +72 -0
- package/dist/claude-api-models.d.ts +24 -0
- package/dist/claude-api-transport.d.ts +5 -0
- package/dist/claude-api.d.ts +23 -0
- package/dist/claude-credentials.d.ts +13 -0
- package/dist/claude-options.d.ts +10 -0
- package/dist/claude-sdk.d.ts +48 -0
- package/dist/claude-task-adapter.d.ts +65 -0
- package/dist/cli.js +4190 -0
- package/dist/codex-account-process.d.ts +149 -0
- package/dist/codex-account-transport.d.ts +39 -0
- package/dist/codex-account.d.ts +126 -0
- package/dist/codex-config.d.ts +53 -0
- package/dist/codex-host.d.ts +40 -0
- package/dist/codex-managed-baseline.d.ts +5 -0
- package/dist/codex-managed-catalog.d.ts +32 -0
- package/dist/codex-managed-config.d.ts +93 -0
- package/dist/codex-managed-ledger.d.ts +37 -0
- package/dist/codex-managed-session.d.ts +62 -0
- package/dist/codex-managed-task-adapter.d.ts +21 -0
- package/dist/codex-process.d.ts +67 -0
- package/dist/codex-protocol-manifest.d.ts +27 -0
- package/dist/codex-relay.d.ts +80 -0
- package/dist/codex-scratch.d.ts +36 -0
- package/dist/codex-session.d.ts +44 -0
- package/dist/codex-task-adapter.d.ts +24 -0
- package/dist/codex-task-process.d.ts +21 -0
- package/dist/devin-acp.d.ts +105 -0
- package/dist/devin-adapter.d.ts +44 -0
- package/dist/devin-client.d.ts +36 -0
- package/dist/devin-mcp.d.ts +28 -0
- package/dist/egress-bridge.d.ts +35 -0
- package/dist/egress-client.d.ts +66 -0
- package/dist/index-kg2gx694.js +7217 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +3455 -0
- package/dist/judge.d.ts +121 -0
- package/dist/loopback-server.d.ts +17 -0
- package/dist/managed-account.d.ts +176 -0
- package/dist/models.d.ts +23 -0
- package/dist/os-sandbox.d.ts +168 -0
- package/dist/private-file.d.ts +215 -0
- package/dist/process-port.d.ts +44 -0
- package/dist/process-write.d.ts +13 -0
- package/dist/provider-process.d.ts +26 -0
- package/dist/public-web.d.ts +21 -0
- package/dist/router.d.ts +43 -0
- package/dist/runtime.d.ts +90 -0
- package/dist/sqlite-port.d.ts +29 -0
- package/dist/task-runtime.d.ts +150 -0
- package/dist/validation.d.ts +6 -0
- package/package.json +70 -0
- package/sandbox/loopback-forwarder.cjs +172 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CLAUDE_CODE_MIN_VERSION,
|
|
4
|
+
JUDGE_KEY_ENV,
|
|
5
|
+
JUDGE_KEY_VENDOR_ENV,
|
|
6
|
+
JUDGE_TOKEN_FILE,
|
|
7
|
+
JUDGE_URL_ENV,
|
|
8
|
+
PRIVATE_CONTROL_REJECT,
|
|
9
|
+
SqliteAccountLeases,
|
|
10
|
+
__require,
|
|
11
|
+
assertAgentTaskAccountLease,
|
|
12
|
+
assertCodexHostFileStable,
|
|
13
|
+
assertCodexManagedConfigResponse,
|
|
14
|
+
assertCodexProtocolManifest,
|
|
15
|
+
assertPrivateDirectory,
|
|
16
|
+
assertPrivateStat,
|
|
17
|
+
boundedText,
|
|
18
|
+
buildCodexProtocolManifest,
|
|
19
|
+
canonicalizePrivatePath,
|
|
20
|
+
checkJudgeKeyTarget,
|
|
21
|
+
claudeCodeVersionAdmitted,
|
|
22
|
+
claudeTaskRuntimeIdentity,
|
|
23
|
+
codexManagedAccountConfiguration,
|
|
24
|
+
createBwrapOsSandbox,
|
|
25
|
+
createCapabilityBroker,
|
|
26
|
+
createCapabilityProfile,
|
|
27
|
+
createClaudeTaskAdapter,
|
|
28
|
+
createCodexAccountProcess,
|
|
29
|
+
createCodexAccountStdioTransport,
|
|
30
|
+
createCodexManagedTaskAdapter,
|
|
31
|
+
createEgressBridge,
|
|
32
|
+
createManagedCodexAccountController,
|
|
33
|
+
createPublicWeb,
|
|
34
|
+
createSandboxedProviderProcessFactory,
|
|
35
|
+
createSeatbeltOsSandbox,
|
|
36
|
+
egressBridgeDialer,
|
|
37
|
+
fsyncDirectory,
|
|
38
|
+
hasJudgeKey,
|
|
39
|
+
identifier,
|
|
40
|
+
inspectCodexHostExecutable,
|
|
41
|
+
inspectCodexHostRuntime,
|
|
42
|
+
matchesPrivateStat,
|
|
43
|
+
openAccountDatabase,
|
|
44
|
+
openPrivateRead,
|
|
45
|
+
openPrivateWrite,
|
|
46
|
+
openPrivateWriteSync,
|
|
47
|
+
planBwrapPolicy,
|
|
48
|
+
planSeatbeltPolicy,
|
|
49
|
+
providerProcessWriteResult,
|
|
50
|
+
readExactPrivateFile,
|
|
51
|
+
removeJudgeKey,
|
|
52
|
+
resolveJudge,
|
|
53
|
+
resolveJudgeKey,
|
|
54
|
+
runAgentTask,
|
|
55
|
+
safeInteger,
|
|
56
|
+
sameFileIdentity,
|
|
57
|
+
storeJudgeKey,
|
|
58
|
+
streamFdContent,
|
|
59
|
+
verifyOsSandboxExecutable,
|
|
60
|
+
writeFileOnce
|
|
61
|
+
} from "./index-kg2gx694.js";
|
|
62
|
+
|
|
63
|
+
// src/cli.ts
|
|
64
|
+
import { resolve as resolve3 } from "node:path";
|
|
65
|
+
import { join as join14 } from "node:path";
|
|
66
|
+
|
|
67
|
+
// src/cli/state.ts
|
|
68
|
+
import { constants } from "node:fs";
|
|
69
|
+
import { chmod, copyFile, lstat, mkdir, readdir } from "node:fs/promises";
|
|
70
|
+
import { homedir } from "node:os";
|
|
71
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
72
|
+
var CLI_STATE_ENV = "XCB_STATE";
|
|
73
|
+
var STATE_DIRNAME = ".xcb";
|
|
74
|
+
var LEGACY_STATE_ENV = "AGENTMIXER_STATE";
|
|
75
|
+
var LEGACY_STATE_DIRNAME = ".agentmixer";
|
|
76
|
+
var MAX_MIGRATION_ENTRIES = 16384;
|
|
77
|
+
var MAX_MIGRATION_BYTES = 4 * 1024 * 1024 * 1024;
|
|
78
|
+
function cliStateRootPath(env = (name) => process.env[name]) {
|
|
79
|
+
const override = env(CLI_STATE_ENV);
|
|
80
|
+
if (override !== undefined) {
|
|
81
|
+
return canonicalizePrivatePath(override, { code: "XCB_STATE_INVALID", reject: PRIVATE_CONTROL_REJECT, maxLength: Infinity });
|
|
82
|
+
}
|
|
83
|
+
const home = homedir();
|
|
84
|
+
if (typeof home !== "string" || !isAbsolute(home))
|
|
85
|
+
throw new Error("XCB_HOME_UNAVAILABLE");
|
|
86
|
+
return join(home, STATE_DIRNAME);
|
|
87
|
+
}
|
|
88
|
+
async function privateDirectory(path) {
|
|
89
|
+
canonicalizePrivatePath(path, { code: "XCB_DIRECTORY_INVALID", reject: PRIVATE_CONTROL_REJECT, maxLength: Infinity });
|
|
90
|
+
return (await assertPrivateDirectory(path, {
|
|
91
|
+
code: "XCB_DIRECTORY_NOT_PRIVATE",
|
|
92
|
+
owner: "self",
|
|
93
|
+
mode: "ownerOnly",
|
|
94
|
+
canonical: "self",
|
|
95
|
+
statOrder: "realpathFirst",
|
|
96
|
+
stats: "number"
|
|
97
|
+
})).physical;
|
|
98
|
+
}
|
|
99
|
+
async function ensureCliState(child) {
|
|
100
|
+
const root = await mkdir(cliStateRootPath(), { mode: 448, recursive: true }).then(async () => await privateDirectory(cliStateRootPath()));
|
|
101
|
+
if (child === undefined)
|
|
102
|
+
return { root, path: root };
|
|
103
|
+
const name = boundedText(child, 64);
|
|
104
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(name))
|
|
105
|
+
throw new Error("XCB_STATE_CHILD_INVALID");
|
|
106
|
+
const path = join(root, name);
|
|
107
|
+
await mkdir(path, { mode: 448, recursive: true });
|
|
108
|
+
return { root, path: await privateDirectory(path) };
|
|
109
|
+
}
|
|
110
|
+
function assertWorkspaceStateSeparation(workspace, stateRoot) {
|
|
111
|
+
const contains = (parent, child) => {
|
|
112
|
+
const path = relative(parent, child);
|
|
113
|
+
return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
114
|
+
};
|
|
115
|
+
if (contains(workspace, stateRoot) || contains(stateRoot, workspace)) {
|
|
116
|
+
throw new Error("workspace overlaps private xcb state; choose a separate workspace or state root");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function legacyStateRootPath(env = (name) => process.env[name]) {
|
|
120
|
+
const override = env(LEGACY_STATE_ENV);
|
|
121
|
+
let candidate = join(homedir(), LEGACY_STATE_DIRNAME);
|
|
122
|
+
if (override !== undefined) {
|
|
123
|
+
candidate = canonicalizePrivatePath(override, { code: "AGENTMIXER_STATE_INVALID", reject: PRIVATE_CONTROL_REJECT, maxLength: Infinity });
|
|
124
|
+
}
|
|
125
|
+
const stat = await lstat(candidate).catch(() => null);
|
|
126
|
+
if (stat === null)
|
|
127
|
+
return null;
|
|
128
|
+
return await privateDirectory(candidate);
|
|
129
|
+
}
|
|
130
|
+
async function migrateLegacyState(targetRoot, env = (name) => process.env[name]) {
|
|
131
|
+
const source = await legacyStateRootPath(env);
|
|
132
|
+
if (source === null)
|
|
133
|
+
throw new Error("XCB_LEGACY_STATE_ABSENT");
|
|
134
|
+
const target = await privateDirectory(targetRoot);
|
|
135
|
+
if (source === target)
|
|
136
|
+
throw new Error("XCB_LEGACY_STATE_SAME_AS_TARGET");
|
|
137
|
+
if ((await readdir(target)).length !== 0)
|
|
138
|
+
throw new Error("XCB_STATE_TARGET_NOT_EMPTY");
|
|
139
|
+
let entries = 0, bytes = 0;
|
|
140
|
+
const copyInto = async (from, to) => {
|
|
141
|
+
for (const name of await readdir(from)) {
|
|
142
|
+
if (name === "." || name === ".." || name.includes("/") || /[\x00-\x1f\x7f]/u.test(name))
|
|
143
|
+
throw new Error("XCB_LEGACY_ENTRY_INVALID");
|
|
144
|
+
const sourcePath = join(from, name);
|
|
145
|
+
const stat = await lstat(sourcePath);
|
|
146
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() && !stat.isFile())
|
|
147
|
+
throw new Error("XCB_LEGACY_ENTRY_UNSUPPORTED");
|
|
148
|
+
entries += 1;
|
|
149
|
+
if (entries > MAX_MIGRATION_ENTRIES)
|
|
150
|
+
throw new Error("XCB_LEGACY_STATE_TOO_LARGE");
|
|
151
|
+
const targetPath = join(to, name);
|
|
152
|
+
if (stat.isDirectory()) {
|
|
153
|
+
if ((stat.mode & 63) !== 0)
|
|
154
|
+
throw new Error("XCB_LEGACY_ENTRY_NOT_PRIVATE");
|
|
155
|
+
await mkdir(targetPath, { mode: 448 });
|
|
156
|
+
await copyInto(sourcePath, targetPath);
|
|
157
|
+
} else {
|
|
158
|
+
bytes += stat.size;
|
|
159
|
+
if (bytes > MAX_MIGRATION_BYTES)
|
|
160
|
+
throw new Error("XCB_LEGACY_STATE_TOO_LARGE");
|
|
161
|
+
await copyFile(sourcePath, targetPath, constants.COPYFILE_EXCL);
|
|
162
|
+
await chmod(targetPath, stat.mode & 511);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
await copyInto(source, target);
|
|
167
|
+
return { source, entries };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/cli/binaries.ts
|
|
171
|
+
import { statSync } from "node:fs";
|
|
172
|
+
import { chmod as chmod2, lstat as lstat2, realpath } from "node:fs/promises";
|
|
173
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
174
|
+
import { createHash } from "node:crypto";
|
|
175
|
+
import { delimiter, isAbsolute as isAbsolute2, join as join4 } from "node:path";
|
|
176
|
+
import { homedir as homedir2 } from "node:os";
|
|
177
|
+
|
|
178
|
+
// src/codex-scratch.ts
|
|
179
|
+
var MAX_CONFIGURATION_BYTES = 256 * 1024;
|
|
180
|
+
|
|
181
|
+
// src/codex-process.ts
|
|
182
|
+
var CODEX_NATIVE_VERSION = "0.153.4";
|
|
183
|
+
var CODEX_NATIVE_SHA256 = "b973d440acac501fd2594a43e7ca9ce41e0a65b9dfb28d0d7a7837c99e1261e3";
|
|
184
|
+
|
|
185
|
+
// src/cli/devin.ts
|
|
186
|
+
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
187
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
188
|
+
import { join as join3 } from "node:path";
|
|
189
|
+
|
|
190
|
+
// src/cli/auth.ts
|
|
191
|
+
import { spawn } from "node:child_process";
|
|
192
|
+
import { mkdir as mkdir2, readFile, rm } from "node:fs/promises";
|
|
193
|
+
import { join as join2 } from "node:path";
|
|
194
|
+
import { StringDecoder } from "node:string_decoder";
|
|
195
|
+
var fail = (code) => {
|
|
196
|
+
throw new Error(code);
|
|
197
|
+
};
|
|
198
|
+
async function providerAuthDirs(stateRoot, provider) {
|
|
199
|
+
const root = await privateDirectory(stateRoot);
|
|
200
|
+
for (const name of [`${provider}-auth`, `${provider}-home`]) {
|
|
201
|
+
await mkdir2(join2(root, name), { mode: 448, recursive: true });
|
|
202
|
+
}
|
|
203
|
+
return { config: await privateDirectory(join2(root, `${provider}-auth`)), home: await privateDirectory(join2(root, `${provider}-home`)) };
|
|
204
|
+
}
|
|
205
|
+
function managedLoginEnv(home, config) {
|
|
206
|
+
return {
|
|
207
|
+
HOME: home,
|
|
208
|
+
CLAUDE_CONFIG_DIR: config,
|
|
209
|
+
TMPDIR: join2(home, "tmp"),
|
|
210
|
+
PATH: "/usr/bin:/bin:/usr/local/bin",
|
|
211
|
+
LANG: "en_US.UTF-8",
|
|
212
|
+
NO_COLOR: "1",
|
|
213
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
var TOKEN_PATH = (stateRoot) => join2(stateRoot, "claude-oauth-token");
|
|
217
|
+
function loginOutput(write, hidden, hide) {
|
|
218
|
+
const decoder = new StringDecoder("utf8"), prefix = "sk-ant-";
|
|
219
|
+
let held = "", matched = 0, escape = 0;
|
|
220
|
+
const render = (text) => {
|
|
221
|
+
let visible = "";
|
|
222
|
+
for (const character of text) {
|
|
223
|
+
if (hidden())
|
|
224
|
+
break;
|
|
225
|
+
if (escape === 1) {
|
|
226
|
+
escape = character === "[" ? 2 : character === "]" ? 3 : 0;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (escape === 2) {
|
|
230
|
+
if (character >= "@" && character <= "~")
|
|
231
|
+
escape = 0;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (escape === 3) {
|
|
235
|
+
if (character === "\x07")
|
|
236
|
+
escape = 0;
|
|
237
|
+
else if (character === "\x1B")
|
|
238
|
+
escape = 4;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (escape === 4) {
|
|
242
|
+
escape = character === "\\" ? 0 : character === "\x1B" ? 4 : 3;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (character === "\x1B") {
|
|
246
|
+
escape = 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/u.test(character))
|
|
250
|
+
continue;
|
|
251
|
+
if (matched > 0 && /\s/u.test(character)) {
|
|
252
|
+
held += character;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (character === prefix[matched]) {
|
|
256
|
+
held += character;
|
|
257
|
+
if (++matched === prefix.length) {
|
|
258
|
+
hide();
|
|
259
|
+
held = "";
|
|
260
|
+
visible += `<token captured>
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
visible += held;
|
|
265
|
+
held = character === prefix[0] ? character : "";
|
|
266
|
+
matched = held.length;
|
|
267
|
+
if (held === "")
|
|
268
|
+
visible += character;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (visible !== "")
|
|
272
|
+
write(visible);
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
push(chunk) {
|
|
276
|
+
const text = decoder.write(chunk);
|
|
277
|
+
render(text);
|
|
278
|
+
return text;
|
|
279
|
+
},
|
|
280
|
+
end() {
|
|
281
|
+
const text = decoder.end();
|
|
282
|
+
render(text);
|
|
283
|
+
if (!hidden() && held !== "")
|
|
284
|
+
write(held);
|
|
285
|
+
held = "";
|
|
286
|
+
return text;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
async function storeToken(stateRoot, token) {
|
|
291
|
+
await privateDirectory(stateRoot);
|
|
292
|
+
await writeFileOnce(TOKEN_PATH(stateRoot), token + `
|
|
293
|
+
`, { exclusive: false, nofollow: false, truncate: true });
|
|
294
|
+
}
|
|
295
|
+
async function readClaudeOAuthToken(stateRoot) {
|
|
296
|
+
try {
|
|
297
|
+
const text = await readFile(TOKEN_PATH(stateRoot), "utf8");
|
|
298
|
+
if (text.length > 2048)
|
|
299
|
+
return null;
|
|
300
|
+
const token = text.trim();
|
|
301
|
+
return /^sk-ant-oat\d{2}-[A-Za-z0-9_-]{16,1024}$/u.test(token) ? token : null;
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function clearClaudeOAuthToken(stateRoot) {
|
|
307
|
+
await rm(TOKEN_PATH(stateRoot), { force: true });
|
|
308
|
+
}
|
|
309
|
+
async function claudeLogin(stateRoot, inspection) {
|
|
310
|
+
if (!inspection.versionMatches)
|
|
311
|
+
fail("CLAUDE_VERSION_MISMATCH");
|
|
312
|
+
const { config, home } = await providerAuthDirs(stateRoot, "claude");
|
|
313
|
+
await mkdir2(join2(home, "tmp"), { mode: 448, recursive: true });
|
|
314
|
+
const env = managedLoginEnv(home, config);
|
|
315
|
+
const setupToken = (args = ["setup-token"]) => new Promise((resolve) => {
|
|
316
|
+
let captured2 = "", hidden = false, bytes = 0, oversized = false;
|
|
317
|
+
const child = spawn(inspection.executablePath, args, {
|
|
318
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
319
|
+
env,
|
|
320
|
+
detached: false,
|
|
321
|
+
timeout: 600000,
|
|
322
|
+
killSignal: "SIGKILL"
|
|
323
|
+
});
|
|
324
|
+
const hide = () => {
|
|
325
|
+
hidden = true;
|
|
326
|
+
};
|
|
327
|
+
const stdout = loginOutput((text2) => {
|
|
328
|
+
process.stdout.write(text2);
|
|
329
|
+
}, () => hidden, hide);
|
|
330
|
+
const stderr = loginOutput((text2) => {
|
|
331
|
+
process.stderr.write(text2);
|
|
332
|
+
}, () => hidden, hide);
|
|
333
|
+
const bounded = (chunk) => {
|
|
334
|
+
bytes += chunk.byteLength;
|
|
335
|
+
if (bytes <= 64 * 1024)
|
|
336
|
+
return true;
|
|
337
|
+
oversized = true;
|
|
338
|
+
hide();
|
|
339
|
+
child.kill("SIGKILL");
|
|
340
|
+
return false;
|
|
341
|
+
};
|
|
342
|
+
child.stdout.on("data", (chunk) => {
|
|
343
|
+
if (!bounded(chunk))
|
|
344
|
+
return;
|
|
345
|
+
captured2 += stdout.push(chunk);
|
|
346
|
+
});
|
|
347
|
+
child.stderr.on("data", (chunk) => {
|
|
348
|
+
if (bounded(chunk))
|
|
349
|
+
stderr.push(chunk);
|
|
350
|
+
});
|
|
351
|
+
child.on("error", () => resolve({ code: 1, captured: "" }));
|
|
352
|
+
child.on("close", (status) => {
|
|
353
|
+
captured2 += stdout.end();
|
|
354
|
+
stderr.end();
|
|
355
|
+
resolve({ code: oversized ? 1 : status ?? 1, captured: captured2 });
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
let result = await setupToken();
|
|
359
|
+
if (result.code !== 0) {
|
|
360
|
+
const login = await setupToken(["auth", "login"]);
|
|
361
|
+
if (login.code === 0)
|
|
362
|
+
result = await setupToken();
|
|
363
|
+
}
|
|
364
|
+
const { code, captured } = result;
|
|
365
|
+
const text = captured.replaceAll(/\x1b\[[0-9;?]*[a-zA-Z]/gu, "");
|
|
366
|
+
const raw = text.match(/sk-ant-oat\d{2}-[A-Za-z0-9_-]+(?:\n[ \t]*[A-Za-z0-9_-]+[ \t]*(?=\n|$))*/u)?.[0]?.replaceAll(/\s+/gu, "");
|
|
367
|
+
if (code !== 0)
|
|
368
|
+
fail("CLAUDE_LOGIN_FAILED");
|
|
369
|
+
const token = raw !== undefined && /^sk-ant-oat\d{2}-[A-Za-z0-9_-]{16,1024}$/u.test(raw) ? raw : fail("CLAUDE_LOGIN_FAILED");
|
|
370
|
+
await storeToken(stateRoot, token);
|
|
371
|
+
}
|
|
372
|
+
async function claudeAuthStatus(stateRoot) {
|
|
373
|
+
const token = await readClaudeOAuthToken(stateRoot);
|
|
374
|
+
return Object.freeze({ loggedIn: token !== null, authMethod: token === null ? null : "subscription-token" });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/cli/devin.ts
|
|
378
|
+
var fail2 = (code) => {
|
|
379
|
+
throw new Error(code);
|
|
380
|
+
};
|
|
381
|
+
var CLI_DEVIN_MIN_VERSION = "3000.10.27";
|
|
382
|
+
var CLI_DEVIN_MAX_MAJOR = 3000;
|
|
383
|
+
var CLI_DEVIN_QUALIFICATION_REQUIRED = "Devin tasks are disabled: the exact effective tool inventory, command-tool exclusion, configuration isolation and workspace confinement have not been qualified. A matching CLI version or sign-in does not qualify this route; use Claude or an admitted Codex route until host qualification is implemented.";
|
|
384
|
+
function versionTuple(version) {
|
|
385
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version.trim());
|
|
386
|
+
if (match === null)
|
|
387
|
+
return null;
|
|
388
|
+
return Object.freeze([Number(match[1]), Number(match[2]), Number(match[3])]);
|
|
389
|
+
}
|
|
390
|
+
function devinCliVersionMatches(version) {
|
|
391
|
+
const got = versionTuple(version), min = versionTuple(CLI_DEVIN_MIN_VERSION);
|
|
392
|
+
if (got === null || min === null || got[0] !== CLI_DEVIN_MAX_MAJOR)
|
|
393
|
+
return false;
|
|
394
|
+
for (let index = 0;index < 3; index += 1) {
|
|
395
|
+
if (got[index] !== min[index])
|
|
396
|
+
return got[index] > min[index];
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
function devinManagedEnv(home) {
|
|
401
|
+
return Object.freeze({
|
|
402
|
+
HOME: home,
|
|
403
|
+
XDG_DATA_HOME: join3(home, ".local", "share"),
|
|
404
|
+
XDG_CONFIG_HOME: join3(home, ".config"),
|
|
405
|
+
XDG_CACHE_HOME: join3(home, ".cache"),
|
|
406
|
+
TMPDIR: join3(home, "tmp"),
|
|
407
|
+
PATH: "/usr/bin:/bin:/usr/local/bin",
|
|
408
|
+
LANG: "en_US.UTF-8",
|
|
409
|
+
NO_COLOR: "1"
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
async function managedAuthContext(stateRoot) {
|
|
413
|
+
const { home } = await providerAuthDirs(stateRoot, "devin");
|
|
414
|
+
await mkdir3(join3(home, "tmp"), { mode: 448, recursive: true });
|
|
415
|
+
return Object.freeze({ home, env: devinManagedEnv(home) });
|
|
416
|
+
}
|
|
417
|
+
async function devinAuthStatus(stateRoot, inspection) {
|
|
418
|
+
const { env } = await managedAuthContext(stateRoot);
|
|
419
|
+
const result = spawnSync(inspection.executablePath, ["auth", "status"], {
|
|
420
|
+
env: { ...env },
|
|
421
|
+
timeout: 20000,
|
|
422
|
+
encoding: "utf8",
|
|
423
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
424
|
+
});
|
|
425
|
+
if (typeof result.stdout !== "string")
|
|
426
|
+
return Object.freeze({ loggedIn: false, planType: null });
|
|
427
|
+
const text = boundedText(result.stdout, 16 * 1024, true);
|
|
428
|
+
const loggedIn = /^Logged in\b/mu.test(text);
|
|
429
|
+
const plan = /^\s*Plan:\s*(.+)$/mu.exec(text)?.[1]?.trim();
|
|
430
|
+
return Object.freeze({ loggedIn, planType: loggedIn && plan !== undefined && plan !== "" ? boundedText(plan, 64) : null });
|
|
431
|
+
}
|
|
432
|
+
async function devinLogin(stateRoot, inspection) {
|
|
433
|
+
const { env } = await managedAuthContext(stateRoot);
|
|
434
|
+
const code = await new Promise((resolve) => {
|
|
435
|
+
const child = spawn2(inspection.executablePath, ["auth", "login"], { stdio: "inherit", env: { ...env }, detached: false });
|
|
436
|
+
child.on("error", () => resolve(1));
|
|
437
|
+
child.on("exit", (status) => resolve(status ?? 1));
|
|
438
|
+
});
|
|
439
|
+
if (code !== 0)
|
|
440
|
+
fail2("DEVIN_LOGIN_FAILED");
|
|
441
|
+
}
|
|
442
|
+
async function devinLogout(stateRoot, inspection) {
|
|
443
|
+
const { env } = await managedAuthContext(stateRoot);
|
|
444
|
+
await new Promise((resolve) => {
|
|
445
|
+
const child = spawn2(inspection.executablePath, ["auth", "logout"], { stdio: "inherit", env: { ...env }, detached: false });
|
|
446
|
+
child.on("error", () => resolve(0));
|
|
447
|
+
child.on("exit", (status) => resolve(status ?? 0));
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/cli/binaries.ts
|
|
452
|
+
var CLI_CODEX_ENV = "XCB_CODEX";
|
|
453
|
+
var CLI_CLAUDE_ENV = "XCB_CLAUDE";
|
|
454
|
+
var CLI_DEVIN_ENV = "XCB_DEVIN";
|
|
455
|
+
var LEGACY_CLI_CODEX_ENV = "AGENTMIXER_CODEX";
|
|
456
|
+
var LEGACY_CLI_CLAUDE_ENV = "AGENTMIXER_CLAUDE";
|
|
457
|
+
var LEGACY_CLI_DEVIN_ENV = "AGENTMIXER_DEVIN";
|
|
458
|
+
var MAX_EXECUTABLE_BYTES = 256 * 1024 * 1024;
|
|
459
|
+
async function inspectCliExecutable(rawPath) {
|
|
460
|
+
const canonical = canonicalizePrivatePath(rawPath, { code: "XCB_EXECUTABLE_INVALID", resolved: false, reject: PRIVATE_CONTROL_REJECT, maxLength: Infinity });
|
|
461
|
+
const executablePath = await realpath(canonical);
|
|
462
|
+
const stat = await lstat2(executablePath);
|
|
463
|
+
assertPrivateStat(stat, {
|
|
464
|
+
kind: "file",
|
|
465
|
+
noSymlink: true,
|
|
466
|
+
links: "single",
|
|
467
|
+
owner: "selfOrRoot",
|
|
468
|
+
mode: [{ mask: 18, equals: 0 }, { mask: 73, notEquals: 0 }, { mask: 3072, equals: 0 }],
|
|
469
|
+
size: { min: 1, max: MAX_EXECUTABLE_BYTES }
|
|
470
|
+
}, "XCB_EXECUTABLE_INVALID");
|
|
471
|
+
const handle = await openPrivateRead(executablePath, { nonblock: false });
|
|
472
|
+
try {
|
|
473
|
+
const bytes = await handle.readFile();
|
|
474
|
+
if (bytes.byteLength > MAX_EXECUTABLE_BYTES)
|
|
475
|
+
throw new Error("XCB_EXECUTABLE_INVALID");
|
|
476
|
+
return { executablePath, sha256: createHash("sha256").update(bytes).digest("hex"), bytes: new Uint8Array(bytes) };
|
|
477
|
+
} finally {
|
|
478
|
+
await handle.close();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function executableExists(path) {
|
|
482
|
+
try {
|
|
483
|
+
const stat = statSync(path);
|
|
484
|
+
return stat.isFile() && (stat.mode & 73) !== 0;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function pathEntries(env) {
|
|
490
|
+
const raw = env("PATH");
|
|
491
|
+
if (typeof raw !== "string")
|
|
492
|
+
return [];
|
|
493
|
+
return raw.split(delimiter).filter((entry) => isAbsolute2(entry) && entry.length <= 512).slice(0, 64);
|
|
494
|
+
}
|
|
495
|
+
var PROVIDER_ENV = { claude: CLI_CLAUDE_ENV, codex: CLI_CODEX_ENV, devin: CLI_DEVIN_ENV };
|
|
496
|
+
var LEGACY_PROVIDER_ENV = { claude: LEGACY_CLI_CLAUDE_ENV, codex: LEGACY_CLI_CODEX_ENV, devin: LEGACY_CLI_DEVIN_ENV };
|
|
497
|
+
function cliBinaryCandidates(provider, env = (name) => process.env[name]) {
|
|
498
|
+
const pinned = env(PROVIDER_ENV[provider]) ?? env(LEGACY_PROVIDER_ENV[provider]);
|
|
499
|
+
const command = provider;
|
|
500
|
+
const home = homedir2();
|
|
501
|
+
const known = provider === "claude" ? [join4(home, ".local", "bin", "claude"), join4(home, ".claude", "local", "claude")] : provider === "codex" ? [join4(home, ".codex", "bin", "codex"), join4(home, ".local", "bin", "codex")] : [join4(home, ".local", "bin", "devin"), join4(home, ".devin", "bin", "devin")];
|
|
502
|
+
const found = [
|
|
503
|
+
...pinned !== undefined ? [pinned] : [],
|
|
504
|
+
...pathEntries(env).map((entry) => join4(entry, command)),
|
|
505
|
+
...known
|
|
506
|
+
];
|
|
507
|
+
return Object.freeze([...new Set(found)].filter((path) => isAbsolute2(path) && path.length <= 512).slice(0, 32));
|
|
508
|
+
}
|
|
509
|
+
function reportedVersion(executablePath) {
|
|
510
|
+
let result;
|
|
511
|
+
try {
|
|
512
|
+
result = spawnSync2(executablePath, ["--version"], {
|
|
513
|
+
timeout: 15000,
|
|
514
|
+
encoding: "utf8",
|
|
515
|
+
env: { PATH: "/usr/bin:/bin", HOME: homedir2(), LANG: "en_US.UTF-8" },
|
|
516
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
517
|
+
});
|
|
518
|
+
} catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
if (result.status !== 0 || typeof result.stdout !== "string")
|
|
522
|
+
return null;
|
|
523
|
+
const match = /(?:^|\s)(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:[-+.][0-9A-Za-z.-]*)?/u.exec(result.stdout.trim());
|
|
524
|
+
return match === null ? null : boundedText(match[0].trim(), 160);
|
|
525
|
+
}
|
|
526
|
+
async function repairCliExecutableMode(rawPath) {
|
|
527
|
+
if (typeof rawPath !== "string" || !isAbsolute2(rawPath))
|
|
528
|
+
return false;
|
|
529
|
+
let resolved;
|
|
530
|
+
try {
|
|
531
|
+
resolved = await realpath(rawPath);
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const stat = await lstat2(resolved);
|
|
537
|
+
if (!matchesPrivateStat(stat, { kind: "file", noSymlink: true, links: "single", owner: "self", mode: [{ mask: 3072, equals: 0 }] }))
|
|
538
|
+
return false;
|
|
539
|
+
if ((stat.mode & 18) === 0)
|
|
540
|
+
return false;
|
|
541
|
+
await chmod2(resolved, 493);
|
|
542
|
+
return true;
|
|
543
|
+
} catch {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function inspectCliBinary(provider, env = (name) => process.env[name], repair = false) {
|
|
548
|
+
for (const candidate of cliBinaryCandidates(provider, env)) {
|
|
549
|
+
if (!executableExists(candidate))
|
|
550
|
+
continue;
|
|
551
|
+
let inspected;
|
|
552
|
+
try {
|
|
553
|
+
inspected = await inspectCliExecutable(candidate);
|
|
554
|
+
} catch {
|
|
555
|
+
if (!repair || !await repairCliExecutableMode(candidate))
|
|
556
|
+
continue;
|
|
557
|
+
try {
|
|
558
|
+
inspected = await inspectCliExecutable(candidate);
|
|
559
|
+
} catch {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const version = reportedVersion(inspected.executablePath);
|
|
564
|
+
if (version === null)
|
|
565
|
+
continue;
|
|
566
|
+
const versionMatches = provider === "codex" ? version === CODEX_NATIVE_VERSION : provider === "claude" ? claudeCodeVersionAdmitted(version) : devinCliVersionMatches(version);
|
|
567
|
+
const pinnedSha256 = provider === "codex" ? CODEX_NATIVE_SHA256 : null;
|
|
568
|
+
return Object.freeze({
|
|
569
|
+
provider,
|
|
570
|
+
executablePath: inspected.executablePath,
|
|
571
|
+
version,
|
|
572
|
+
sha256: inspected.sha256,
|
|
573
|
+
pinnedSha256,
|
|
574
|
+
versionMatches,
|
|
575
|
+
digestMatches: pinnedSha256 === null ? true : inspected.sha256 === pinnedSha256
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/cli/codex.ts
|
|
582
|
+
import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
|
|
583
|
+
import { mkdir as mkdir5, mkdtemp as mkdtemp2, opendir as opendir2, readFile as readFile2, realpath as realpath3, rm as rm3 } from "node:fs/promises";
|
|
584
|
+
import { join as join7 } from "node:path";
|
|
585
|
+
|
|
586
|
+
// src/codex-managed-process.ts
|
|
587
|
+
import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
|
|
588
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
589
|
+
import { closeSync, fsyncSync, writeSync } from "node:fs";
|
|
590
|
+
import { lstat as lstat3, mkdir as mkdir4, mkdtemp, opendir, realpath as realpath2, rm as rm2, unlink } from "node:fs/promises";
|
|
591
|
+
import { join as join5 } from "node:path";
|
|
592
|
+
import { PassThrough, Writable } from "node:stream";
|
|
593
|
+
|
|
594
|
+
// src/codex-managed-offline-protocol.ts
|
|
595
|
+
function createManagedOfflineProtocol(process2, accountHome, signal, deadline) {
|
|
596
|
+
const stdin = process2.stdin;
|
|
597
|
+
if (!stdin)
|
|
598
|
+
throw Error("OFFLINE_DIAGNOSTIC_STDIN_UNAVAILABLE");
|
|
599
|
+
let closing = false, configured = false, disabledNotices = 0, frames = 0, bytes = 0, line = "", failure = null, joined = false, decoded = false;
|
|
600
|
+
const methods = [], writes = new Set, decoder = new TextDecoder("utf-8", { fatal: true });
|
|
601
|
+
let revoke;
|
|
602
|
+
const revoked = new Promise((_, reject) => {
|
|
603
|
+
revoke = reject;
|
|
604
|
+
});
|
|
605
|
+
revoked.catch(() => {});
|
|
606
|
+
let pending;
|
|
607
|
+
const invalid = (code) => {
|
|
608
|
+
throw Error(code);
|
|
609
|
+
};
|
|
610
|
+
function object2(value) {
|
|
611
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length > 256)
|
|
612
|
+
return invalid("OFFLINE_DIAGNOSTIC_RESPONSE_INVALID");
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
function fail3(code) {
|
|
616
|
+
failure ??= code;
|
|
617
|
+
revoke(Error(failure));
|
|
618
|
+
pending?.reject(Error(code));
|
|
619
|
+
pending = undefined;
|
|
620
|
+
}
|
|
621
|
+
function active() {
|
|
622
|
+
if (closing || signal.aborted || failure || Date.now() >= deadline)
|
|
623
|
+
throw Error(failure ?? "OFFLINE_DIAGNOSTIC_PROTOCOL_STOPPED");
|
|
624
|
+
}
|
|
625
|
+
async function bounded(work) {
|
|
626
|
+
let timer;
|
|
627
|
+
try {
|
|
628
|
+
return await Promise.race([work, revoked, new Promise((_, reject) => {
|
|
629
|
+
timer = setTimeout(() => reject(Error("OFFLINE_DIAGNOSTIC_PROTOCOL_DEADLINE")), Math.max(0, deadline - Date.now()));
|
|
630
|
+
})]);
|
|
631
|
+
} finally {
|
|
632
|
+
clearTimeout(timer);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function write(frame, method) {
|
|
636
|
+
active();
|
|
637
|
+
methods.push(method);
|
|
638
|
+
const encoded = JSON.stringify(frame) + `
|
|
639
|
+
`;
|
|
640
|
+
if (methods.length > 3 || Buffer.byteLength(encoded) > 8192)
|
|
641
|
+
return Promise.reject(Error("OFFLINE_DIAGNOSTIC_REQUEST_BOUND"));
|
|
642
|
+
const work = new Promise((resolve, reject) => stdin.write(encoded, (error) => error ? reject(Error("OFFLINE_DIAGNOSTIC_WRITE_FAILED")) : resolve()));
|
|
643
|
+
writes.add(work);
|
|
644
|
+
work.then(() => writes.delete(work), () => writes.delete(work));
|
|
645
|
+
return bounded(work);
|
|
646
|
+
}
|
|
647
|
+
async function rpc(id, method, params) {
|
|
648
|
+
active();
|
|
649
|
+
if (pending)
|
|
650
|
+
return invalid("OFFLINE_DIAGNOSTIC_CONCURRENT_RPC");
|
|
651
|
+
const result2 = new Promise((resolve, reject) => {
|
|
652
|
+
pending = { id, resolve, reject };
|
|
653
|
+
});
|
|
654
|
+
result2.catch(() => {});
|
|
655
|
+
await write({ id, method, params }, method);
|
|
656
|
+
return bounded(result2);
|
|
657
|
+
}
|
|
658
|
+
function onData(chunk) {
|
|
659
|
+
if (closing)
|
|
660
|
+
return;
|
|
661
|
+
try {
|
|
662
|
+
if (!(chunk instanceof Uint8Array) || (bytes += chunk.byteLength) > 2 * 1024 * 1024)
|
|
663
|
+
return invalid("OFFLINE_DIAGNOSTIC_RESPONSE_BOUND");
|
|
664
|
+
line += decoder.decode(chunk, { stream: true });
|
|
665
|
+
let index;
|
|
666
|
+
while ((index = line.indexOf(`
|
|
667
|
+
`)) !== -1) {
|
|
668
|
+
const encoded = line.slice(0, index);
|
|
669
|
+
line = line.slice(index + 1);
|
|
670
|
+
if (++frames > 32 || Buffer.byteLength(encoded) > 1024 * 1024)
|
|
671
|
+
return invalid("OFFLINE_DIAGNOSTIC_FRAME_BOUND");
|
|
672
|
+
const frame = object2(JSON.parse(encoded));
|
|
673
|
+
if (frame.jsonrpc !== undefined && frame.jsonrpc !== "2.0")
|
|
674
|
+
return invalid("OFFLINE_DIAGNOSTIC_UNEXPECTED_RESPONSE");
|
|
675
|
+
if (Object.hasOwn(frame, "method")) {
|
|
676
|
+
if (Object.keys(frame).some((key) => !["method", "params", "jsonrpc", "emittedAtMs"].includes(key)) || frame.method !== "remoteControl/status/changed" || ++disabledNotices > 1)
|
|
677
|
+
return invalid("OFFLINE_DIAGNOSTIC_NATIVE_OPERATION_DENIED");
|
|
678
|
+
if (frame.emittedAtMs !== undefined && (!Number.isSafeInteger(frame.emittedAtMs) || Number(frame.emittedAtMs) < 0))
|
|
679
|
+
return invalid("OFFLINE_DIAGNOSTIC_NATIVE_OPERATION_DENIED");
|
|
680
|
+
const params = object2(frame.params);
|
|
681
|
+
if (params.status !== "disabled" || Object.keys(params).some((key) => !["status", "installationId", "serverName", "environmentId"].includes(key)))
|
|
682
|
+
return invalid("OFFLINE_DIAGNOSTIC_NATIVE_OPERATION_DENIED");
|
|
683
|
+
for (const [key, limit] of [["installationId", 160], ["serverName", 1024]]) {
|
|
684
|
+
if (typeof params[key] !== "string" || !params[key] || Buffer.byteLength(params[key]) > limit)
|
|
685
|
+
return invalid("OFFLINE_DIAGNOSTIC_NATIVE_OPERATION_DENIED");
|
|
686
|
+
}
|
|
687
|
+
if (params.environmentId != null && (typeof params.environmentId !== "string" || !params.environmentId || Buffer.byteLength(params.environmentId) > 160))
|
|
688
|
+
return invalid("OFFLINE_DIAGNOSTIC_NATIVE_OPERATION_DENIED");
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (!pending || frame.id !== pending.id || Object.hasOwn(frame, "error") || !Object.hasOwn(frame, "result") || Object.keys(frame).some((key) => !["id", "result", "jsonrpc"].includes(key)))
|
|
692
|
+
return invalid("OFFLINE_DIAGNOSTIC_UNEXPECTED_RESPONSE");
|
|
693
|
+
const current = pending;
|
|
694
|
+
pending = undefined;
|
|
695
|
+
current.resolve(frame.result);
|
|
696
|
+
}
|
|
697
|
+
if (Buffer.byteLength(line) > 1024 * 1024)
|
|
698
|
+
return invalid("OFFLINE_DIAGNOSTIC_FRAME_BOUND");
|
|
699
|
+
} catch (error) {
|
|
700
|
+
fail3(error instanceof Error && /^OFFLINE_DIAGNOSTIC_[A-Z_]+$/u.test(error.message) ? error.message : "OFFLINE_DIAGNOSTIC_RESPONSE_INVALID");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function finishDecoding() {
|
|
704
|
+
if (decoded)
|
|
705
|
+
return;
|
|
706
|
+
decoded = true;
|
|
707
|
+
try {
|
|
708
|
+
line += decoder.decode();
|
|
709
|
+
if (line.length !== 0)
|
|
710
|
+
fail3("OFFLINE_DIAGNOSTIC_TRUNCATED_FRAME");
|
|
711
|
+
} catch {
|
|
712
|
+
fail3("OFFLINE_DIAGNOSTIC_TRUNCATED_UTF8");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function onEnd() {
|
|
716
|
+
finishDecoding();
|
|
717
|
+
if (!closing && !configured)
|
|
718
|
+
fail3("OFFLINE_DIAGNOSTIC_PREMATURE_EXIT");
|
|
719
|
+
}
|
|
720
|
+
function onError() {
|
|
721
|
+
fail3("OFFLINE_DIAGNOSTIC_STREAM_FAILED");
|
|
722
|
+
}
|
|
723
|
+
function onAbort() {
|
|
724
|
+
fail3("OFFLINE_DIAGNOSTIC_CANCELLED");
|
|
725
|
+
}
|
|
726
|
+
process2.stdout.on("data", onData);
|
|
727
|
+
process2.stdout.on("end", onEnd);
|
|
728
|
+
process2.stdout.on("error", onError);
|
|
729
|
+
stdin.on("error", onError);
|
|
730
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
731
|
+
const result = Promise.resolve().then(async () => {
|
|
732
|
+
await bounded(process2.ready);
|
|
733
|
+
active();
|
|
734
|
+
const initialized = object2(await rpc(1, "initialize", { clientInfo: { name: "xcb-offline-diagnostic", version: "0.6.0" }, capabilities: { experimentalApi: false, requestAttestation: false } }));
|
|
735
|
+
for (const [field, limit] of [["userAgent", 1024], ["codexHome", 4096], ["platformFamily", 128], ["platformOs", 128]]) {
|
|
736
|
+
if (typeof initialized[field] !== "string" || !initialized[field] || Buffer.byteLength(initialized[field]) > limit)
|
|
737
|
+
return invalid("OFFLINE_DIAGNOSTIC_INITIALIZE_INVALID");
|
|
738
|
+
}
|
|
739
|
+
if (initialized.codexHome !== accountHome || Object.keys(initialized).some((key) => !["userAgent", "codexHome", "platformFamily", "platformOs"].includes(key)))
|
|
740
|
+
return invalid("OFFLINE_DIAGNOSTIC_INITIALIZE_INVALID");
|
|
741
|
+
await write({ method: "initialized" }, "initialized");
|
|
742
|
+
const configuration = await rpc(2, "config/read", { cwd: process2.cwd, includeLayers: false });
|
|
743
|
+
active();
|
|
744
|
+
assertCodexManagedConfigResponse(configuration, { cwd: process2.cwd });
|
|
745
|
+
configured = true;
|
|
746
|
+
if (disabledNotices !== 1 || line.length !== 0)
|
|
747
|
+
return invalid("OFFLINE_DIAGNOSTIC_STARTUP_UNPROVEN");
|
|
748
|
+
}).catch((error) => {
|
|
749
|
+
fail3(error instanceof Error && /^OFFLINE_DIAGNOSTIC_[A-Z_]+$/u.test(error.message) ? error.message : "OFFLINE_DIAGNOSTIC_CONFIGURATION_INVALID");
|
|
750
|
+
throw Error(failure);
|
|
751
|
+
});
|
|
752
|
+
result.catch(() => {});
|
|
753
|
+
function stop() {
|
|
754
|
+
if (closing)
|
|
755
|
+
return;
|
|
756
|
+
closing = true;
|
|
757
|
+
finishDecoding();
|
|
758
|
+
revoke(Error("OFFLINE_DIAGNOSTIC_PROTOCOL_STOPPED"));
|
|
759
|
+
pending?.reject(Error("OFFLINE_DIAGNOSTIC_PROTOCOL_STOPPED"));
|
|
760
|
+
pending = undefined;
|
|
761
|
+
process2.stdout.off("data", onData);
|
|
762
|
+
process2.stdout.off("end", onEnd);
|
|
763
|
+
process2.stdout.off("error", onError);
|
|
764
|
+
stdin.off("error", onError);
|
|
765
|
+
signal.removeEventListener("abort", onAbort);
|
|
766
|
+
}
|
|
767
|
+
const receipt = () => Object.freeze({ methods: Object.freeze([...methods]), configurationObserved: configured, disabledNotices, frameCount: frames, failure, joined });
|
|
768
|
+
return Object.freeze({ result, stop, async join() {
|
|
769
|
+
stop();
|
|
770
|
+
await Promise.allSettled([result, ...writes]);
|
|
771
|
+
joined = writes.size === 0;
|
|
772
|
+
return receipt();
|
|
773
|
+
}, receipt });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/codex-managed-process.ts
|
|
777
|
+
var hash = (value) => createHash2("sha256").update(value).digest("hex");
|
|
778
|
+
var fail3 = (code) => {
|
|
779
|
+
throw new Error(code);
|
|
780
|
+
};
|
|
781
|
+
function check(value, code) {
|
|
782
|
+
if (!value)
|
|
783
|
+
fail3(code);
|
|
784
|
+
}
|
|
785
|
+
function digest(value) {
|
|
786
|
+
check(typeof value === "string" && /^[a-f0-9]{64}$/u.test(value), "CODEX_MANAGED_PROCESS_PIN_INVALID");
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
789
|
+
function record(value, keys) {
|
|
790
|
+
check(value !== null && typeof value === "object" && [Object.prototype, null].includes(Object.getPrototypeOf(value)), "CODEX_MANAGED_PROCESS_OBJECT_INVALID");
|
|
791
|
+
const result = Object.create(null);
|
|
792
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
793
|
+
check(typeof key === "string" && keys.includes(key), "CODEX_MANAGED_PROCESS_UNKNOWN_FIELD");
|
|
794
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
795
|
+
check("value" in descriptor && descriptor.enumerable, "CODEX_MANAGED_PROCESS_ACCESSOR_DENIED");
|
|
796
|
+
result[key] = descriptor.value;
|
|
797
|
+
}
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
function path(value) {
|
|
801
|
+
return canonicalizePrivatePath(value, { code: "CODEX_MANAGED_PROCESS_PATH_INVALID" });
|
|
802
|
+
}
|
|
803
|
+
function egressHost(value) {
|
|
804
|
+
check(typeof value === "string", "CODEX_MANAGED_PROCESS_EGRESS_INVALID");
|
|
805
|
+
const lowered = value.toLowerCase();
|
|
806
|
+
check(lowered.length > 0 && Buffer.byteLength(lowered) <= 253 && /^[a-z0-9._:\[\]-]+$/u.test(lowered), "CODEX_MANAGED_PROCESS_EGRESS_INVALID");
|
|
807
|
+
return lowered;
|
|
808
|
+
}
|
|
809
|
+
function sandboxOf(value) {
|
|
810
|
+
if (value === undefined)
|
|
811
|
+
return;
|
|
812
|
+
const raw = record(value, ["executable", "sha256", "readOnlyPaths", "egress"]);
|
|
813
|
+
return Object.freeze({
|
|
814
|
+
executable: path(raw.executable),
|
|
815
|
+
sha256: digest(raw.sha256),
|
|
816
|
+
readOnlyPaths: raw.readOnlyPaths === undefined ? Object.freeze([]) : Object.freeze((check(Array.isArray(raw.readOnlyPaths) && raw.readOnlyPaths.length <= 256, "CODEX_MANAGED_PROCESS_SANDBOX_INVALID"), raw.readOnlyPaths.map((entry) => path(entry)))),
|
|
817
|
+
...raw.egress === undefined ? {} : { egress: (() => {
|
|
818
|
+
const e = record(raw.egress, ["allowlist"]);
|
|
819
|
+
return Object.freeze({ ...e.allowlist === undefined ? {} : { allowlist: (check(Array.isArray(e.allowlist) && e.allowlist.length <= 256, "CODEX_MANAGED_PROCESS_EGRESS_INVALID"), Object.freeze(e.allowlist.map((entry) => egressHost(entry)))) } });
|
|
820
|
+
})() }
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async function directory(value) {
|
|
824
|
+
return (await assertPrivateDirectory(value, { code: "CODEX_MANAGED_PROCESS_PRIVATE_DIRECTORY_REQUIRED", owner: "selfOrThrow" })).metadata;
|
|
825
|
+
}
|
|
826
|
+
var syncDirectory = fsyncDirectory;
|
|
827
|
+
async function durableFile(value, contents) {
|
|
828
|
+
await writeFileOnce(value, contents, { syncFile: true, syncParent: true });
|
|
829
|
+
}
|
|
830
|
+
async function fixedFile(value, contents) {
|
|
831
|
+
await readExactPrivateFile(value, contents, {
|
|
832
|
+
invalidCode: "CODEX_MANAGED_PROCESS_FILE_INVALID",
|
|
833
|
+
changedCode: "CODEX_MANAGED_PROCESS_FILE_CHANGED",
|
|
834
|
+
loop: true,
|
|
835
|
+
stableCode: "CODEX_HOST_EXECUTABLE_CHANGED",
|
|
836
|
+
stablePlainFile: true
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
async function copyExecutable(sourcePath, destination, expectedSha256) {
|
|
840
|
+
const source = await openPrivateRead(sourcePath);
|
|
841
|
+
try {
|
|
842
|
+
const before = await source.stat({ bigint: true });
|
|
843
|
+
assertPrivateStat(before, { kind: "file", size: { min: 1n, max: 256n * 1024n * 1024n } }, "CODEX_MANAGED_PROCESS_EXECUTABLE_INVALID");
|
|
844
|
+
const target = await openPrivateWrite(destination, { mode: 320 });
|
|
845
|
+
try {
|
|
846
|
+
const sha = createHash2("sha256");
|
|
847
|
+
const count = await streamFdContent(source, before.size, {
|
|
848
|
+
code: "CODEX_MANAGED_PROCESS_EXECUTABLE_CHANGED",
|
|
849
|
+
earlyGrowth: true,
|
|
850
|
+
onChunk: async (chunk) => {
|
|
851
|
+
sha.update(chunk);
|
|
852
|
+
await target.writeFile(chunk);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
check(count === Number(before.size) && sha.digest("hex") === expectedSha256 && await realpath2(sourcePath) === sourcePath, "CODEX_MANAGED_PROCESS_EXECUTABLE_CHANGED");
|
|
856
|
+
assertCodexHostFileStable(before, await source.stat({ bigint: true }), await lstat3(sourcePath, { bigint: true }));
|
|
857
|
+
await target.sync();
|
|
858
|
+
} finally {
|
|
859
|
+
await target.close();
|
|
860
|
+
}
|
|
861
|
+
} finally {
|
|
862
|
+
await source.close();
|
|
863
|
+
}
|
|
864
|
+
await inspectCodexHostExecutable(destination, expectedSha256);
|
|
865
|
+
}
|
|
866
|
+
async function inspectScratch(scratch) {
|
|
867
|
+
const entries = [];
|
|
868
|
+
for (const name of ["", "home", "tmp", "work"]) {
|
|
869
|
+
const value = name ? join5(scratch, name) : scratch, metadata = await directory(value), iterator = await opendir(value, { bufferSize: 4 });
|
|
870
|
+
const expected = name ? [] : ["home", "tmp", "work"], found = [];
|
|
871
|
+
try {
|
|
872
|
+
for (;; ) {
|
|
873
|
+
const entry = await iterator.read();
|
|
874
|
+
if (!entry)
|
|
875
|
+
break;
|
|
876
|
+
check(expected.includes(entry.name) && found.length < expected.length, "CODEX_MANAGED_PROCESS_SCRATCH_INVALID");
|
|
877
|
+
found.push(entry.name);
|
|
878
|
+
}
|
|
879
|
+
} finally {
|
|
880
|
+
await iterator.close();
|
|
881
|
+
}
|
|
882
|
+
check(JSON.stringify(found.sort()) === JSON.stringify(expected), "CODEX_MANAGED_PROCESS_SCRATCH_INVALID");
|
|
883
|
+
const current = await directory(value);
|
|
884
|
+
check(current.dev === metadata.dev && current.ino === metadata.ino, "CODEX_MANAGED_PROCESS_SCRATCH_CHANGED");
|
|
885
|
+
entries.push({ name, device: String(metadata.dev), inode: String(metadata.ino) });
|
|
886
|
+
}
|
|
887
|
+
const content = hash(JSON.stringify({ schema: "xcb.codex-managed-scratch.v1", directories: entries.map((entry) => entry.name), mode: 448 }));
|
|
888
|
+
return { content, identity: hash(JSON.stringify({ scratch, content, entries })) };
|
|
889
|
+
}
|
|
890
|
+
async function bounded(promise, deadline) {
|
|
891
|
+
const remaining = deadline - Date.now();
|
|
892
|
+
check(remaining > 0, "CODEX_MANAGED_PROCESS_DEADLINE");
|
|
893
|
+
let timer;
|
|
894
|
+
try {
|
|
895
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
896
|
+
timer = setTimeout(() => reject(new Error("CODEX_MANAGED_PROCESS_DEADLINE")), remaining);
|
|
897
|
+
})]);
|
|
898
|
+
} finally {
|
|
899
|
+
clearTimeout(timer);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
var system = {
|
|
903
|
+
inspectParent: inspectCodexHostRuntime,
|
|
904
|
+
startEgressBridge: (request) => createEgressBridge({ socketPath: request.socketPath, ...request.allowlist === undefined ? {} : { allowlist: request.allowlist }, dialer: egressBridgeDialer }),
|
|
905
|
+
spawn: (request) => spawn3(request.executable, [...request.args], { cwd: request.cwd, env: { ...request.env }, detached: true, stdio: ["pipe", "pipe", "pipe"] }),
|
|
906
|
+
processGroup(pid) {
|
|
907
|
+
const result = spawnSync3("/bin/ps", ["-p", String(pid), "-o", "pgid="], { encoding: "utf8", timeout: 1000, maxBuffer: 1024, env: { PATH: "/usr/bin:/bin" } });
|
|
908
|
+
return !result.error && result.status === 0 && /^[1-9][0-9]*$/u.test(result.stdout.trim()) ? Number(result.stdout.trim()) : null;
|
|
909
|
+
},
|
|
910
|
+
signalGroup(pgid, signal) {
|
|
911
|
+
try {
|
|
912
|
+
process.kill(-pgid, signal);
|
|
913
|
+
return true;
|
|
914
|
+
} catch (error) {
|
|
915
|
+
if (error.code === "ESRCH")
|
|
916
|
+
return false;
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
function codexManagedOfflineSandbox(input) {
|
|
922
|
+
const raw = record(input, ["executable", "scratch", "accountHome"]), executable = path(raw.executable), scratch = path(raw.scratch), accountHome = path(raw.accountHome);
|
|
923
|
+
check(executable !== scratch && !executable.startsWith(scratch + "/") && executable !== accountHome && !executable.startsWith(accountHome + "/") && scratch !== accountHome && !scratch.startsWith(accountHome + "/") && !accountHome.startsWith(scratch + "/"), "CODEX_MANAGED_PROCESS_LAYOUT_INVALID");
|
|
924
|
+
const q = JSON.stringify;
|
|
925
|
+
return `(version 1)
|
|
926
|
+
(deny default)
|
|
927
|
+
(allow process-exec (literal ${q(executable)}))
|
|
928
|
+
(allow file-read* (literal ${q(executable)}) (subpath "/System/Library") (subpath "/usr/lib") (subpath "/Library/Apple/System/Library") (subpath "/System/Cryptexes/OS") (subpath "/System/Volumes/Preboot/Cryptexes/OS") (literal "/dev/null") (literal "/dev/urandom") (literal "/dev/random"))
|
|
929
|
+
(allow file-write* (literal "/dev/null"))
|
|
930
|
+
(allow file-read-data file-write-data (literal "/dev/fd/0") (literal "/dev/fd/1") (literal "/dev/fd/2"))
|
|
931
|
+
(allow file-map-executable (literal ${q(executable)}) (subpath "/System/Library") (subpath "/usr/lib") (subpath "/System/Cryptexes/OS") (subpath "/System/Volumes/Preboot/Cryptexes/OS"))
|
|
932
|
+
(allow file-read* (literal "/") (path-ancestors "/System/Cryptexes/OS") (path-ancestors "/System/Volumes/Preboot/Cryptexes/OS"))
|
|
933
|
+
(allow file-read-metadata (path-ancestors ${q(executable)}) (path-ancestors ${q(scratch)}) (path-ancestors ${q(accountHome)}))
|
|
934
|
+
(allow file-read* file-write* (subpath ${q(scratch)}) (subpath ${q(accountHome)}))
|
|
935
|
+
(allow file-read-data file-read-metadata (literal "/etc/codex/requirements.toml") (literal "/private/etc/codex/requirements.toml"))
|
|
936
|
+
(allow file-read-metadata (path-ancestors "/etc/codex/requirements.toml") (path-ancestors "/private/etc/codex/requirements.toml"))
|
|
937
|
+
(allow sysctl-read)
|
|
938
|
+
(allow process-info* (target self))
|
|
939
|
+
(allow signal (target self))
|
|
940
|
+
`;
|
|
941
|
+
}
|
|
942
|
+
function codexManagedProviderSandbox(input) {
|
|
943
|
+
return codexManagedOfflineSandbox(input) + `(allow network-outbound (literal "/private/var/run/mDNSResponder") (remote tcp "*:443"))
|
|
944
|
+
` + `(allow file-read-metadata (literal "/var"))
|
|
945
|
+
`;
|
|
946
|
+
}
|
|
947
|
+
function codexManagedProviderV2Sandbox(input) {
|
|
948
|
+
return codexManagedProviderSandbox(input) + `(allow file-read* (literal "/private/etc/ssl/cert.pem"))
|
|
949
|
+
`;
|
|
950
|
+
}
|
|
951
|
+
function createCodexManagedProcessLauncher(options, trustedSystem = system) {
|
|
952
|
+
const raw = record(options, ["stateRoot", "runtime", "admission", "startupTimeoutMs"]), stateRoot = path(raw.stateRoot);
|
|
953
|
+
const r = record(raw.runtime, ["executablePath", "version", "sha256", "schemaSha256", "parentRuntime", "sandbox"]), p = record(r.parentRuntime, ["expectedSha256"]);
|
|
954
|
+
const runtimeSandbox = sandboxOf(r.sandbox);
|
|
955
|
+
const runtime = Object.freeze({
|
|
956
|
+
executablePath: path(r.executablePath),
|
|
957
|
+
version: identifier(r.version),
|
|
958
|
+
sha256: digest(r.sha256),
|
|
959
|
+
schemaSha256: digest(r.schemaSha256),
|
|
960
|
+
parentRuntime: Object.freeze({ expectedSha256: digest(p.expectedSha256) }),
|
|
961
|
+
...runtimeSandbox === undefined ? {} : { sandbox: runtimeSandbox }
|
|
962
|
+
});
|
|
963
|
+
const a = record(raw.admission, ["profile", "taskRuntimeVersion", "taskRuntimeDigest", "nativeSha256", "schemaSha256", "parentSha256"]);
|
|
964
|
+
const sandboxProfile = a.profile;
|
|
965
|
+
check(sandboxProfile === "managed-task-offline-candidate-v1" || sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v1" || sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v2", "CODEX_MANAGED_PROCESS_ADMISSION_MISMATCH");
|
|
966
|
+
check(digest(a.nativeSha256) === runtime.sha256 && digest(a.schemaSha256) === runtime.schemaSha256 && digest(a.parentSha256) === runtime.parentRuntime.expectedSha256, "CODEX_MANAGED_PROCESS_ADMISSION_MISMATCH");
|
|
967
|
+
const taskRuntime = Object.freeze({ version: identifier(a.taskRuntimeVersion), digest: digest(a.taskRuntimeDigest) }), startupMs = safeInteger(raw.startupTimeoutMs ?? 1e4, 1, 120000);
|
|
968
|
+
const host = Object.freeze({
|
|
969
|
+
inspectParent: trustedSystem.inspectParent.bind(trustedSystem),
|
|
970
|
+
spawn: trustedSystem.spawn.bind(trustedSystem),
|
|
971
|
+
processGroup: trustedSystem.processGroup.bind(trustedSystem),
|
|
972
|
+
signalGroup: trustedSystem.signalGroup.bind(trustedSystem),
|
|
973
|
+
...trustedSystem.startEgressBridge === undefined ? {} : { startEgressBridge: trustedSystem.startEgressBridge.bind(trustedSystem) },
|
|
974
|
+
syncDirectory: trustedSystem.syncDirectory?.bind(trustedSystem) ?? syncDirectory
|
|
975
|
+
});
|
|
976
|
+
const seen = new WeakSet;
|
|
977
|
+
let generation = 0;
|
|
978
|
+
return Object.freeze({ launch(input) {
|
|
979
|
+
const launch = record(input, ["request", "runId", "accountId", "workspaceId", "configuration", "accountLease", "cancellationSignal"]), request = launch.request;
|
|
980
|
+
const lease = assertAgentTaskAccountLease(request), originalSignal = request.signal, cancellationSignal = launch.cancellationSignal;
|
|
981
|
+
check(request.route.provider === "codex" && request.route.authentication === "subscription" && launch.accountLease === lease && launch.runId === request.runId && launch.accountId === request.accountId && launch.workspaceId === request.workspaceId, "CODEX_MANAGED_PROCESS_BINDING_MISMATCH");
|
|
982
|
+
check(request.runtime.runtimeVersion === taskRuntime.version && request.runtime.runtimeDigest === taskRuntime.digest, "CODEX_MANAGED_PROCESS_RUNTIME_MAPPING_MISMATCH");
|
|
983
|
+
check(launch.configuration === codexManagedAccountConfiguration(), "CODEX_MANAGED_PROCESS_CONFIGURATION_MISMATCH");
|
|
984
|
+
check(cancellationSignal instanceof AbortSignal, "CODEX_MANAGED_PROCESS_SIGNAL_INVALID");
|
|
985
|
+
check(!seen.has(lease), "CODEX_MANAGED_PROCESS_DUPLICATE_RUN");
|
|
986
|
+
seen.add(lease);
|
|
987
|
+
const processGeneration = safeInteger(++generation, 1, Number.MAX_SAFE_INTEGER), configuration = codexManagedAccountConfiguration();
|
|
988
|
+
const binding = Object.freeze({
|
|
989
|
+
route: Object.freeze({ ...request.route }),
|
|
990
|
+
accountId: request.accountId,
|
|
991
|
+
workspaceId: request.workspaceId,
|
|
992
|
+
runId: request.runId,
|
|
993
|
+
profile: Object.freeze({ ...request.profile }),
|
|
994
|
+
model: Object.freeze({ ...request.model }),
|
|
995
|
+
runtime: Object.freeze({ ...request.runtime }),
|
|
996
|
+
accountLease: lease
|
|
997
|
+
});
|
|
998
|
+
return Promise.resolve(createOwnedCore({
|
|
999
|
+
stateRoot,
|
|
1000
|
+
runtime,
|
|
1001
|
+
host,
|
|
1002
|
+
binding,
|
|
1003
|
+
lease,
|
|
1004
|
+
processGeneration,
|
|
1005
|
+
configuration,
|
|
1006
|
+
runId: request.runId,
|
|
1007
|
+
originalSignal,
|
|
1008
|
+
cancellationSignal,
|
|
1009
|
+
startupMs,
|
|
1010
|
+
sandboxProfile,
|
|
1011
|
+
executionDeadline: request.executionDeadlineUnixMs,
|
|
1012
|
+
outerDeadline: request.cleanupDeadlineUnixMs,
|
|
1013
|
+
maxCleanupMs: request.limits.maxCleanupMs,
|
|
1014
|
+
schema: "xcb.codex-managed-process.v1",
|
|
1015
|
+
authority() {
|
|
1016
|
+
assertAgentTaskAccountLease(request);
|
|
1017
|
+
check(request.accountLease === lease && request.signal === originalSignal, "CODEX_MANAGED_PROCESS_BINDING_MISMATCH");
|
|
1018
|
+
}
|
|
1019
|
+
}));
|
|
1020
|
+
} });
|
|
1021
|
+
}
|
|
1022
|
+
function createOwnedCore(input) {
|
|
1023
|
+
const {
|
|
1024
|
+
stateRoot,
|
|
1025
|
+
runtime,
|
|
1026
|
+
host,
|
|
1027
|
+
binding,
|
|
1028
|
+
lease,
|
|
1029
|
+
processGeneration,
|
|
1030
|
+
configuration,
|
|
1031
|
+
runId,
|
|
1032
|
+
schema,
|
|
1033
|
+
originalSignal,
|
|
1034
|
+
cancellationSignal,
|
|
1035
|
+
startupMs,
|
|
1036
|
+
executionDeadline,
|
|
1037
|
+
outerDeadline,
|
|
1038
|
+
maxCleanupMs,
|
|
1039
|
+
authority
|
|
1040
|
+
} = input;
|
|
1041
|
+
const sandboxProfile = input.sandboxProfile ?? "managed-task-offline-candidate-v1";
|
|
1042
|
+
const network = sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v2" ? "general-tcp443-system-resolver-var-metadata-ca-file-candidate" : sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v1" ? "general-tcp443-system-resolver-var-metadata-candidate" : "denied";
|
|
1043
|
+
const owned = Object.freeze({ accountId: lease.accountId, owner: lease.owner, leaseGeneration: lease.generation, processGeneration });
|
|
1044
|
+
const accountRoot = join5(stateRoot, "accounts", lease.accountId), accountHome = join5(accountRoot, "codex-home"), runs = join5(stateRoot, "runs");
|
|
1045
|
+
const root = join5(runs, `task-${runId}-${processGeneration}-${randomBytes(12).toString("hex")}`), scratch = join5(root, "scratch"), runtimeRoot = join5(root, "runtime"), executable = join5(runtimeRoot, "codex"), cwd = join5(scratch, "work"), custodyPath = join5(root, "custody.jsonl"), lockPath = join5(accountRoot, "active.json");
|
|
1046
|
+
const lockContents = JSON.stringify({ schema: "xcb.codex-account-lock.v1", binding: owned, journalPath: custodyPath }) + `
|
|
1047
|
+
`;
|
|
1048
|
+
const startupDeadline = Math.min(executionDeadline, Date.now() + startupMs);
|
|
1049
|
+
const state = {
|
|
1050
|
+
phase: "preparing",
|
|
1051
|
+
launchAttempted: false,
|
|
1052
|
+
pid: null,
|
|
1053
|
+
pgid: null,
|
|
1054
|
+
rootExited: false,
|
|
1055
|
+
groupAbsent: false,
|
|
1056
|
+
stdioJoined: false,
|
|
1057
|
+
lockReleased: false,
|
|
1058
|
+
scratchRetained: false,
|
|
1059
|
+
nativeExitCode: null,
|
|
1060
|
+
nativeExitSignal: null,
|
|
1061
|
+
runtimeSnapshotSha256: "",
|
|
1062
|
+
profileSha256: "",
|
|
1063
|
+
scratchContentSha256: "",
|
|
1064
|
+
scratchIdentitySha256: ""
|
|
1065
|
+
};
|
|
1066
|
+
let child, lockOwned = false, pendingReleaseSync = false, journalFd, journalFailed = false, sequence = 0, previous = null, bridge;
|
|
1067
|
+
let scratchIdentity, runtimeIdentity, preparationSettled = false, nativeClosed = false, spawnEvent = false, spawnError = false, closing = false, pendingWrites = 0;
|
|
1068
|
+
let stopTask, cleanupDeadline, cleanupErrors = [], executionTimer;
|
|
1069
|
+
const runtimeErrors = new Set, nativeStreams = { stdin: false, stdout: false, stderr: false };
|
|
1070
|
+
let resolveExit, resolveClose;
|
|
1071
|
+
const exited = new Promise((done) => {
|
|
1072
|
+
resolveExit = done;
|
|
1073
|
+
}), closed = new Promise((done) => {
|
|
1074
|
+
resolveClose = done;
|
|
1075
|
+
});
|
|
1076
|
+
const streamClosures = [], stdout = new PassThrough;
|
|
1077
|
+
stdout.on("error", () => {});
|
|
1078
|
+
let inputClosed = false;
|
|
1079
|
+
const stdin = new Writable({ write(chunk, _encoding, done) {
|
|
1080
|
+
if (!child || closing || state.phase !== "running") {
|
|
1081
|
+
done(new Error("CODEX_MANAGED_PROCESS_STDIN_UNAVAILABLE"));
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
pendingWrites++;
|
|
1085
|
+
let settled = false;
|
|
1086
|
+
const settle = (error2) => {
|
|
1087
|
+
if (settled)
|
|
1088
|
+
return;
|
|
1089
|
+
settled = true;
|
|
1090
|
+
pendingWrites--;
|
|
1091
|
+
done(error2);
|
|
1092
|
+
};
|
|
1093
|
+
try {
|
|
1094
|
+
child.stdin.write(chunk, settle);
|
|
1095
|
+
} catch {
|
|
1096
|
+
settle(new Error("CODEX_MANAGED_PROCESS_STDIN_FAILED"));
|
|
1097
|
+
}
|
|
1098
|
+
} });
|
|
1099
|
+
stdin.on("error", () => {});
|
|
1100
|
+
function write(bytes) {
|
|
1101
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0)
|
|
1102
|
+
return Promise.reject(Error("CODEX_WRITE_INVALID"));
|
|
1103
|
+
if (!child || closing || state.phase !== "running")
|
|
1104
|
+
return Promise.resolve({ outcome: "refused-before-write", acceptedBytes: 0 });
|
|
1105
|
+
const copy = Buffer.from(bytes);
|
|
1106
|
+
return new Promise((resolve) => {
|
|
1107
|
+
try {
|
|
1108
|
+
stdin.write(copy, (error2) => resolve(error2 ? { outcome: "indeterminate", acceptedBytes: 0 } : providerProcessWriteResult({ outcome: "accepted-full", acceptedBytes: copy.byteLength }, copy.byteLength)));
|
|
1109
|
+
} catch {
|
|
1110
|
+
resolve({ outcome: "indeterminate", acceptedBytes: 0 });
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
const inputClosure = new Promise((done) => stdin.once("close", () => {
|
|
1115
|
+
inputClosed = true;
|
|
1116
|
+
done();
|
|
1117
|
+
}));
|
|
1118
|
+
const receipt = () => Object.freeze({
|
|
1119
|
+
schema,
|
|
1120
|
+
binding,
|
|
1121
|
+
processGeneration,
|
|
1122
|
+
productionQualified: false,
|
|
1123
|
+
network,
|
|
1124
|
+
profile: sandboxProfile,
|
|
1125
|
+
nativeVersion: runtime.version,
|
|
1126
|
+
executableSha256: runtime.sha256,
|
|
1127
|
+
schemaSha256: runtime.schemaSha256,
|
|
1128
|
+
parentRuntimeSha256: runtime.parentRuntime.expectedSha256,
|
|
1129
|
+
configSha256: hash(configuration),
|
|
1130
|
+
custodyPath,
|
|
1131
|
+
...state,
|
|
1132
|
+
runtimeErrors: Object.freeze([...runtimeErrors]),
|
|
1133
|
+
cleanupErrors: Object.freeze([...cleanupErrors])
|
|
1134
|
+
});
|
|
1135
|
+
const error = (code) => {
|
|
1136
|
+
if (runtimeErrors.size < 24)
|
|
1137
|
+
runtimeErrors.add(code);
|
|
1138
|
+
};
|
|
1139
|
+
function persist() {
|
|
1140
|
+
check(journalFd !== undefined && !journalFailed && sequence < 32, "CODEX_MANAGED_PROCESS_JOURNAL_FAILED");
|
|
1141
|
+
const line = JSON.stringify({ sequence, previousSha256: previous, snapshot: receipt() }) + `
|
|
1142
|
+
`;
|
|
1143
|
+
try {
|
|
1144
|
+
const bytes = Buffer.from(line);
|
|
1145
|
+
check(bytes.length <= 16 * 1024, "CODEX_MANAGED_PROCESS_JOURNAL_BOUND");
|
|
1146
|
+
let offset = 0;
|
|
1147
|
+
while (offset < bytes.length) {
|
|
1148
|
+
const count = writeSync(journalFd, bytes, offset, bytes.length - offset);
|
|
1149
|
+
check(count > 0, "CODEX_MANAGED_PROCESS_JOURNAL_FAILED");
|
|
1150
|
+
offset += count;
|
|
1151
|
+
}
|
|
1152
|
+
fsyncSync(journalFd);
|
|
1153
|
+
previous = hash(line);
|
|
1154
|
+
sequence++;
|
|
1155
|
+
} catch {
|
|
1156
|
+
journalFailed = true;
|
|
1157
|
+
fail3("CODEX_MANAGED_PROCESS_JOURNAL_FAILED");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function admitted() {
|
|
1161
|
+
authority();
|
|
1162
|
+
check(!originalSignal.aborted && !cancellationSignal.aborted && !closing && Date.now() < startupDeadline, "CODEX_MANAGED_PROCESS_START_CANCELLED");
|
|
1163
|
+
}
|
|
1164
|
+
const preparation = Promise.resolve().then(async () => {
|
|
1165
|
+
admitted();
|
|
1166
|
+
await directory(stateRoot);
|
|
1167
|
+
await directory(join5(stateRoot, "accounts"));
|
|
1168
|
+
await directory(accountRoot);
|
|
1169
|
+
await fixedFile(join5(accountRoot, "owner.json"), JSON.stringify({ schema: "xcb.codex-account-home.v1", accountId: owned.accountId }) + `
|
|
1170
|
+
`);
|
|
1171
|
+
const parent = await host.inspectParent(runtime.parentRuntime);
|
|
1172
|
+
check(parent.sha256 === runtime.parentRuntime.expectedSha256 && (parent.platform === "darwin" || parent.platform === "linux") && (parent.arch === "arm64" || parent.arch === "x64") && parent.version === "1.3.14", "CODEX_MANAGED_PROCESS_PARENT_MISMATCH");
|
|
1173
|
+
check(parent.platform === "linux" === (runtime.sandbox !== undefined), "CODEX_MANAGED_PROCESS_SANDBOX_MISMATCH");
|
|
1174
|
+
admitted();
|
|
1175
|
+
await inspectCodexHostExecutable(runtime.executablePath, runtime.sha256);
|
|
1176
|
+
admitted();
|
|
1177
|
+
await directory(runs);
|
|
1178
|
+
await mkdir4(root, { mode: 448 });
|
|
1179
|
+
await syncDirectory(runs);
|
|
1180
|
+
journalFd = openPrivateWriteSync(custodyPath);
|
|
1181
|
+
fsyncSync(journalFd);
|
|
1182
|
+
await syncDirectory(root);
|
|
1183
|
+
const lock = await openPrivateWrite(lockPath);
|
|
1184
|
+
lockOwned = true;
|
|
1185
|
+
try {
|
|
1186
|
+
await lock.writeFile(lockContents);
|
|
1187
|
+
await lock.sync();
|
|
1188
|
+
} finally {
|
|
1189
|
+
await lock.close();
|
|
1190
|
+
}
|
|
1191
|
+
await syncDirectory(accountRoot);
|
|
1192
|
+
persist();
|
|
1193
|
+
await directory(accountHome);
|
|
1194
|
+
await fixedFile(join5(accountHome, "config.toml"), configuration);
|
|
1195
|
+
admitted();
|
|
1196
|
+
await mkdir4(scratch, { mode: 448 });
|
|
1197
|
+
state.scratchRetained = true;
|
|
1198
|
+
scratchIdentity = await directory(scratch);
|
|
1199
|
+
await mkdir4(runtimeRoot, { mode: 448 });
|
|
1200
|
+
runtimeIdentity = await directory(runtimeRoot);
|
|
1201
|
+
for (const name of ["home", "tmp", "work"])
|
|
1202
|
+
await mkdir4(join5(scratch, name), { mode: 448 });
|
|
1203
|
+
await copyExecutable(runtime.executablePath, executable, runtime.sha256);
|
|
1204
|
+
state.runtimeSnapshotSha256 = runtime.sha256;
|
|
1205
|
+
const policyPath = join5(root, parent.platform === "darwin" ? "sandbox.sb" : "sandbox.json");
|
|
1206
|
+
if (parent.platform === "linux" && sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v1") {
|
|
1207
|
+
check(host.startEgressBridge !== undefined && runtime.sandbox.egress !== undefined, "CODEX_MANAGED_PROCESS_EGRESS_UNAVAILABLE");
|
|
1208
|
+
bridge = await host.startEgressBridge({ socketPath: join5(root, "egress.sock"), ...runtime.sandbox.egress.allowlist === undefined ? {} : { allowlist: runtime.sandbox.egress.allowlist } });
|
|
1209
|
+
}
|
|
1210
|
+
const sandboxPlan = parent.platform === "darwin" ? await createSeatbeltOsSandbox({ generateProfile: (spec) => (sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v2" ? codexManagedProviderV2Sandbox : sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v1" ? codexManagedProviderSandbox : codexManagedOfflineSandbox)({ executable: spec.executable, scratch: spec.scratch, accountHome: spec.accountHome }) }).plan({ platform: "darwin", executable, scratch, accountHome, network: sandboxProfile === "managed-task-offline-candidate-v1" ? "denied" : "provider-tcp443-dns", policyPath }) : await createBwrapOsSandbox({ executable: runtime.sandbox.executable, sha256: runtime.sandbox.sha256 }).plan({
|
|
1211
|
+
platform: "linux",
|
|
1212
|
+
executable,
|
|
1213
|
+
scratch,
|
|
1214
|
+
accountHome,
|
|
1215
|
+
readOnlyPaths: runtime.sandbox.readOnlyPaths ?? [],
|
|
1216
|
+
network: sandboxProfile === "managed-task-offline-candidate-v1" ? "denied" : "provider-tcp443-dns",
|
|
1217
|
+
...bridge === undefined ? {} : { egressSocket: bridge.socketPath },
|
|
1218
|
+
policyPath
|
|
1219
|
+
});
|
|
1220
|
+
state.profileSha256 = sandboxPlan.policySha256;
|
|
1221
|
+
await durableFile(policyPath, sandboxPlan.policy);
|
|
1222
|
+
await syncDirectory(runtimeRoot);
|
|
1223
|
+
const inspected = await inspectScratch(scratch);
|
|
1224
|
+
state.scratchContentSha256 = inspected.content;
|
|
1225
|
+
state.scratchIdentitySha256 = inspected.identity;
|
|
1226
|
+
await fixedFile(join5(accountHome, "config.toml"), configuration);
|
|
1227
|
+
await fixedFile(lockPath, lockContents);
|
|
1228
|
+
await directory(accountHome);
|
|
1229
|
+
admitted();
|
|
1230
|
+
state.phase = "launch-pending";
|
|
1231
|
+
state.launchAttempted = true;
|
|
1232
|
+
persist();
|
|
1233
|
+
const wrapped = sandboxPlan.wrap({
|
|
1234
|
+
args: Object.freeze(["app-server", "--strict-config", "--listen", "stdio://"]),
|
|
1235
|
+
cwd,
|
|
1236
|
+
env: Object.freeze({
|
|
1237
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
1238
|
+
HOME: join5(scratch, "home"),
|
|
1239
|
+
CODEX_HOME: accountHome,
|
|
1240
|
+
TMPDIR: join5(scratch, "tmp"),
|
|
1241
|
+
NO_COLOR: "1",
|
|
1242
|
+
CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED: "1",
|
|
1243
|
+
...sandboxProfile === "managed-task-provider-tcp443-dns-candidate-v2" && parent.platform === "darwin" ? { SSL_CERT_FILE: "/etc/ssl/cert.pem" } : {},
|
|
1244
|
+
...bridge === undefined ? {} : { XCB_EGRESS_SOCKET: bridge.socketPath }
|
|
1245
|
+
})
|
|
1246
|
+
});
|
|
1247
|
+
child = host.spawn(Object.freeze({
|
|
1248
|
+
executable: sandboxPlan.executable,
|
|
1249
|
+
args: wrapped.args,
|
|
1250
|
+
cwd,
|
|
1251
|
+
env: wrapped.env,
|
|
1252
|
+
detached: true,
|
|
1253
|
+
stdio: Object.freeze(["pipe", "pipe", "pipe"])
|
|
1254
|
+
}));
|
|
1255
|
+
state.pid = child.pid ?? null;
|
|
1256
|
+
child.once("spawn", () => {
|
|
1257
|
+
spawnEvent = true;
|
|
1258
|
+
});
|
|
1259
|
+
child.once("exit", (code, signal) => {
|
|
1260
|
+
state.rootExited = true;
|
|
1261
|
+
state.nativeExitCode = code;
|
|
1262
|
+
state.nativeExitSignal = signal;
|
|
1263
|
+
resolveExit();
|
|
1264
|
+
});
|
|
1265
|
+
child.once("close", () => {
|
|
1266
|
+
nativeClosed = true;
|
|
1267
|
+
if (!spawnEvent && spawnError && state.pid === null) {
|
|
1268
|
+
state.rootExited = true;
|
|
1269
|
+
state.groupAbsent = true;
|
|
1270
|
+
resolveExit();
|
|
1271
|
+
}
|
|
1272
|
+
resolveClose();
|
|
1273
|
+
});
|
|
1274
|
+
child.once("error", () => {
|
|
1275
|
+
spawnError = true;
|
|
1276
|
+
error("native-spawn");
|
|
1277
|
+
});
|
|
1278
|
+
for (const name of ["stdin", "stdout", "stderr"])
|
|
1279
|
+
streamClosures.push(new Promise((done) => child[name].once("close", () => {
|
|
1280
|
+
nativeStreams[name] = true;
|
|
1281
|
+
done();
|
|
1282
|
+
})));
|
|
1283
|
+
let stdoutBytes = 0, stderrBytes = 0;
|
|
1284
|
+
const streamFailed = (code) => {
|
|
1285
|
+
error(code);
|
|
1286
|
+
stopAndJoin().catch(() => {});
|
|
1287
|
+
};
|
|
1288
|
+
child.stdout.on("data", (chunk) => {
|
|
1289
|
+
stdoutBytes += chunk.length;
|
|
1290
|
+
if (stdoutBytes > 16 * 1024 * 1024)
|
|
1291
|
+
streamFailed("stdout-bound");
|
|
1292
|
+
else if (!closing)
|
|
1293
|
+
stdout.write(chunk);
|
|
1294
|
+
});
|
|
1295
|
+
child.stderr.on("data", (chunk) => {
|
|
1296
|
+
stderrBytes += chunk.length;
|
|
1297
|
+
if (stderrBytes > 256 * 1024)
|
|
1298
|
+
streamFailed("stderr-bound");
|
|
1299
|
+
});
|
|
1300
|
+
child.stdout.on("end", () => {
|
|
1301
|
+
stdout.end();
|
|
1302
|
+
});
|
|
1303
|
+
for (const name of ["stdin", "stdout", "stderr"])
|
|
1304
|
+
child[name].on("error", () => streamFailed(`native-${name}`));
|
|
1305
|
+
persist();
|
|
1306
|
+
check(state.pid !== null && Number.isSafeInteger(state.pid) && state.pid > 0, "CODEX_MANAGED_PROCESS_PID_UNPROVEN");
|
|
1307
|
+
state.pgid = host.processGroup(state.pid);
|
|
1308
|
+
check(state.pgid === state.pid, "CODEX_MANAGED_PROCESS_GROUP_UNPROVEN");
|
|
1309
|
+
admitted();
|
|
1310
|
+
check(!state.rootExited, "CODEX_MANAGED_PROCESS_PREMATURE_EXIT");
|
|
1311
|
+
state.phase = "running";
|
|
1312
|
+
persist();
|
|
1313
|
+
}).catch((caught) => {
|
|
1314
|
+
error(caught instanceof Error && /^(?:CODEX|TASK)_[A-Z_]+$/u.test(caught.message) ? caught.message : "preparation-failed");
|
|
1315
|
+
if (!state.launchAttempted) {
|
|
1316
|
+
state.rootExited = true;
|
|
1317
|
+
state.groupAbsent = true;
|
|
1318
|
+
resolveExit();
|
|
1319
|
+
}
|
|
1320
|
+
throw new Error("CODEX_MANAGED_PROCESS_PREPARATION_FAILED");
|
|
1321
|
+
}).finally(() => {
|
|
1322
|
+
preparationSettled = true;
|
|
1323
|
+
});
|
|
1324
|
+
preparation.catch(() => {});
|
|
1325
|
+
function cancel() {
|
|
1326
|
+
stopAndJoin().catch(() => {});
|
|
1327
|
+
}
|
|
1328
|
+
originalSignal.addEventListener("abort", cancel, { once: true });
|
|
1329
|
+
cancellationSignal.addEventListener("abort", cancel, { once: true });
|
|
1330
|
+
executionTimer = setTimeout(cancel, Math.max(0, executionDeadline - Date.now()));
|
|
1331
|
+
if (originalSignal.aborted || cancellationSignal.aborted)
|
|
1332
|
+
cancel();
|
|
1333
|
+
const ready = bounded(preparation, startupDeadline).catch(() => {
|
|
1334
|
+
error("startup-failed");
|
|
1335
|
+
cancel();
|
|
1336
|
+
throw new Error("CODEX_MANAGED_PROCESS_UNAVAILABLE");
|
|
1337
|
+
});
|
|
1338
|
+
ready.catch(() => {});
|
|
1339
|
+
function stopAndJoin() {
|
|
1340
|
+
if (stopTask !== undefined)
|
|
1341
|
+
return stopTask;
|
|
1342
|
+
closing = true;
|
|
1343
|
+
cleanupDeadline ??= Math.min(outerDeadline, Date.now() + maxCleanupMs);
|
|
1344
|
+
const deadline = cleanupDeadline;
|
|
1345
|
+
clearTimeout(executionTimer);
|
|
1346
|
+
originalSignal.removeEventListener("abort", cancel);
|
|
1347
|
+
cancellationSignal.removeEventListener("abort", cancel);
|
|
1348
|
+
const task = Promise.resolve().then(async () => {
|
|
1349
|
+
cleanupErrors = [];
|
|
1350
|
+
try {
|
|
1351
|
+
if (!preparationSettled)
|
|
1352
|
+
await bounded(preparation.catch(() => {}), deadline);
|
|
1353
|
+
if (child) {
|
|
1354
|
+
try {
|
|
1355
|
+
child.stdin.end();
|
|
1356
|
+
} catch {
|
|
1357
|
+
error("stdin-end");
|
|
1358
|
+
}
|
|
1359
|
+
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
1360
|
+
if (state.rootExited || Date.now() >= deadline)
|
|
1361
|
+
break;
|
|
1362
|
+
if (state.pgid !== null && state.pgid === state.pid)
|
|
1363
|
+
host.signalGroup(state.pgid, signal);
|
|
1364
|
+
else
|
|
1365
|
+
child.kill(signal);
|
|
1366
|
+
if (!state.rootExited)
|
|
1367
|
+
try {
|
|
1368
|
+
await bounded(exited, Math.min(deadline, Date.now() + 250));
|
|
1369
|
+
} catch {}
|
|
1370
|
+
}
|
|
1371
|
+
if (state.rootExited && state.pgid === state.pid && state.pgid !== null)
|
|
1372
|
+
state.groupAbsent = !host.signalGroup(state.pgid, 0);
|
|
1373
|
+
if (!nativeClosed || !nativeStreams.stdin || !nativeStreams.stdout || !nativeStreams.stderr)
|
|
1374
|
+
await bounded(Promise.all([closed, ...streamClosures]), deadline);
|
|
1375
|
+
check(nativeClosed && state.rootExited && state.groupAbsent && nativeStreams.stdin && nativeStreams.stdout && nativeStreams.stderr && pendingWrites === 0, "CODEX_MANAGED_PROCESS_STOP_UNPROVEN");
|
|
1376
|
+
} else
|
|
1377
|
+
check(preparationSettled && !state.launchAttempted && state.rootExited, "CODEX_MANAGED_PROCESS_LAUNCH_UNCERTAIN");
|
|
1378
|
+
stdin.end();
|
|
1379
|
+
stdin.destroy();
|
|
1380
|
+
stdout.end();
|
|
1381
|
+
stdout.resume();
|
|
1382
|
+
if (!inputClosed)
|
|
1383
|
+
await bounded(inputClosure, deadline);
|
|
1384
|
+
if (!stdout.readableEnded)
|
|
1385
|
+
await bounded(new Promise((done) => stdout.once("end", done)), deadline);
|
|
1386
|
+
check(inputClosed && pendingWrites === 0, "CODEX_MANAGED_PROCESS_WRITES_UNJOINED");
|
|
1387
|
+
state.stdioJoined = true;
|
|
1388
|
+
if (bridge !== undefined) {
|
|
1389
|
+
const receipt2 = await bridge.close();
|
|
1390
|
+
check(receipt2.listenerClosed && receipt2.socketsJoined && receipt2.socketRemoved, "CODEX_MANAGED_PROCESS_EGRESS_UNJOINED");
|
|
1391
|
+
bridge = undefined;
|
|
1392
|
+
}
|
|
1393
|
+
if (lockOwned) {
|
|
1394
|
+
check(!journalFailed, "CODEX_MANAGED_PROCESS_JOURNAL_FAILED");
|
|
1395
|
+
await fixedFile(lockPath, lockContents);
|
|
1396
|
+
if (scratchIdentity && state.scratchRetained) {
|
|
1397
|
+
const current = await directory(scratch);
|
|
1398
|
+
check(current.dev === scratchIdentity.dev && current.ino === scratchIdentity.ino, "CODEX_MANAGED_PROCESS_SCRATCH_CHANGED");
|
|
1399
|
+
await rm2(scratch, { recursive: true });
|
|
1400
|
+
state.scratchRetained = false;
|
|
1401
|
+
}
|
|
1402
|
+
if (runtimeIdentity) {
|
|
1403
|
+
const current = await directory(runtimeRoot);
|
|
1404
|
+
check(current.dev === runtimeIdentity.dev && current.ino === runtimeIdentity.ino, "CODEX_MANAGED_PROCESS_RUNTIME_CHANGED");
|
|
1405
|
+
await rm2(runtimeRoot, { recursive: true });
|
|
1406
|
+
runtimeIdentity = undefined;
|
|
1407
|
+
}
|
|
1408
|
+
state.phase = "release-pending";
|
|
1409
|
+
persist();
|
|
1410
|
+
await unlink(lockPath);
|
|
1411
|
+
lockOwned = false;
|
|
1412
|
+
pendingReleaseSync = true;
|
|
1413
|
+
}
|
|
1414
|
+
if (pendingReleaseSync) {
|
|
1415
|
+
await host.syncDirectory(accountRoot);
|
|
1416
|
+
pendingReleaseSync = false;
|
|
1417
|
+
state.lockReleased = true;
|
|
1418
|
+
} else if (!state.lockReleased)
|
|
1419
|
+
state.lockReleased = !state.launchAttempted;
|
|
1420
|
+
check(state.lockReleased && !journalFailed, "CODEX_MANAGED_PROCESS_RELEASE_UNPROVEN");
|
|
1421
|
+
state.phase = "closed";
|
|
1422
|
+
if (journalFd !== undefined) {
|
|
1423
|
+
persist();
|
|
1424
|
+
closeSync(journalFd);
|
|
1425
|
+
journalFd = undefined;
|
|
1426
|
+
}
|
|
1427
|
+
} catch {
|
|
1428
|
+
state.phase = "recovery-required";
|
|
1429
|
+
cleanupErrors = ["cleanup-unproven"];
|
|
1430
|
+
if (journalFd !== undefined && !journalFailed)
|
|
1431
|
+
try {
|
|
1432
|
+
persist();
|
|
1433
|
+
} catch {}
|
|
1434
|
+
}
|
|
1435
|
+
return receipt();
|
|
1436
|
+
});
|
|
1437
|
+
stopTask = task;
|
|
1438
|
+
task.then((result) => {
|
|
1439
|
+
if (result.phase !== "closed" && stopTask === task)
|
|
1440
|
+
stopTask = undefined;
|
|
1441
|
+
});
|
|
1442
|
+
return task;
|
|
1443
|
+
}
|
|
1444
|
+
const handle = Object.freeze({ cwd, stdin, write, stdout, exited, ready, receipt, stopAndJoin });
|
|
1445
|
+
return handle;
|
|
1446
|
+
}
|
|
1447
|
+
var retainedOfflineDiagnostics = new Map;
|
|
1448
|
+
async function assertNoDiagnosticAuth(accountHome) {
|
|
1449
|
+
try {
|
|
1450
|
+
await lstat3(join5(accountHome, "auth.json"));
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (error.code === "ENOENT")
|
|
1453
|
+
return;
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
fail3("OFFLINE_DIAGNOSTIC_AUTH_STATE_UNEXPECTED");
|
|
1457
|
+
}
|
|
1458
|
+
function runCodexManagedOfflineDiagnostic(options, trustedSystem = system) {
|
|
1459
|
+
const raw = record(options, ["directory", "runtime", "signal", "admission"]), parentDirectory = path(raw.directory), signal = raw.signal;
|
|
1460
|
+
check(signal instanceof AbortSignal, "OFFLINE_DIAGNOSTIC_SIGNAL_INVALID");
|
|
1461
|
+
const r = record(raw.runtime, ["executablePath", "version", "sha256", "schemaSha256", "parentRuntime", "sandbox"]), p = record(r.parentRuntime, ["expectedSha256"]);
|
|
1462
|
+
const runtimeSandbox = sandboxOf(r.sandbox);
|
|
1463
|
+
const runtime = Object.freeze({
|
|
1464
|
+
executablePath: path(r.executablePath),
|
|
1465
|
+
version: identifier(r.version),
|
|
1466
|
+
sha256: digest(r.sha256),
|
|
1467
|
+
schemaSha256: digest(r.schemaSha256),
|
|
1468
|
+
parentRuntime: Object.freeze({ expectedSha256: digest(p.expectedSha256) }),
|
|
1469
|
+
...runtimeSandbox === undefined ? {} : { sandbox: runtimeSandbox }
|
|
1470
|
+
});
|
|
1471
|
+
const admission = record(raw.admission, ["kind", "nativeSha256", "schemaSha256", "parentSha256"]);
|
|
1472
|
+
check(admission.kind === "managed-offline-lifecycle-diagnostic-v1" && digest(admission.nativeSha256) === runtime.sha256 && digest(admission.schemaSha256) === runtime.schemaSha256 && digest(admission.parentSha256) === runtime.parentRuntime.expectedSha256, "OFFLINE_DIAGNOSTIC_ADMISSION_MISMATCH");
|
|
1473
|
+
const host = Object.freeze({ inspectParent: trustedSystem.inspectParent.bind(trustedSystem), spawn: trustedSystem.spawn.bind(trustedSystem), processGroup: trustedSystem.processGroup.bind(trustedSystem), signalGroup: trustedSystem.signalGroup.bind(trustedSystem), syncDirectory: trustedSystem.syncDirectory?.bind(trustedSystem) ?? syncDirectory });
|
|
1474
|
+
const outerDeadline = Date.now() + 60000, executionDeadline = outerDeadline - 15000;
|
|
1475
|
+
return Promise.resolve().then(async () => {
|
|
1476
|
+
signal.throwIfAborted();
|
|
1477
|
+
await directory(parentDirectory);
|
|
1478
|
+
signal.throwIfAborted();
|
|
1479
|
+
const root = await mkdtemp(join5(parentDirectory, "managed-offline-diagnostic-"));
|
|
1480
|
+
await directory(root);
|
|
1481
|
+
await syncDirectory(parentDirectory);
|
|
1482
|
+
const stateRoot = join5(root, "state");
|
|
1483
|
+
await mkdir4(stateRoot, { mode: 448 });
|
|
1484
|
+
await syncDirectory(root);
|
|
1485
|
+
const databasePath = join5(root, "account-leases.sqlite"), journalPath = join5(root, "diagnostic.jsonl"), receiptPath = join5(root, "receipt.json");
|
|
1486
|
+
await durableFile(databasePath, "");
|
|
1487
|
+
await durableFile(journalPath, "");
|
|
1488
|
+
let acquiredJournal, acquiredDatabase, leases;
|
|
1489
|
+
try {
|
|
1490
|
+
acquiredJournal = await openPrivateWrite(journalPath, { create: false, exclusive: false, append: true });
|
|
1491
|
+
acquiredDatabase = await openAccountDatabase(databasePath);
|
|
1492
|
+
leases = new SqliteAccountLeases(acquiredDatabase);
|
|
1493
|
+
} catch (error2) {
|
|
1494
|
+
try {
|
|
1495
|
+
acquiredDatabase?.close();
|
|
1496
|
+
} finally {
|
|
1497
|
+
await acquiredJournal?.close();
|
|
1498
|
+
}
|
|
1499
|
+
throw error2;
|
|
1500
|
+
}
|
|
1501
|
+
const journal = acquiredJournal, database = acquiredDatabase;
|
|
1502
|
+
const custody = { database, journal, joins: new Set };
|
|
1503
|
+
retainedOfflineDiagnostics.set(root, custody);
|
|
1504
|
+
function retain(work) {
|
|
1505
|
+
custody.joins.add(work);
|
|
1506
|
+
work.then(() => custody.joins.delete(work), () => custody.joins.delete(work));
|
|
1507
|
+
return work;
|
|
1508
|
+
}
|
|
1509
|
+
const diagnosticId = randomBytes(12).toString("hex"), accountId = `diagnostic-${diagnosticId}`, accountHome = join5(stateRoot, "accounts", accountId, "codex-home"), configuration = codexManagedAccountConfiguration();
|
|
1510
|
+
const controller = new AbortController, interrupt = () => controller.abort();
|
|
1511
|
+
signal.addEventListener("abort", interrupt, { once: true });
|
|
1512
|
+
if (signal.aborted)
|
|
1513
|
+
controller.abort();
|
|
1514
|
+
const timer = setTimeout(interrupt, Math.max(0, executionDeadline - Date.now()));
|
|
1515
|
+
let lease, bootstrap, core;
|
|
1516
|
+
let protocol, bootstrapJoined = false, processJoined = false, protocolReceipt = null, sequence = 0, journalFailed = false;
|
|
1517
|
+
const failures = new Set, error = (code) => {
|
|
1518
|
+
if (failures.size < 16)
|
|
1519
|
+
failures.add(code);
|
|
1520
|
+
};
|
|
1521
|
+
async function persist(event, detail) {
|
|
1522
|
+
try {
|
|
1523
|
+
check(++sequence <= 12, "OFFLINE_DIAGNOSTIC_JOURNAL_BOUND");
|
|
1524
|
+
const line = JSON.stringify({ sequence, event, detail }) + `
|
|
1525
|
+
`;
|
|
1526
|
+
check(Buffer.byteLength(line) <= 16 * 1024, "OFFLINE_DIAGNOSTIC_JOURNAL_BOUND");
|
|
1527
|
+
await journal.writeFile(line);
|
|
1528
|
+
await journal.sync();
|
|
1529
|
+
} catch {
|
|
1530
|
+
journalFailed = true;
|
|
1531
|
+
throw Error("OFFLINE_DIAGNOSTIC_JOURNAL_FAILED");
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const cleanupDeadline = () => Math.min(outerDeadline, Date.now() + 1e4);
|
|
1535
|
+
function active() {
|
|
1536
|
+
controller.signal.throwIfAborted();
|
|
1537
|
+
check(Date.now() < executionDeadline, "OFFLINE_DIAGNOSTIC_DEADLINE");
|
|
1538
|
+
}
|
|
1539
|
+
let bootstrapCleanupDeadline;
|
|
1540
|
+
async function joinBootstrap() {
|
|
1541
|
+
if (!bootstrap)
|
|
1542
|
+
return;
|
|
1543
|
+
bootstrapCleanupDeadline ??= cleanupDeadline();
|
|
1544
|
+
const stopped = await retain(bootstrap.stopAndJoin({ binding: bootstrap.binding, deadlineMs: bootstrapCleanupDeadline })), receipt2 = bootstrap.receipt();
|
|
1545
|
+
bootstrapJoined = stopped.processExited && stopped.processGroupStopped && stopped.stdoutEnded && stopped.stderrEnded && JSON.stringify(stopped.binding) === JSON.stringify(bootstrap.binding) && receipt2.phase === "closed" && receipt2.lockReleased && !receipt2.scratchRetained;
|
|
1546
|
+
check(bootstrapJoined, "OFFLINE_DIAGNOSTIC_BOOTSTRAP_UNJOINED");
|
|
1547
|
+
if (receipt2.failures.length !== 0)
|
|
1548
|
+
error("OFFLINE_DIAGNOSTIC_BOOTSTRAP_RUNTIME_FAILED");
|
|
1549
|
+
}
|
|
1550
|
+
try {
|
|
1551
|
+
await persist("prepared", { nativeSha256: runtime.sha256, schemaSha256: runtime.schemaSha256, parentSha256: runtime.parentRuntime.expectedSha256, configurationSha256: hash(configuration), productionQualified: false, network: "denied" });
|
|
1552
|
+
active();
|
|
1553
|
+
const bootstrapBegan = Date.now();
|
|
1554
|
+
lease = leases.acquire({ provider: "codex", accountId, owner: `bootstrap-${diagnosticId}`, now: bootstrapBegan, ttlMs: outerDeadline - bootstrapBegan });
|
|
1555
|
+
bootstrap = createCodexAccountProcess({ stateRoot, mode: "offline", runtime, binding: { accountId, owner: lease.owner, leaseGeneration: lease.generation, processGeneration: 1 }, startupTimeoutMs: 1e4 }, host);
|
|
1556
|
+
custody.bootstrap = bootstrap;
|
|
1557
|
+
await bounded(bootstrap.ready, Math.min(executionDeadline, Date.now() + 1e4));
|
|
1558
|
+
controller.signal.throwIfAborted();
|
|
1559
|
+
await joinBootstrap();
|
|
1560
|
+
check(failures.size === 0, "OFFLINE_DIAGNOSTIC_BOOTSTRAP_RUNTIME_FAILED");
|
|
1561
|
+
await persist("bootstrap-joined", { process: bootstrap.receipt(), leaseGeneration: lease.generation });
|
|
1562
|
+
check(!journalFailed && leases.release(lease), "OFFLINE_DIAGNOSTIC_BOOTSTRAP_RELEASE_FAILED");
|
|
1563
|
+
lease = undefined;
|
|
1564
|
+
active();
|
|
1565
|
+
await fixedFile(join5(accountHome, "config.toml"), configuration);
|
|
1566
|
+
await assertNoDiagnosticAuth(accountHome);
|
|
1567
|
+
active();
|
|
1568
|
+
const taskBegan = Date.now();
|
|
1569
|
+
lease = leases.acquire({ provider: "codex", accountId, owner: `diagnostic-${diagnosticId}`, now: taskBegan, ttlMs: outerDeadline - taskBegan });
|
|
1570
|
+
const admittedLease = lease, binding = Object.freeze({ kind: "offline-diagnostic", diagnosticId, accountId, accountLease: admittedLease });
|
|
1571
|
+
await persist("diagnostic-admitted", { kind: binding.kind, accountId, leaseGeneration: lease.generation });
|
|
1572
|
+
core = createOwnedCore({
|
|
1573
|
+
stateRoot,
|
|
1574
|
+
runtime,
|
|
1575
|
+
host,
|
|
1576
|
+
binding,
|
|
1577
|
+
lease: admittedLease,
|
|
1578
|
+
processGeneration: 2,
|
|
1579
|
+
configuration,
|
|
1580
|
+
runId: `diagnostic-${diagnosticId}`,
|
|
1581
|
+
schema: "xcb.codex-managed-offline-process.v1",
|
|
1582
|
+
originalSignal: controller.signal,
|
|
1583
|
+
cancellationSignal: controller.signal,
|
|
1584
|
+
startupMs: 1e4,
|
|
1585
|
+
executionDeadline,
|
|
1586
|
+
outerDeadline,
|
|
1587
|
+
maxCleanupMs: 1e4,
|
|
1588
|
+
authority() {
|
|
1589
|
+
check(lease === admittedLease && JSON.stringify(leases.inspect("codex", accountId)) === JSON.stringify(admittedLease), "OFFLINE_DIAGNOSTIC_LEASE_CHANGED");
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
custody.core = core;
|
|
1593
|
+
protocol = createManagedOfflineProtocol(core, accountHome, controller.signal, executionDeadline);
|
|
1594
|
+
custody.protocol = protocol;
|
|
1595
|
+
await protocol.result;
|
|
1596
|
+
controller.signal.throwIfAborted();
|
|
1597
|
+
await fixedFile(join5(accountHome, "config.toml"), configuration);
|
|
1598
|
+
await assertNoDiagnosticAuth(accountHome);
|
|
1599
|
+
} catch (caught) {
|
|
1600
|
+
error(caught instanceof Error && /^(?:OFFLINE_DIAGNOSTIC|CODEX_MANAGED_PROCESS|CODEX_ACCOUNT_PROCESS)_[A-Z_]+$/u.test(caught.message) ? caught.message : controller.signal.aborted ? "OFFLINE_DIAGNOSTIC_CANCELLED" : "OFFLINE_DIAGNOSTIC_FAILED");
|
|
1601
|
+
} finally {
|
|
1602
|
+
protocol?.stop();
|
|
1603
|
+
controller.abort();
|
|
1604
|
+
clearTimeout(timer);
|
|
1605
|
+
signal.removeEventListener("abort", interrupt);
|
|
1606
|
+
try {
|
|
1607
|
+
if (core) {
|
|
1608
|
+
const stopped = await retain(core.stopAndJoin());
|
|
1609
|
+
processJoined = stopped.phase === "closed" && stopped.rootExited && stopped.groupAbsent && stopped.stdioJoined && stopped.lockReleased && stopped.cleanupErrors.length === 0;
|
|
1610
|
+
if (stopped.runtimeErrors.length !== 0)
|
|
1611
|
+
error("OFFLINE_DIAGNOSTIC_PROCESS_RUNTIME_FAILED");
|
|
1612
|
+
check(processJoined, "OFFLINE_DIAGNOSTIC_PROCESS_UNJOINED");
|
|
1613
|
+
await persist("diagnostic-process-joined", { process: stopped });
|
|
1614
|
+
} else
|
|
1615
|
+
await joinBootstrap();
|
|
1616
|
+
} catch {
|
|
1617
|
+
error("OFFLINE_DIAGNOSTIC_PROCESS_UNJOINED");
|
|
1618
|
+
}
|
|
1619
|
+
try {
|
|
1620
|
+
if (protocol) {
|
|
1621
|
+
protocolReceipt = await bounded(retain(protocol.join()), cleanupDeadline());
|
|
1622
|
+
check(protocolReceipt.joined, "OFFLINE_DIAGNOSTIC_PROTOCOL_UNJOINED");
|
|
1623
|
+
if (protocolReceipt.failure)
|
|
1624
|
+
error(protocolReceipt.failure);
|
|
1625
|
+
}
|
|
1626
|
+
} catch {
|
|
1627
|
+
error("OFFLINE_DIAGNOSTIC_PROTOCOL_UNJOINED");
|
|
1628
|
+
protocolReceipt = protocol?.receipt() ?? null;
|
|
1629
|
+
}
|
|
1630
|
+
try {
|
|
1631
|
+
if (lease && !journalFailed && (core ? processJoined && protocolReceipt?.joined === true : bootstrapJoined)) {
|
|
1632
|
+
await persist("lease-release", { owner: core ? "diagnostic" : "bootstrap", generation: lease.generation });
|
|
1633
|
+
check(leases.release(lease), "OFFLINE_DIAGNOSTIC_RELEASE_FAILED");
|
|
1634
|
+
lease = undefined;
|
|
1635
|
+
}
|
|
1636
|
+
} catch {
|
|
1637
|
+
error("OFFLINE_DIAGNOSTIC_RELEASE_FAILED");
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const leaseReleased = lease === undefined && leases.inspect("codex", accountId) === null;
|
|
1641
|
+
const receipt = Object.freeze({
|
|
1642
|
+
schema: "xcb.codex-managed-offline-diagnostic.v1",
|
|
1643
|
+
passed: failures.size === 0 && bootstrapJoined && processJoined && leaseReleased && protocolReceipt?.configurationObserved === true,
|
|
1644
|
+
productionQualified: false,
|
|
1645
|
+
network: "denied",
|
|
1646
|
+
root,
|
|
1647
|
+
receiptPath,
|
|
1648
|
+
configurationSha256: hash(configuration),
|
|
1649
|
+
nativeSha256: runtime.sha256,
|
|
1650
|
+
schemaSha256: runtime.schemaSha256,
|
|
1651
|
+
parentSha256: runtime.parentRuntime.expectedSha256,
|
|
1652
|
+
bootstrapJoined,
|
|
1653
|
+
processJoined,
|
|
1654
|
+
leaseReleased,
|
|
1655
|
+
protocol: protocolReceipt,
|
|
1656
|
+
failures: Object.freeze([...failures])
|
|
1657
|
+
});
|
|
1658
|
+
try {
|
|
1659
|
+
await persist("finished", receipt);
|
|
1660
|
+
} finally {
|
|
1661
|
+
if (leaseReleased && (!bootstrap || bootstrapJoined) && (!core || processJoined) && (!protocol || protocolReceipt?.joined === true) && custody.joins.size === 0) {
|
|
1662
|
+
await journal.close();
|
|
1663
|
+
database.close();
|
|
1664
|
+
retainedOfflineDiagnostics.delete(root);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
await durableFile(receiptPath, JSON.stringify(receipt) + `
|
|
1668
|
+
`);
|
|
1669
|
+
return receipt;
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/cli/qualification.ts
|
|
1674
|
+
import { lstat as lstat4, rename } from "node:fs/promises";
|
|
1675
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1676
|
+
import { join as join6 } from "node:path";
|
|
1677
|
+
var MAX_QUALIFICATION_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1678
|
+
var RECORD_VERSION = "xcb.cli-qualification.v1";
|
|
1679
|
+
var fail4 = (code) => {
|
|
1680
|
+
throw new Error(code);
|
|
1681
|
+
};
|
|
1682
|
+
var digest2 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : fail4("QUALIFICATION_DIGEST_INVALID");
|
|
1683
|
+
function recordPath(root, provider) {
|
|
1684
|
+
return join6(root, `qualification-${identifier(provider)}.json`);
|
|
1685
|
+
}
|
|
1686
|
+
function parseRecord(value) {
|
|
1687
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
1688
|
+
fail4("QUALIFICATION_RECORD_INVALID");
|
|
1689
|
+
const raw = value;
|
|
1690
|
+
if (Reflect.ownKeys(raw).sort().join(",") !== "admittedAtUnixMs,authentication,evidenceDigest,executablePath,executableSha256,expiresAtUnixMs,profileDigest,provider,routeId,runtimeDigest,runtimeVersion,version") {
|
|
1691
|
+
fail4("QUALIFICATION_RECORD_INVALID");
|
|
1692
|
+
}
|
|
1693
|
+
const record2 = Object.freeze({
|
|
1694
|
+
version: RECORD_VERSION,
|
|
1695
|
+
provider: boundedText(raw.provider, 32),
|
|
1696
|
+
routeId: identifier(raw.routeId),
|
|
1697
|
+
authentication: boundedText(raw.authentication, 32),
|
|
1698
|
+
executablePath: boundedText(raw.executablePath, 4096),
|
|
1699
|
+
executableSha256: digest2(raw.executableSha256),
|
|
1700
|
+
runtimeVersion: boundedText(raw.runtimeVersion, 160),
|
|
1701
|
+
runtimeDigest: digest2(raw.runtimeDigest),
|
|
1702
|
+
profileDigest: digest2(raw.profileDigest),
|
|
1703
|
+
evidenceDigest: digest2(raw.evidenceDigest),
|
|
1704
|
+
admittedAtUnixMs: safeInteger(raw.admittedAtUnixMs, 0, Number.MAX_SAFE_INTEGER),
|
|
1705
|
+
expiresAtUnixMs: safeInteger(raw.expiresAtUnixMs, 0, Number.MAX_SAFE_INTEGER)
|
|
1706
|
+
});
|
|
1707
|
+
if (raw.version !== RECORD_VERSION || record2.expiresAtUnixMs <= record2.admittedAtUnixMs)
|
|
1708
|
+
fail4("QUALIFICATION_RECORD_INVALID");
|
|
1709
|
+
return record2;
|
|
1710
|
+
}
|
|
1711
|
+
async function readCliQualification(stateRoot, provider) {
|
|
1712
|
+
const root = await privateDirectory(stateRoot);
|
|
1713
|
+
const path2 = recordPath(root, provider);
|
|
1714
|
+
try {
|
|
1715
|
+
const stat = await lstat4(path2);
|
|
1716
|
+
assertPrivateStat(stat, {
|
|
1717
|
+
kind: "file",
|
|
1718
|
+
noSymlink: true,
|
|
1719
|
+
owner: "self",
|
|
1720
|
+
mode: [{ mask: 63, equals: 0 }],
|
|
1721
|
+
size: { max: 8192 }
|
|
1722
|
+
}, "QUALIFICATION_RECORD_INVALID");
|
|
1723
|
+
const handle = await openPrivateRead(path2, { nonblock: false });
|
|
1724
|
+
try {
|
|
1725
|
+
const text = await handle.readFile("utf8");
|
|
1726
|
+
const record2 = parseRecord(JSON.parse(text));
|
|
1727
|
+
if (record2.provider !== provider || record2.expiresAtUnixMs <= Date.now())
|
|
1728
|
+
return null;
|
|
1729
|
+
return record2;
|
|
1730
|
+
} finally {
|
|
1731
|
+
await handle.close();
|
|
1732
|
+
}
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
if (error.code === "ENOENT")
|
|
1735
|
+
return null;
|
|
1736
|
+
if (error instanceof Error && error.message === "QUALIFICATION_RECORD_INVALID")
|
|
1737
|
+
return null;
|
|
1738
|
+
throw error;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
async function writeCliQualification(stateRoot, record2) {
|
|
1742
|
+
const root = await privateDirectory(stateRoot);
|
|
1743
|
+
const parsed = parseRecord(JSON.parse(JSON.stringify(record2)));
|
|
1744
|
+
const path2 = recordPath(root, parsed.provider);
|
|
1745
|
+
const temp = `${path2}.tmp-${process.pid}`;
|
|
1746
|
+
await writeFileOnce(temp, JSON.stringify(parsed, null, 2) + `
|
|
1747
|
+
`, { exclusive: false, truncate: true, syncFile: true });
|
|
1748
|
+
await rename(temp, path2);
|
|
1749
|
+
return parsed;
|
|
1750
|
+
}
|
|
1751
|
+
function toTaskQualification(record2, expected) {
|
|
1752
|
+
if (record2.provider === "devin" || expected.route.provider === "devin") {
|
|
1753
|
+
return Object.freeze({ status: "unqualified", reason: "Devin host qualification is not implemented; legacy binary-only admission is insufficient." });
|
|
1754
|
+
}
|
|
1755
|
+
if (record2.provider !== expected.route.provider || record2.routeId !== expected.route.id || record2.authentication !== expected.route.authentication || record2.profileDigest !== expected.profile.digest || record2.runtimeVersion !== expected.runtimeVersion || record2.runtimeDigest !== expected.runtimeDigest || record2.expiresAtUnixMs <= Date.now()) {
|
|
1756
|
+
return Object.freeze({ status: "unqualified", reason: "Local admission record does not match this route, runtime and profile." });
|
|
1757
|
+
}
|
|
1758
|
+
return Object.freeze({
|
|
1759
|
+
status: "qualified",
|
|
1760
|
+
route: Object.freeze({ ...expected.route }),
|
|
1761
|
+
profile: Object.freeze({ id: expected.profile.id, version: expected.profile.version, digest: expected.profile.digest }),
|
|
1762
|
+
runtimeVersion: record2.runtimeVersion,
|
|
1763
|
+
runtimeDigest: record2.runtimeDigest,
|
|
1764
|
+
evidenceDigest: record2.evidenceDigest,
|
|
1765
|
+
expiresAt: record2.expiresAtUnixMs,
|
|
1766
|
+
controls: Object.freeze({
|
|
1767
|
+
noCommandTools: true,
|
|
1768
|
+
exactToolInventory: true,
|
|
1769
|
+
workspaceReadIsolation: true,
|
|
1770
|
+
workspaceWriteIsolation: true,
|
|
1771
|
+
isolatedConfiguration: true,
|
|
1772
|
+
authOutsideWorkspace: true,
|
|
1773
|
+
hostBrokerOnly: true
|
|
1774
|
+
})
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
function buildQualificationRecord(input) {
|
|
1778
|
+
const admittedAtUnixMs = safeInteger(input.now, 0, Number.MAX_SAFE_INTEGER);
|
|
1779
|
+
const record2 = Object.freeze({
|
|
1780
|
+
version: RECORD_VERSION,
|
|
1781
|
+
provider: boundedText(input.provider, 32),
|
|
1782
|
+
routeId: identifier(input.route.id),
|
|
1783
|
+
authentication: boundedText(input.route.authentication, 32),
|
|
1784
|
+
executablePath: boundedText(input.executablePath, 4096),
|
|
1785
|
+
executableSha256: digest2(input.executableSha256),
|
|
1786
|
+
runtimeVersion: boundedText(input.runtimeVersion, 160),
|
|
1787
|
+
runtimeDigest: digest2(input.runtimeDigest),
|
|
1788
|
+
profileDigest: digest2(input.profileDigest),
|
|
1789
|
+
evidenceDigest: "",
|
|
1790
|
+
admittedAtUnixMs,
|
|
1791
|
+
expiresAtUnixMs: admittedAtUnixMs + MAX_QUALIFICATION_AGE_MS
|
|
1792
|
+
});
|
|
1793
|
+
const evidenceDigest = input.evidenceDigest === undefined ? createHash3("sha256").update(JSON.stringify({ ...record2, evidenceDigest: "" })).digest("hex") : digest2(input.evidenceDigest);
|
|
1794
|
+
return Object.freeze({ ...record2, evidenceDigest });
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// src/cli/codex.ts
|
|
1798
|
+
var CLI_CODEX_SCHEMA_SHA256 = "ffb75cdcfe84e2a417ad481e7998a924db9f9cb9b4bea065b0067acbb5886748";
|
|
1799
|
+
var CLI_CODEX_SCHEMA_FILES = 416;
|
|
1800
|
+
var CLI_CODEX_SCHEMA_BYTES = 4137012;
|
|
1801
|
+
var CLI_CODEX_BUN_DARWIN_ARM64_SHA256 = "e0c90ec15d33363e6b70713d56bc3b2c7585c17f40a0fe0f8fd9305901d4e233";
|
|
1802
|
+
var CLI_CODEX_RUNTIME_VERSION = `codex-app-server:${CODEX_NATIVE_VERSION}:managed-task:v2`;
|
|
1803
|
+
function cliCodexHostDiagnostic() {
|
|
1804
|
+
if (typeof Bun === "undefined" || Bun.version !== "1.3.14") {
|
|
1805
|
+
return "Codex managed mode requires Bun 1.3.14. Launch xcb-compat with Bun 1.3.14, then run `xcb-compat doctor`; Node cannot run this qualified route.";
|
|
1806
|
+
}
|
|
1807
|
+
if (process.platform !== "darwin" || process.arch !== "arm64") {
|
|
1808
|
+
return "Codex managed mode requires macOS arm64; this host has no qualified Codex sandbox.";
|
|
1809
|
+
}
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
function assertCliCodexHost() {
|
|
1813
|
+
if (typeof Bun === "undefined" || Bun.version !== "1.3.14")
|
|
1814
|
+
throw new Error(`CLI_CODEX_BUN_REQUIRED: ${cliCodexHostDiagnostic()}`);
|
|
1815
|
+
if (process.platform !== "darwin" || process.arch !== "arm64")
|
|
1816
|
+
throw new Error(`CLI_CODEX_HOST_UNSUPPORTED: ${cliCodexHostDiagnostic()}`);
|
|
1817
|
+
}
|
|
1818
|
+
var hash2 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
1819
|
+
function cliCodexRuntimeIdentity(input) {
|
|
1820
|
+
const digest3 = hash2(JSON.stringify({ version: CLI_CODEX_RUNTIME_VERSION, ...input }));
|
|
1821
|
+
return Object.freeze({ version: CLI_CODEX_RUNTIME_VERSION, digest: digest3 });
|
|
1822
|
+
}
|
|
1823
|
+
async function schemaBundleDigest(root) {
|
|
1824
|
+
const paths = [];
|
|
1825
|
+
async function visit(directory2, relative2) {
|
|
1826
|
+
const iterator = await opendir2(directory2);
|
|
1827
|
+
try {
|
|
1828
|
+
for (;; ) {
|
|
1829
|
+
const entry = await iterator.read();
|
|
1830
|
+
if (entry === null)
|
|
1831
|
+
break;
|
|
1832
|
+
const child = relative2 === "" ? entry.name : `${relative2}/${entry.name}`;
|
|
1833
|
+
if (entry.isDirectory())
|
|
1834
|
+
await visit(join7(directory2, entry.name), child);
|
|
1835
|
+
else if (entry.isFile() && /^[A-Za-z0-9._/-]{1,512}$/u.test(child))
|
|
1836
|
+
paths.push(child);
|
|
1837
|
+
else
|
|
1838
|
+
throw new Error("CLI_CODEX_SCHEMA_ENTRY_INVALID");
|
|
1839
|
+
if (paths.length > 1024)
|
|
1840
|
+
throw new Error("CLI_CODEX_SCHEMA_FILES_INVALID");
|
|
1841
|
+
}
|
|
1842
|
+
} finally {
|
|
1843
|
+
await iterator.close();
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
await visit(root, "");
|
|
1847
|
+
const digest3 = createHash4("sha256");
|
|
1848
|
+
let bytes = 0;
|
|
1849
|
+
for (const relative2 of paths.sort()) {
|
|
1850
|
+
const contents = await readFile2(join7(root, relative2));
|
|
1851
|
+
bytes += contents.length;
|
|
1852
|
+
if (bytes > 16 * 1024 * 1024)
|
|
1853
|
+
throw new Error("CLI_CODEX_SCHEMA_BYTES_INVALID");
|
|
1854
|
+
digest3.update(relative2).update("\x00").update(String(contents.length)).update("\x00").update(contents);
|
|
1855
|
+
}
|
|
1856
|
+
return { sha256: digest3.digest("hex"), files: paths.length, bytes };
|
|
1857
|
+
}
|
|
1858
|
+
async function qualifyCliCodexRuntime(input) {
|
|
1859
|
+
assertCliCodexHost();
|
|
1860
|
+
if (input.inspection.provider !== "codex" || input.inspection.version !== CODEX_NATIVE_VERSION || input.inspection.sha256 !== CODEX_NATIVE_SHA256 || !input.inspection.versionMatches || !input.inspection.digestMatches) {
|
|
1861
|
+
throw new Error("CLI_CODEX_RUNTIME_UNADMITTED");
|
|
1862
|
+
}
|
|
1863
|
+
const stateRoot = await privateDirectory(input.stateRoot), executablePath = await realpath3(input.inspection.executablePath);
|
|
1864
|
+
await inspectCodexHostExecutable(executablePath, CODEX_NATIVE_SHA256);
|
|
1865
|
+
const parent = await inspectCodexHostRuntime({ expectedSha256: CLI_CODEX_BUN_DARWIN_ARM64_SHA256 });
|
|
1866
|
+
const temporary = await realpath3(await mkdtemp2(join7(stateRoot, "codex-qualification-")));
|
|
1867
|
+
let cleanup = true;
|
|
1868
|
+
try {
|
|
1869
|
+
const scratch = join7(temporary, "schema-scratch"), accountHome = join7(temporary, "schema-account"), schemaDirectory = join7(scratch, "schema");
|
|
1870
|
+
await mkdir5(scratch, { mode: 448 });
|
|
1871
|
+
await mkdir5(accountHome, { mode: 448 });
|
|
1872
|
+
await mkdir5(schemaDirectory, { mode: 448 });
|
|
1873
|
+
const profile = codexManagedOfflineSandbox({ executable: executablePath, scratch, accountHome });
|
|
1874
|
+
const generated = Bun.spawn(["/usr/bin/sandbox-exec", "-p", profile, executablePath, "app-server", "generate-json-schema", "--experimental", "--out", schemaDirectory], {
|
|
1875
|
+
cwd: scratch,
|
|
1876
|
+
env: { PATH: "/usr/bin:/bin", HOME: scratch, TMPDIR: scratch, NO_COLOR: "1" },
|
|
1877
|
+
stdout: "pipe",
|
|
1878
|
+
stderr: "pipe",
|
|
1879
|
+
timeout: 30000
|
|
1880
|
+
});
|
|
1881
|
+
const [code, stdout, stderr] = await Promise.all([generated.exited, new Response(generated.stdout).arrayBuffer(), new Response(generated.stderr).arrayBuffer()]);
|
|
1882
|
+
if (code !== 0 || stdout.byteLength > 64 * 1024 || stderr.byteLength > 64 * 1024)
|
|
1883
|
+
throw new Error("CLI_CODEX_SCHEMA_GENERATION_FAILED");
|
|
1884
|
+
const schema = await schemaBundleDigest(schemaDirectory);
|
|
1885
|
+
if (schema.sha256 !== CLI_CODEX_SCHEMA_SHA256 || schema.files !== CLI_CODEX_SCHEMA_FILES || schema.bytes !== CLI_CODEX_SCHEMA_BYTES) {
|
|
1886
|
+
throw new Error("CLI_CODEX_SCHEMA_MISMATCH");
|
|
1887
|
+
}
|
|
1888
|
+
const manifest = buildCodexProtocolManifest({
|
|
1889
|
+
protocolVersion: "v2-experimental",
|
|
1890
|
+
sourceVersion: CODEX_NATIVE_VERSION,
|
|
1891
|
+
executableSha256: CODEX_NATIVE_SHA256,
|
|
1892
|
+
schemaSha256: schema.sha256,
|
|
1893
|
+
generatedAtUnixMs: (input.now ?? Date.now)()
|
|
1894
|
+
});
|
|
1895
|
+
assertCodexProtocolManifest(manifest, { version: CODEX_NATIVE_VERSION, sha256: CODEX_NATIVE_SHA256, schemaSha256: schema.sha256 });
|
|
1896
|
+
const controller = new AbortController, timer = setTimeout(() => controller.abort(), 60000);
|
|
1897
|
+
let diagnostic;
|
|
1898
|
+
try {
|
|
1899
|
+
diagnostic = await runCodexManagedOfflineDiagnostic({
|
|
1900
|
+
directory: temporary,
|
|
1901
|
+
runtime: {
|
|
1902
|
+
executablePath,
|
|
1903
|
+
version: CODEX_NATIVE_VERSION,
|
|
1904
|
+
sha256: CODEX_NATIVE_SHA256,
|
|
1905
|
+
schemaSha256: schema.sha256,
|
|
1906
|
+
parentRuntime: { expectedSha256: parent.sha256 }
|
|
1907
|
+
},
|
|
1908
|
+
signal: controller.signal,
|
|
1909
|
+
admission: {
|
|
1910
|
+
kind: "managed-offline-lifecycle-diagnostic-v1",
|
|
1911
|
+
nativeSha256: CODEX_NATIVE_SHA256,
|
|
1912
|
+
schemaSha256: schema.sha256,
|
|
1913
|
+
parentSha256: parent.sha256
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
} finally {
|
|
1917
|
+
clearTimeout(timer);
|
|
1918
|
+
}
|
|
1919
|
+
const protocol = diagnostic.protocol;
|
|
1920
|
+
cleanup = diagnostic.bootstrapJoined && diagnostic.processJoined && diagnostic.leaseReleased && protocol?.joined === true;
|
|
1921
|
+
if (!diagnostic.passed || !cleanup || protocol === null || protocol.failure !== null || protocol.configurationObserved !== true || JSON.stringify(protocol.methods) !== JSON.stringify(["initialize", "initialized", "config/read"]) || protocol.disabledNotices !== 1)
|
|
1922
|
+
throw new Error("CLI_CODEX_OFFLINE_DIAGNOSTIC_FAILED");
|
|
1923
|
+
const runtime = cliCodexRuntimeIdentity({
|
|
1924
|
+
nativeSha256: CODEX_NATIVE_SHA256,
|
|
1925
|
+
schemaSha256: schema.sha256,
|
|
1926
|
+
parentSha256: parent.sha256
|
|
1927
|
+
});
|
|
1928
|
+
const content = Object.freeze({
|
|
1929
|
+
schema: "xcb.cli-codex-admission.v1",
|
|
1930
|
+
platform: "darwin-arm64",
|
|
1931
|
+
runtime,
|
|
1932
|
+
manifest,
|
|
1933
|
+
nativeSha256: CODEX_NATIVE_SHA256,
|
|
1934
|
+
schemaSha256: schema.sha256,
|
|
1935
|
+
parentSha256: parent.sha256,
|
|
1936
|
+
configurationSha256: diagnostic.configurationSha256,
|
|
1937
|
+
offline: Object.freeze({
|
|
1938
|
+
passed: true,
|
|
1939
|
+
network: "denied",
|
|
1940
|
+
bootstrapJoined: true,
|
|
1941
|
+
processJoined: true,
|
|
1942
|
+
leaseReleased: true,
|
|
1943
|
+
methods: Object.freeze(["initialize", "initialized", "config/read"]),
|
|
1944
|
+
configurationObserved: true,
|
|
1945
|
+
disabledNotices: 1,
|
|
1946
|
+
frameCount: protocol.frameCount
|
|
1947
|
+
}),
|
|
1948
|
+
productionQualified: false
|
|
1949
|
+
});
|
|
1950
|
+
return Object.freeze({ ...content, evidenceDigest: hash2(JSON.stringify(content)) });
|
|
1951
|
+
} finally {
|
|
1952
|
+
if (cleanup)
|
|
1953
|
+
await rm3(temporary, { recursive: true, force: true });
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function currentCodexIdentity() {
|
|
1957
|
+
return cliCodexRuntimeIdentity({
|
|
1958
|
+
nativeSha256: CODEX_NATIVE_SHA256,
|
|
1959
|
+
schemaSha256: CLI_CODEX_SCHEMA_SHA256,
|
|
1960
|
+
parentSha256: CLI_CODEX_BUN_DARWIN_ARM64_SHA256
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
async function admittedCodex(stateRoot, inspection) {
|
|
1964
|
+
assertCliCodexHost();
|
|
1965
|
+
if (process.platform !== "darwin" || process.arch !== "arm64" || inspection.provider !== "codex" || inspection.version !== CODEX_NATIVE_VERSION || inspection.sha256 !== CODEX_NATIVE_SHA256 || !inspection.versionMatches || !inspection.digestMatches)
|
|
1966
|
+
throw new Error("CLI_CODEX_RUNTIME_UNADMITTED");
|
|
1967
|
+
const expected = currentCodexIdentity(), record2 = await readCliQualification(stateRoot, "codex");
|
|
1968
|
+
if (record2 === null || record2.executablePath !== inspection.executablePath || record2.executableSha256 !== CODEX_NATIVE_SHA256 || record2.runtimeVersion !== expected.version || record2.runtimeDigest !== expected.digest)
|
|
1969
|
+
throw new Error("CLI_CODEX_RUNTIME_UNADMITTED");
|
|
1970
|
+
return expected;
|
|
1971
|
+
}
|
|
1972
|
+
function createCliCodexManagedLauncher(stateRoot, inspection, runtime = currentCodexIdentity()) {
|
|
1973
|
+
assertCliCodexHost();
|
|
1974
|
+
return createCodexManagedProcessLauncher({
|
|
1975
|
+
stateRoot,
|
|
1976
|
+
runtime: {
|
|
1977
|
+
executablePath: inspection.executablePath,
|
|
1978
|
+
version: CODEX_NATIVE_VERSION,
|
|
1979
|
+
sha256: CODEX_NATIVE_SHA256,
|
|
1980
|
+
schemaSha256: CLI_CODEX_SCHEMA_SHA256,
|
|
1981
|
+
parentRuntime: { expectedSha256: CLI_CODEX_BUN_DARWIN_ARM64_SHA256 }
|
|
1982
|
+
},
|
|
1983
|
+
admission: {
|
|
1984
|
+
profile: "managed-task-provider-tcp443-dns-candidate-v2",
|
|
1985
|
+
taskRuntimeVersion: runtime.version,
|
|
1986
|
+
taskRuntimeDigest: runtime.digest,
|
|
1987
|
+
nativeSha256: CODEX_NATIVE_SHA256,
|
|
1988
|
+
schemaSha256: CLI_CODEX_SCHEMA_SHA256,
|
|
1989
|
+
parentSha256: CLI_CODEX_BUN_DARWIN_ARM64_SHA256
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
async function withCodexAccount(stateRoot, inspection, action) {
|
|
1994
|
+
await admittedCodex(stateRoot, inspection);
|
|
1995
|
+
const root = await privateDirectory(stateRoot), database = await openAccountDatabase(join7(root, "account-leases.sqlite"));
|
|
1996
|
+
const leases = new SqliteAccountLeases(database), owner = `cli-codex-${randomBytes2(10).toString("hex")}`;
|
|
1997
|
+
const runtime = {
|
|
1998
|
+
executablePath: inspection.executablePath,
|
|
1999
|
+
version: CODEX_NATIVE_VERSION,
|
|
2000
|
+
sha256: CODEX_NATIVE_SHA256,
|
|
2001
|
+
schemaSha256: CLI_CODEX_SCHEMA_SHA256,
|
|
2002
|
+
parentRuntime: { expectedSha256: CLI_CODEX_BUN_DARWIN_ARM64_SHA256 }
|
|
2003
|
+
};
|
|
2004
|
+
const controller = createManagedCodexAccountController({
|
|
2005
|
+
accountId: "local",
|
|
2006
|
+
owner,
|
|
2007
|
+
processGeneration: Date.now(),
|
|
2008
|
+
leases,
|
|
2009
|
+
transportFactory(binding, onEvent) {
|
|
2010
|
+
const processPort = createCodexAccountProcess({
|
|
2011
|
+
binding,
|
|
2012
|
+
stateRoot: root,
|
|
2013
|
+
runtime,
|
|
2014
|
+
mode: "device-code",
|
|
2015
|
+
deviceCodeAdmission: {
|
|
2016
|
+
profile: "codex-account-device-code-tcp443-dns-v3",
|
|
2017
|
+
nativeSha256: CODEX_NATIVE_SHA256,
|
|
2018
|
+
schemaSha256: CLI_CODEX_SCHEMA_SHA256,
|
|
2019
|
+
parentSha256: CLI_CODEX_BUN_DARWIN_ARM64_SHA256
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
return createCodexAccountStdioTransport({ binding, process: processPort, onEvent, initializeTimeoutMs: 20000, closeTimeoutMs: 20000 });
|
|
2023
|
+
},
|
|
2024
|
+
operationTimeoutMs: 30000,
|
|
2025
|
+
closeTimeoutMs: 20000
|
|
2026
|
+
});
|
|
2027
|
+
try {
|
|
2028
|
+
return await action(controller);
|
|
2029
|
+
} finally {
|
|
2030
|
+
let released = false;
|
|
2031
|
+
try {
|
|
2032
|
+
released = (await controller.close()).released;
|
|
2033
|
+
} finally {
|
|
2034
|
+
database.close();
|
|
2035
|
+
}
|
|
2036
|
+
if (!released)
|
|
2037
|
+
throw new Error("CLI_CODEX_ACCOUNT_CLOSE_UNPROVEN");
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
async function codexAuthStatus(stateRoot, inspection) {
|
|
2041
|
+
try {
|
|
2042
|
+
const snapshot = await withCodexAccount(stateRoot, inspection, (controller) => controller.check());
|
|
2043
|
+
return Object.freeze({
|
|
2044
|
+
admitted: true,
|
|
2045
|
+
loggedIn: snapshot.state === "signed-in",
|
|
2046
|
+
planType: snapshot.planType ?? null,
|
|
2047
|
+
models: Object.freeze(snapshot.models.map((model) => model.id))
|
|
2048
|
+
});
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
if (error instanceof Error && error.message === "CLI_CODEX_RUNTIME_UNADMITTED") {
|
|
2051
|
+
return Object.freeze({ admitted: false, loggedIn: false, planType: null, models: Object.freeze([]) });
|
|
2052
|
+
}
|
|
2053
|
+
throw error;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
async function codexLogin(stateRoot, inspection, onChallenge, signal) {
|
|
2057
|
+
return await withCodexAccount(stateRoot, inspection, async (controller) => {
|
|
2058
|
+
let snapshot = await controller.check(signal);
|
|
2059
|
+
if (snapshot.state === "signed-in")
|
|
2060
|
+
return snapshot;
|
|
2061
|
+
const challenge = await controller.startLogin("chatgptDeviceCode", signal);
|
|
2062
|
+
onChallenge(challenge);
|
|
2063
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
2064
|
+
while (Date.now() < deadline && !signal?.aborted) {
|
|
2065
|
+
await Bun.sleep(1500);
|
|
2066
|
+
try {
|
|
2067
|
+
snapshot = await controller.check(signal);
|
|
2068
|
+
} catch {
|
|
2069
|
+
if (controller.snapshot().state !== "unchecked")
|
|
2070
|
+
throw new Error("CLI_CODEX_LOGIN_FAILED");
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
if (snapshot.state === "signed-in")
|
|
2074
|
+
return snapshot;
|
|
2075
|
+
if (snapshot.state === "recovery-required" || snapshot.state === "unavailable")
|
|
2076
|
+
throw new Error("CLI_CODEX_LOGIN_FAILED");
|
|
2077
|
+
}
|
|
2078
|
+
await controller.cancelLogin(challenge.loginId).catch(() => {});
|
|
2079
|
+
throw new Error(signal?.aborted ? "CLI_CODEX_LOGIN_ABORTED" : "CLI_CODEX_LOGIN_TIMEOUT");
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
async function codexLogout(stateRoot, inspection) {
|
|
2083
|
+
await withCodexAccount(stateRoot, inspection, async (controller) => {
|
|
2084
|
+
await controller.logout();
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/cli/sandbox.ts
|
|
2089
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
2090
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2091
|
+
import { writeFileSync, mkdirSync, openSync, closeSync as closeSync2, readSync, writeSync as writeSync2, fstatSync, lstatSync, realpathSync, rmSync, constants as constants2 } from "node:fs";
|
|
2092
|
+
import { access, lstat as lstat5, mkdir as mkdir6, readFile as readFile3, realpath as realpath4 } from "node:fs/promises";
|
|
2093
|
+
import { dirname, isAbsolute as isAbsolute3, join as join8 } from "node:path";
|
|
2094
|
+
import { fileURLToPath } from "node:url";
|
|
2095
|
+
var fail5 = (code) => {
|
|
2096
|
+
throw new Error(code);
|
|
2097
|
+
};
|
|
2098
|
+
function path2(value) {
|
|
2099
|
+
return canonicalizePrivatePath(value, { code: "CLI_SANDBOX_PATH_INVALID" });
|
|
2100
|
+
}
|
|
2101
|
+
function claudeCliSandboxPolicy(input) {
|
|
2102
|
+
const executable = path2(input.executable), scratch = path2(input.scratch), accountHome = path2(input.accountHome);
|
|
2103
|
+
const inside = (inner, outer) => inner === outer || inner.startsWith(outer + "/");
|
|
2104
|
+
if (inside(executable, scratch) || inside(executable, accountHome) || inside(scratch, accountHome) || inside(accountHome, scratch)) {
|
|
2105
|
+
fail5("CLI_SANDBOX_LAYOUT_INVALID");
|
|
2106
|
+
}
|
|
2107
|
+
const uid = process.getuid?.();
|
|
2108
|
+
if (typeof uid !== "number" || !Number.isSafeInteger(uid) || uid < 0)
|
|
2109
|
+
fail5("CLI_SANDBOX_UID_INVALID");
|
|
2110
|
+
const claudeTmp = `/private/tmp/claude-${uid}`;
|
|
2111
|
+
const q = JSON.stringify;
|
|
2112
|
+
return `(version 1)
|
|
2113
|
+
(deny default)
|
|
2114
|
+
(allow process-exec (literal ${q(executable)}))
|
|
2115
|
+
(allow process-fork)
|
|
2116
|
+
(allow process-info* (target self))
|
|
2117
|
+
(allow signal (target self))
|
|
2118
|
+
(allow sysctl-read)
|
|
2119
|
+
(allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo"))
|
|
2120
|
+
(allow file-ioctl (literal "/dev/null") (subpath "/dev/fd"))
|
|
2121
|
+
(allow file-read* file-write* (literal "/dev/null") (literal "/dev/urandom") (literal "/dev/random") (literal "/dev/dtracehelper") (subpath "/dev/fd"))
|
|
2122
|
+
(allow file-read* (literal "/") (literal "/tmp") (literal "/etc") (literal "/var") (literal "/Library") (literal "/private/etc") (literal "/private/tmp") (literal "/private/var")
|
|
2123
|
+
(literal ${q(executable)})
|
|
2124
|
+
(subpath "/System") (subpath "/usr") (subpath "/Library/Preferences") (subpath "/Library/Apple") (subpath "/etc") (subpath "/private/etc") (subpath "/var/db/timezone") (subpath "/private/var/db/timezone"))
|
|
2125
|
+
(allow file-map-executable (literal ${q(executable)}) (subpath "/System") (subpath "/usr"))
|
|
2126
|
+
(allow file-read* file-write* (subpath ${q(scratch)}) (subpath ${q(accountHome)}) (subpath ${q(claudeTmp)}))
|
|
2127
|
+
(allow file-read-metadata (path-ancestors ${q(executable)}) (path-ancestors ${q(scratch)}) (path-ancestors ${q(accountHome)}) (path-ancestors ${q(claudeTmp)}))
|
|
2128
|
+
(allow network-outbound (literal "/private/var/run/mDNSResponder") (literal "/private/var/run/syslog") (remote tcp "*:443"))
|
|
2129
|
+
`;
|
|
2130
|
+
}
|
|
2131
|
+
var SEATBELT = "/usr/bin/sandbox-exec";
|
|
2132
|
+
var BWRAP_CANDIDATES = ["/usr/bin/bwrap", "/bin/bwrap", "/usr/local/bin/bwrap"];
|
|
2133
|
+
var FORWARDER_PORT = 48123;
|
|
2134
|
+
function lddClosure(executable) {
|
|
2135
|
+
const result = spawnSync4("ldd", [executable], { encoding: "utf8", timeout: 15000 });
|
|
2136
|
+
const paths = new Set;
|
|
2137
|
+
if (result.status === 0 && typeof result.stdout === "string") {
|
|
2138
|
+
for (const match of result.stdout.matchAll(/(?:=>\s*)?(\/[A-Za-z0-9._\/+:-]+)/g)) {
|
|
2139
|
+
if (isAbsolute3(match[1]))
|
|
2140
|
+
paths.add(match[1]);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return [...paths].sort();
|
|
2144
|
+
}
|
|
2145
|
+
async function prepareCliLinuxSandbox(input) {
|
|
2146
|
+
const bridgeDirectory = path2(input.bridgeDirectory);
|
|
2147
|
+
await mkdir6(bridgeDirectory, { mode: 448, recursive: true });
|
|
2148
|
+
let bwrap;
|
|
2149
|
+
for (const candidate of BWRAP_CANDIDATES) {
|
|
2150
|
+
if (await access(candidate, constants2.X_OK).then(() => true, () => false)) {
|
|
2151
|
+
bwrap = await realpath4(candidate);
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
const wrapper = bwrap ?? fail5("CLI_SANDBOX_BWRAP_MISSING");
|
|
2156
|
+
const bwrapSha256 = createHash5("sha256").update(await readFile3(wrapper)).digest("hex");
|
|
2157
|
+
await verifyOsSandboxExecutable(wrapper, bwrapSha256, 8n * 1024n * 1024n);
|
|
2158
|
+
const runtime = await realpath4(process.execPath);
|
|
2159
|
+
let script;
|
|
2160
|
+
for (const rel of ["../../sandbox/loopback-forwarder.cjs", "../sandbox/loopback-forwarder.cjs"]) {
|
|
2161
|
+
const candidate = fileURLToPath(new URL(rel, import.meta.url));
|
|
2162
|
+
if (await lstat5(candidate).then((meta) => meta.isFile(), () => false)) {
|
|
2163
|
+
script = await realpath4(candidate);
|
|
2164
|
+
break;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
const forwarderScript = script ?? fail5("CLI_SANDBOX_FORWARDER_MISSING");
|
|
2168
|
+
const runtimeLibs = lddClosure(runtime);
|
|
2169
|
+
const socketPath = join8(bridgeDirectory, `egress-${process.pid}-${createHash5("sha256").update(`${Date.now()}:${Math.random()}`).digest("hex").slice(0, 12)}.sock`);
|
|
2170
|
+
const bridge = await createEgressBridge({ socketPath, dialer: egressBridgeDialer });
|
|
2171
|
+
return Object.freeze({
|
|
2172
|
+
socketPath,
|
|
2173
|
+
plan({ executable: exe, scratch, accountHome, policyPath }) {
|
|
2174
|
+
const readOnlyPaths = [...new Set([...runtimeLibs, ...lddClosure(exe)])].sort();
|
|
2175
|
+
return planBwrapPolicy({
|
|
2176
|
+
platform: "linux",
|
|
2177
|
+
executable: exe,
|
|
2178
|
+
scratch,
|
|
2179
|
+
accountHome,
|
|
2180
|
+
readOnlyPaths,
|
|
2181
|
+
network: "provider-tcp443-dns",
|
|
2182
|
+
egressSocket: socketPath,
|
|
2183
|
+
egressForward: {
|
|
2184
|
+
runtime,
|
|
2185
|
+
script: forwarderScript,
|
|
2186
|
+
port: FORWARDER_PORT,
|
|
2187
|
+
envFile: join8(scratch, "forwarder.env")
|
|
2188
|
+
},
|
|
2189
|
+
policyPath
|
|
2190
|
+
}, wrapper);
|
|
2191
|
+
},
|
|
2192
|
+
close: () => bridge.close()
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
function claudeCliProcessFactory(accountHome, linux, platform = process.platform) {
|
|
2196
|
+
if (platform === "darwin") {
|
|
2197
|
+
const home = path2(accountHome);
|
|
2198
|
+
return (input) => {
|
|
2199
|
+
const executable = path2(input.executable);
|
|
2200
|
+
const runRoot = dirname(executable);
|
|
2201
|
+
const scratch = join8(runRoot, "scratch");
|
|
2202
|
+
const policyPath = join8(runRoot, "sandbox.sb");
|
|
2203
|
+
const plan = planSeatbeltPolicy({ platform: "darwin", executable, scratch, accountHome: home, network: "provider-tcp443-dns", policyPath }, claudeCliSandboxPolicy({ executable, scratch, accountHome: home }));
|
|
2204
|
+
writeFileSync(policyPath, plan.policy, { mode: 256, flag: "w" });
|
|
2205
|
+
return createSandboxedProviderProcessFactory(plan)(input);
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
if (platform === "linux" && linux !== undefined) {
|
|
2209
|
+
const home = path2(accountHome);
|
|
2210
|
+
return (input) => {
|
|
2211
|
+
const executable = path2(input.executable);
|
|
2212
|
+
const runRoot = dirname(executable);
|
|
2213
|
+
const scratch = join8(runRoot, "scratch");
|
|
2214
|
+
mkdirSync(scratch, { mode: 448, recursive: true });
|
|
2215
|
+
const policyPath = join8(runRoot, "sandbox.json");
|
|
2216
|
+
const plan = linux.plan({ executable, scratch, accountHome: home, policyPath });
|
|
2217
|
+
writeFileSync(policyPath, plan.policy, { mode: 256, flag: "w" });
|
|
2218
|
+
const env = Object.freeze({ PATH: "/usr/bin:/bin", HOME: scratch, TMPDIR: scratch, ...input.env });
|
|
2219
|
+
return createSandboxedProviderProcessFactory(plan)({ ...input, env });
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
async function seatbeltAvailable() {
|
|
2225
|
+
if (process.platform !== "darwin")
|
|
2226
|
+
return false;
|
|
2227
|
+
try {
|
|
2228
|
+
await access(SEATBELT, constants2.X_OK);
|
|
2229
|
+
return true;
|
|
2230
|
+
} catch {
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/cli/provider.ts
|
|
2236
|
+
import { join as join9 } from "node:path";
|
|
2237
|
+
var CLI_CLAUDE_ROUTE = "claude-subscription";
|
|
2238
|
+
var CLI_CODEX_ROUTE = "codex-subscription";
|
|
2239
|
+
var CLI_CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-5";
|
|
2240
|
+
var CLI_CODEX_DEFAULT_MODEL = "gpt-5.1-codex-mini";
|
|
2241
|
+
var CLI_DEVIN_DEFAULT_MODEL = "adaptive";
|
|
2242
|
+
var CLI_SYSTEM_PROMPT = [
|
|
2243
|
+
"You are xcb, a coding assistant running inside the user's terminal.",
|
|
2244
|
+
"The workspace tools address files inside the opened project directory only; there is no shell, process or arbitrary-path access.",
|
|
2245
|
+
"Use workspace.list and workspace.search before workspace.read; keep each file's revision and pass it back as expectedRevision to workspace.write.",
|
|
2246
|
+
"Answer directly and concisely. Never claim an action you did not perform through the tools."
|
|
2247
|
+
].join(" ");
|
|
2248
|
+
async function admitCliProvider(stateRoot, provider, profile) {
|
|
2249
|
+
const inspection = await inspectCliBinary(provider, undefined, true);
|
|
2250
|
+
if (inspection === null) {
|
|
2251
|
+
return Object.freeze({ inspection, record: null, detail: `${provider} binary not found` });
|
|
2252
|
+
}
|
|
2253
|
+
if (!inspection.versionMatches) {
|
|
2254
|
+
const required = provider === "codex" ? CODEX_NATIVE_VERSION : provider === "devin" ? CLI_DEVIN_MIN_VERSION : CLAUDE_CODE_MIN_VERSION;
|
|
2255
|
+
const comparator = provider === "codex" ? `pinned ${required}` : `>= ${required}`;
|
|
2256
|
+
const hint = provider === "claude" ? ` — install with \`bun add -g @anthropic-ai/claude-code@2\`` : provider === "devin" ? ` — upgrade the devin CLI to ${required} or newer` : "";
|
|
2257
|
+
return Object.freeze({ inspection, record: null, detail: `${provider} ${inspection.version} found; ${comparator} required${hint}` });
|
|
2258
|
+
}
|
|
2259
|
+
if (provider === "devin") {
|
|
2260
|
+
return Object.freeze({ inspection, record: null, detail: CLI_DEVIN_QUALIFICATION_REQUIRED });
|
|
2261
|
+
}
|
|
2262
|
+
if (provider === "codex") {
|
|
2263
|
+
let evidence;
|
|
2264
|
+
try {
|
|
2265
|
+
evidence = await qualifyCliCodexRuntime({ stateRoot, inspection });
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
const code = error instanceof Error && /^CLI_CODEX_[A-Z_]+$/u.test(error.message) ? error.message : "CLI_CODEX_QUALIFICATION_FAILED";
|
|
2268
|
+
return Object.freeze({ inspection, record: null, detail: cliCodexHostDiagnostic() ?? `codex managed admission failed: ${code}` });
|
|
2269
|
+
}
|
|
2270
|
+
const record3 = await writeCliQualification(await privateDirectory(stateRoot), buildQualificationRecord({
|
|
2271
|
+
provider,
|
|
2272
|
+
route: Object.freeze({ id: CLI_CODEX_ROUTE, provider: "codex", authentication: "subscription" }),
|
|
2273
|
+
executablePath: inspection.executablePath,
|
|
2274
|
+
executableSha256: inspection.sha256,
|
|
2275
|
+
runtimeVersion: evidence.runtime.version,
|
|
2276
|
+
runtimeDigest: evidence.runtime.digest,
|
|
2277
|
+
profileDigest: profile.digest,
|
|
2278
|
+
evidenceDigest: evidence.evidenceDigest,
|
|
2279
|
+
now: Date.now()
|
|
2280
|
+
}));
|
|
2281
|
+
return Object.freeze({ inspection, record: record3, detail: `admitted ${provider} ${inspection.version}` });
|
|
2282
|
+
}
|
|
2283
|
+
const identity = claudeTaskRuntimeIdentity({ executableSha256: inspection.sha256, cliVersion: inspection.version, authentication: "subscription" });
|
|
2284
|
+
const record2 = await writeCliQualification(await privateDirectory(stateRoot), buildQualificationRecord({
|
|
2285
|
+
provider,
|
|
2286
|
+
route: Object.freeze({ id: CLI_CLAUDE_ROUTE, provider: "claude", authentication: "subscription" }),
|
|
2287
|
+
executablePath: inspection.executablePath,
|
|
2288
|
+
executableSha256: inspection.sha256,
|
|
2289
|
+
runtimeVersion: identity.version,
|
|
2290
|
+
runtimeDigest: identity.digest,
|
|
2291
|
+
profileDigest: profile.digest,
|
|
2292
|
+
now: Date.now()
|
|
2293
|
+
}));
|
|
2294
|
+
return Object.freeze({ inspection, record: record2, detail: `admitted ${provider} ${inspection.version}` });
|
|
2295
|
+
}
|
|
2296
|
+
async function openCliProvider(stateRoot, provider, profile, events, _context) {
|
|
2297
|
+
const inspection = await inspectCliBinary(provider);
|
|
2298
|
+
if (inspection === null)
|
|
2299
|
+
return Object.freeze({ status: "binary-missing", inspection });
|
|
2300
|
+
if (!inspection.versionMatches)
|
|
2301
|
+
return Object.freeze({ status: "version-mismatch", inspection });
|
|
2302
|
+
if (provider === "devin")
|
|
2303
|
+
return Object.freeze({ status: "unadmitted", inspection, detail: CLI_DEVIN_QUALIFICATION_REQUIRED });
|
|
2304
|
+
if (provider === "codex") {
|
|
2305
|
+
const detail = cliCodexHostDiagnostic();
|
|
2306
|
+
if (detail !== null)
|
|
2307
|
+
return Object.freeze({ status: "sandbox-unavailable", inspection, detail });
|
|
2308
|
+
}
|
|
2309
|
+
const record2 = await readCliQualification(stateRoot, provider);
|
|
2310
|
+
if (record2 === null || record2.executableSha256 !== inspection.sha256 || record2.executablePath !== inspection.executablePath) {
|
|
2311
|
+
return Object.freeze({ status: "unadmitted", inspection });
|
|
2312
|
+
}
|
|
2313
|
+
if (provider === "codex") {
|
|
2314
|
+
if (process.platform !== "darwin" || process.arch !== "arm64")
|
|
2315
|
+
return Object.freeze({ status: "sandbox-unavailable", inspection });
|
|
2316
|
+
const route = Object.freeze({ id: CLI_CODEX_ROUTE, provider: "codex", authentication: "subscription" });
|
|
2317
|
+
const identity = cliCodexRuntimeIdentity({
|
|
2318
|
+
nativeSha256: inspection.sha256,
|
|
2319
|
+
schemaSha256: CLI_CODEX_SCHEMA_SHA256,
|
|
2320
|
+
parentSha256: CLI_CODEX_BUN_DARWIN_ARM64_SHA256
|
|
2321
|
+
});
|
|
2322
|
+
const qualification = toTaskQualification(record2, { route, profile, runtimeVersion: identity.version, runtimeDigest: identity.digest });
|
|
2323
|
+
if (qualification.status !== "qualified")
|
|
2324
|
+
return Object.freeze({ status: "unadmitted", inspection });
|
|
2325
|
+
const adapter = createCodexManagedTaskAdapter({
|
|
2326
|
+
route,
|
|
2327
|
+
runtime: identity,
|
|
2328
|
+
qualification,
|
|
2329
|
+
instructions: Object.freeze({ base: CLI_SYSTEM_PROMPT, developer: "" }),
|
|
2330
|
+
launcher: createCliCodexManagedLauncher(stateRoot, inspection, identity),
|
|
2331
|
+
now: Date.now
|
|
2332
|
+
});
|
|
2333
|
+
return Object.freeze({ status: "ready", adapter, inspection, record: record2 });
|
|
2334
|
+
}
|
|
2335
|
+
if (provider === "claude") {
|
|
2336
|
+
const { config } = await providerAuthDirs(stateRoot, "claude");
|
|
2337
|
+
const route = Object.freeze({ id: CLI_CLAUDE_ROUTE, provider: "claude", authentication: "subscription" });
|
|
2338
|
+
const runtime = Object.freeze({ executablePath: inspection.executablePath, executableSha256: inspection.sha256, cliVersion: inspection.version });
|
|
2339
|
+
const identity = claudeTaskRuntimeIdentity({ executableSha256: inspection.sha256, cliVersion: inspection.version, authentication: "subscription" });
|
|
2340
|
+
const qualification = toTaskQualification(record2, { route, profile, runtimeVersion: identity.version, runtimeDigest: identity.digest });
|
|
2341
|
+
let linuxSandbox;
|
|
2342
|
+
if (process.platform === "linux") {
|
|
2343
|
+
const bridgeDirectory = join9(await privateDirectory(stateRoot), "egress");
|
|
2344
|
+
linuxSandbox = await prepareCliLinuxSandbox({ bridgeDirectory }).catch(() => null) ?? undefined;
|
|
2345
|
+
if (linuxSandbox === undefined)
|
|
2346
|
+
return Object.freeze({ status: "sandbox-unavailable", inspection });
|
|
2347
|
+
}
|
|
2348
|
+
const processFactory = claudeCliProcessFactory(config, linuxSandbox);
|
|
2349
|
+
const adapter = createClaudeTaskAdapter({
|
|
2350
|
+
route,
|
|
2351
|
+
runtime,
|
|
2352
|
+
stateRoot,
|
|
2353
|
+
authDirectory: config,
|
|
2354
|
+
authentication: "subscription",
|
|
2355
|
+
qualification,
|
|
2356
|
+
systemPrompt: CLI_SYSTEM_PROMPT,
|
|
2357
|
+
subscriptionToken: async () => {
|
|
2358
|
+
const token = await readClaudeOAuthToken(stateRoot);
|
|
2359
|
+
if (token === null)
|
|
2360
|
+
throw new Error("CLAUDE_OAUTH_TOKEN_REQUIRED");
|
|
2361
|
+
return token;
|
|
2362
|
+
},
|
|
2363
|
+
...events === undefined ? {} : { events },
|
|
2364
|
+
...processFactory === undefined ? {} : { processFactory }
|
|
2365
|
+
});
|
|
2366
|
+
if (qualification.status !== "qualified") {
|
|
2367
|
+
await linuxSandbox?.close().catch(() => {});
|
|
2368
|
+
return Object.freeze({ status: "unadmitted", inspection });
|
|
2369
|
+
}
|
|
2370
|
+
return Object.freeze({
|
|
2371
|
+
status: "ready",
|
|
2372
|
+
adapter,
|
|
2373
|
+
inspection,
|
|
2374
|
+
record: record2,
|
|
2375
|
+
...linuxSandbox === undefined ? {} : { close: () => linuxSandbox.close() }
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
return Object.freeze({ status: "unadmitted", inspection });
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// src/cli/sessions.ts
|
|
2382
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2383
|
+
import { appendFile, lstat as lstat6, mkdir as mkdir7, rm as rm4 } from "node:fs/promises";
|
|
2384
|
+
import { join as join10 } from "node:path";
|
|
2385
|
+
var MAX_TITLE_BYTES = 200;
|
|
2386
|
+
var MAX_TRANSCRIPT_BYTES = 4 * 1024 * 1024;
|
|
2387
|
+
var MAX_SESSIONS = 512;
|
|
2388
|
+
var MAX_ENTRY_BYTES = 256 * 1024;
|
|
2389
|
+
var TABLE = "xcb_cli_sessions";
|
|
2390
|
+
var LEGACY_TABLE = "agentmixer_cli_sessions";
|
|
2391
|
+
var fail6 = (code) => {
|
|
2392
|
+
throw new Error(code);
|
|
2393
|
+
};
|
|
2394
|
+
var provider = (value) => value === "codex" || value === "claude" || value === "devin" ? value : fail6("SESSION_PROVIDER_INVALID");
|
|
2395
|
+
function sessionFrom(row) {
|
|
2396
|
+
return Object.freeze({
|
|
2397
|
+
id: identifier(row.id),
|
|
2398
|
+
provider: provider(row.provider),
|
|
2399
|
+
accountId: row.account_id === null ? null : identifier(row.account_id),
|
|
2400
|
+
workspace: boundedText(row.workspace, 4096),
|
|
2401
|
+
model: boundedText(row.model, 160),
|
|
2402
|
+
title: boundedText(row.title, MAX_TITLE_BYTES, true),
|
|
2403
|
+
createdAt: safeInteger(row.created_at, 0, Number.MAX_SAFE_INTEGER),
|
|
2404
|
+
lastActiveAt: safeInteger(row.last_active_at, 0, Number.MAX_SAFE_INTEGER),
|
|
2405
|
+
turns: safeInteger(row.turns, 0, Number.MAX_SAFE_INTEGER)
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
function entry(value) {
|
|
2409
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
2410
|
+
fail6("TRANSCRIPT_ENTRY_INVALID");
|
|
2411
|
+
const raw = value;
|
|
2412
|
+
if (Reflect.ownKeys(raw).sort().join(",") !== "at,role,text")
|
|
2413
|
+
fail6("TRANSCRIPT_ENTRY_INVALID");
|
|
2414
|
+
const role = raw.role === "user" || raw.role === "assistant" || raw.role === "system" ? raw.role : fail6("TRANSCRIPT_ENTRY_INVALID");
|
|
2415
|
+
return Object.freeze({ role, text: boundedText(raw.text, MAX_ENTRY_BYTES), at: safeInteger(raw.at, 0, Number.MAX_SAFE_INTEGER) });
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
class CliSessionStore {
|
|
2419
|
+
database;
|
|
2420
|
+
directory;
|
|
2421
|
+
constructor(database, directory2) {
|
|
2422
|
+
this.database = database;
|
|
2423
|
+
this.directory = directory2;
|
|
2424
|
+
const tables = new Set(database.query("SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
|
|
2425
|
+
if (tables.has(LEGACY_TABLE) && !tables.has(TABLE)) {
|
|
2426
|
+
database.exec(`ALTER TABLE ${LEGACY_TABLE} RENAME TO ${TABLE}`);
|
|
2427
|
+
}
|
|
2428
|
+
database.exec(`CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
2429
|
+
id TEXT PRIMARY KEY, provider TEXT NOT NULL, account_id TEXT,
|
|
2430
|
+
workspace TEXT NOT NULL, model TEXT NOT NULL, title TEXT NOT NULL,
|
|
2431
|
+
created_at INTEGER NOT NULL, last_active_at INTEGER NOT NULL, turns INTEGER NOT NULL
|
|
2432
|
+
)`);
|
|
2433
|
+
}
|
|
2434
|
+
static async open(directory2) {
|
|
2435
|
+
await mkdir7(directory2, { mode: 448, recursive: true });
|
|
2436
|
+
const root = await privateDirectory(directory2);
|
|
2437
|
+
const transcripts = join10(root, "transcripts");
|
|
2438
|
+
await mkdir7(transcripts, { mode: 448, recursive: true });
|
|
2439
|
+
const database = await openAccountDatabase(join10(root, "sessions.sqlite"));
|
|
2440
|
+
return new CliSessionStore(database, await privateDirectory(transcripts));
|
|
2441
|
+
}
|
|
2442
|
+
transcriptPath(id) {
|
|
2443
|
+
return join10(this.directory, `${identifier(id)}.jsonl`);
|
|
2444
|
+
}
|
|
2445
|
+
async create(input) {
|
|
2446
|
+
const id = `s_${randomBytes3(12).toString("hex")}`;
|
|
2447
|
+
const workspace = boundedText(input.workspace, 4096);
|
|
2448
|
+
const count = this.database.query("SELECT COUNT(*) AS n FROM xcb_cli_sessions").get();
|
|
2449
|
+
if ((count?.n ?? 0) >= MAX_SESSIONS)
|
|
2450
|
+
fail6("SESSION_LIMIT");
|
|
2451
|
+
const row = this.database.query(`INSERT INTO xcb_cli_sessions (id, provider, account_id, workspace, model, title, created_at, last_active_at, turns)
|
|
2452
|
+
VALUES (?, ?, ?, ?, ?, '', ?, ?, 0) RETURNING *`).get(id, provider(input.provider), input.accountId === null ? null : identifier(input.accountId), workspace, boundedText(input.model, 160), safeInteger(input.now, 0, Number.MAX_SAFE_INTEGER), safeInteger(input.now, 0, Number.MAX_SAFE_INTEGER));
|
|
2453
|
+
return row === null ? fail6("SESSION_CREATE_FAILED") : sessionFrom(row);
|
|
2454
|
+
}
|
|
2455
|
+
get(id) {
|
|
2456
|
+
const row = this.database.query("SELECT * FROM xcb_cli_sessions WHERE id=?").get(identifier(id));
|
|
2457
|
+
return row === null ? null : sessionFrom(row);
|
|
2458
|
+
}
|
|
2459
|
+
list(limit = 64) {
|
|
2460
|
+
const rows = this.database.query("SELECT * FROM xcb_cli_sessions ORDER BY last_active_at DESC LIMIT ?").all(safeInteger(limit, 1, MAX_SESSIONS));
|
|
2461
|
+
return Object.freeze(rows.map(sessionFrom));
|
|
2462
|
+
}
|
|
2463
|
+
async record(session, entries, now) {
|
|
2464
|
+
const current = this.get(session.id) ?? fail6("SESSION_MISSING");
|
|
2465
|
+
for (const item of entries)
|
|
2466
|
+
entry(item);
|
|
2467
|
+
const path3 = this.transcriptPath(session.id);
|
|
2468
|
+
const line = entries.map((item) => `${JSON.stringify(entry(item))}
|
|
2469
|
+
`).join("");
|
|
2470
|
+
await appendFile(path3, line, { mode: 384 });
|
|
2471
|
+
const stat = await lstat6(path3);
|
|
2472
|
+
assertPrivateStat(stat, { kind: "file", size: { max: MAX_TRANSCRIPT_BYTES } }, "TRANSCRIPT_LIMIT");
|
|
2473
|
+
const firstUser = entries.find((item) => item.role === "user");
|
|
2474
|
+
const title = current.title === "" && firstUser !== undefined ? boundedText(firstUser.text.split(`
|
|
2475
|
+
`)[0].slice(0, 80), MAX_TITLE_BYTES, true) : current.title;
|
|
2476
|
+
const row = this.database.query(`UPDATE xcb_cli_sessions SET title=?, last_active_at=?, turns=turns+? WHERE id=? RETURNING *`).get(title, safeInteger(now, 0, Number.MAX_SAFE_INTEGER), entries.length, current.id);
|
|
2477
|
+
return row === null ? current : sessionFrom(row);
|
|
2478
|
+
}
|
|
2479
|
+
async remove(id) {
|
|
2480
|
+
const session = this.get(id);
|
|
2481
|
+
if (session === null)
|
|
2482
|
+
return false;
|
|
2483
|
+
this.database.query("DELETE FROM xcb_cli_sessions WHERE id=?").run(session.id);
|
|
2484
|
+
await rm4(this.transcriptPath(session.id), { force: true });
|
|
2485
|
+
return true;
|
|
2486
|
+
}
|
|
2487
|
+
async prune(beforeMs) {
|
|
2488
|
+
const rows = this.database.query("SELECT id FROM xcb_cli_sessions WHERE last_active_at < ?").all(safeInteger(beforeMs, 0, Number.MAX_SAFE_INTEGER));
|
|
2489
|
+
let removed = 0;
|
|
2490
|
+
for (const row of rows) {
|
|
2491
|
+
if (await this.remove(row.id))
|
|
2492
|
+
removed += 1;
|
|
2493
|
+
}
|
|
2494
|
+
return removed;
|
|
2495
|
+
}
|
|
2496
|
+
async transcript(id, limitBytes = MAX_TRANSCRIPT_BYTES) {
|
|
2497
|
+
const path3 = this.transcriptPath(id);
|
|
2498
|
+
let text;
|
|
2499
|
+
try {
|
|
2500
|
+
const stat = await lstat6(path3);
|
|
2501
|
+
assertPrivateStat(stat, { kind: "file", noSymlink: true, size: { max: limitBytes } }, "TRANSCRIPT_LIMIT");
|
|
2502
|
+
const handle = await openPrivateRead(path3, { nonblock: false });
|
|
2503
|
+
try {
|
|
2504
|
+
text = await handle.readFile("utf8");
|
|
2505
|
+
} finally {
|
|
2506
|
+
await handle.close();
|
|
2507
|
+
}
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
if (error.code === "ENOENT")
|
|
2510
|
+
return Object.freeze([]);
|
|
2511
|
+
throw error;
|
|
2512
|
+
}
|
|
2513
|
+
const lines = text.split(`
|
|
2514
|
+
`).filter((lineText) => lineText !== "");
|
|
2515
|
+
if (lines.length > 4096)
|
|
2516
|
+
fail6("TRANSCRIPT_LIMIT");
|
|
2517
|
+
return Object.freeze(lines.map((lineText) => entry(JSON.parse(lineText))));
|
|
2518
|
+
}
|
|
2519
|
+
close() {
|
|
2520
|
+
this.database.close();
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// src/cli/workspace.ts
|
|
2525
|
+
import { createHash as createHash7, randomBytes as randomBytes4 } from "node:crypto";
|
|
2526
|
+
import { lstatSync as lstatSync2, readdirSync, realpathSync as realpathSync2 } from "node:fs";
|
|
2527
|
+
import { link, lstat as lstat8, readdir as readdir2, rename as rename2, unlink as unlink2 } from "node:fs/promises";
|
|
2528
|
+
import { dirname as dirname2, isAbsolute as isAbsolute5, join as join12, relative as relative3, resolve, sep as sep3 } from "node:path";
|
|
2529
|
+
|
|
2530
|
+
// src/cli/write-coordination.ts
|
|
2531
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
2532
|
+
import { lstat as lstat7, mkdir as mkdir8, realpath as realpath5 } from "node:fs/promises";
|
|
2533
|
+
import { homedir as homedir3 } from "node:os";
|
|
2534
|
+
import { isAbsolute as isAbsolute4, join as join11, relative as relative2, sep as sep2 } from "node:path";
|
|
2535
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2536
|
+
var WAIT_MS = 5000;
|
|
2537
|
+
var queues = new Map;
|
|
2538
|
+
var inside = (path3, root) => {
|
|
2539
|
+
const child = relative2(root, path3);
|
|
2540
|
+
return child === "" || child !== ".." && !child.startsWith(`..${sep2}`) && !isAbsolute4(child);
|
|
2541
|
+
};
|
|
2542
|
+
var busy = (error) => error instanceof Error && /database (?:is )?locked|SQLITE_BUSY|SQLITE_LOCKED/u.test(error.message);
|
|
2543
|
+
function workspaceCoordinationRoot() {
|
|
2544
|
+
return process.env.XCB_COORDINATION_ROOT ?? join11(homedir3(), ".local", "share", "xcb-coordination");
|
|
2545
|
+
}
|
|
2546
|
+
async function withWorkspaceWriteLock(workspace, directory2, action) {
|
|
2547
|
+
canonicalizePrivatePath(directory2, { code: "WORKSPACE_COORDINATION_LAYOUT_INVALID", reject: PRIVATE_CONTROL_REJECT, maxLength: Infinity });
|
|
2548
|
+
if (inside(directory2, workspace) || inside(workspace, directory2))
|
|
2549
|
+
throw new Error("WORKSPACE_COORDINATION_LAYOUT_INVALID");
|
|
2550
|
+
if (await realpath5(workspace) !== workspace || !(await lstat7(workspace)).isDirectory())
|
|
2551
|
+
throw new Error("WORKSPACE_ROOT_CHANGED");
|
|
2552
|
+
const path3 = join11(directory2, `${createHash6("sha256").update(workspace).digest("hex")}.sqlite`);
|
|
2553
|
+
const prior = queues.get(path3);
|
|
2554
|
+
if ((prior?.count ?? 0) >= 128)
|
|
2555
|
+
throw new Error("WORKSPACE_WRITER_QUEUE_FULL");
|
|
2556
|
+
let release;
|
|
2557
|
+
const gate = new Promise((resolve) => {
|
|
2558
|
+
release = resolve;
|
|
2559
|
+
});
|
|
2560
|
+
const previous = prior?.tail ?? Promise.resolve();
|
|
2561
|
+
const ticket = { tail: previous.then(() => gate), count: (prior?.count ?? 0) + 1 };
|
|
2562
|
+
queues.set(path3, ticket);
|
|
2563
|
+
ticket.tail.then(() => {
|
|
2564
|
+
if (queues.get(path3) === ticket)
|
|
2565
|
+
queues.delete(path3);
|
|
2566
|
+
});
|
|
2567
|
+
const deadline = performance.now() + WAIT_MS;
|
|
2568
|
+
let timer;
|
|
2569
|
+
let database;
|
|
2570
|
+
try {
|
|
2571
|
+
await Promise.race([previous, new Promise((_, reject) => {
|
|
2572
|
+
timer = setTimeout(() => reject(new Error("WORKSPACE_WRITER_BUSY")), WAIT_MS);
|
|
2573
|
+
})]);
|
|
2574
|
+
clearTimeout(timer);
|
|
2575
|
+
await mkdir8(directory2, { recursive: true, mode: 448 });
|
|
2576
|
+
await privateDirectory(directory2);
|
|
2577
|
+
try {
|
|
2578
|
+
await writeFileOnce(path3, "", { syncFile: true });
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
if (error.code !== "EEXIST")
|
|
2581
|
+
throw error;
|
|
2582
|
+
}
|
|
2583
|
+
const before = await lstat7(path3, { bigint: true });
|
|
2584
|
+
assertPrivateStat(before, {
|
|
2585
|
+
kind: "file",
|
|
2586
|
+
noSymlink: true,
|
|
2587
|
+
links: "single",
|
|
2588
|
+
owner: "self",
|
|
2589
|
+
mode: [{ mask: 63, equals: 0 }],
|
|
2590
|
+
size: { max: 64n * 1024n }
|
|
2591
|
+
}, "WORKSPACE_COORDINATION_NOT_PRIVATE");
|
|
2592
|
+
for (;; ) {
|
|
2593
|
+
if (performance.now() >= deadline)
|
|
2594
|
+
throw new Error("WORKSPACE_WRITER_BUSY");
|
|
2595
|
+
try {
|
|
2596
|
+
database = await openAccountDatabase(path3, "DELETE");
|
|
2597
|
+
database.exec("PRAGMA busy_timeout=0");
|
|
2598
|
+
database.exec("BEGIN IMMEDIATE");
|
|
2599
|
+
break;
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
database?.close();
|
|
2602
|
+
database = undefined;
|
|
2603
|
+
if (!busy(error))
|
|
2604
|
+
throw error;
|
|
2605
|
+
await delay(5);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
const after = await lstat7(path3, { bigint: true });
|
|
2609
|
+
if (!matchesPrivateStat(after, { kind: "file", links: "single", mode: [{ mask: 63, equals: 0 }] }) || !sameFileIdentity(after, before, ["dev", "ino", "uid"]))
|
|
2610
|
+
throw new Error("WORKSPACE_COORDINATION_CHANGED");
|
|
2611
|
+
if (await realpath5(workspace) !== workspace)
|
|
2612
|
+
throw new Error("WORKSPACE_ROOT_CHANGED");
|
|
2613
|
+
return await action();
|
|
2614
|
+
} finally {
|
|
2615
|
+
clearTimeout(timer);
|
|
2616
|
+
try {
|
|
2617
|
+
database?.exec("ROLLBACK");
|
|
2618
|
+
} finally {
|
|
2619
|
+
try {
|
|
2620
|
+
database?.close();
|
|
2621
|
+
} finally {
|
|
2622
|
+
release();
|
|
2623
|
+
const current = queues.get(path3);
|
|
2624
|
+
if (current !== undefined)
|
|
2625
|
+
current.count--;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// src/cli/workspace.ts
|
|
2632
|
+
var CLI_WORKSPACE_PROFILE_ID = "xcb.workspace";
|
|
2633
|
+
var CLI_WORKSPACE_PROFILE_VERSION = 1;
|
|
2634
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
2635
|
+
var MAX_ENTRIES = 512;
|
|
2636
|
+
var MAX_DEPTH = 12;
|
|
2637
|
+
var fail7 = (code) => {
|
|
2638
|
+
throw new Error(code);
|
|
2639
|
+
};
|
|
2640
|
+
function workspacePath(root, rawPath, { allowRoot = false } = {}) {
|
|
2641
|
+
const text = boundedText(rawPath, 1024);
|
|
2642
|
+
if (text.includes("\x00") || isAbsolute5(text))
|
|
2643
|
+
fail7("WORKSPACE_PATH_INVALID");
|
|
2644
|
+
const resolved = resolve(root, text);
|
|
2645
|
+
const rel = relative3(root, resolved);
|
|
2646
|
+
if (rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute5(rel) || rel === "" && !allowRoot)
|
|
2647
|
+
fail7("WORKSPACE_PATH_ESCAPES");
|
|
2648
|
+
if (rel !== "" && rel.split(sep3).length > MAX_DEPTH)
|
|
2649
|
+
fail7("WORKSPACE_PATH_DEPTH");
|
|
2650
|
+
return resolved;
|
|
2651
|
+
}
|
|
2652
|
+
function assertNoLinks(root, target, { allowLeafMissing = false } = {}) {
|
|
2653
|
+
const rel = relative3(root, target);
|
|
2654
|
+
const parts = rel.split(sep3);
|
|
2655
|
+
let cursor = root;
|
|
2656
|
+
for (const [index, part] of parts.entries()) {
|
|
2657
|
+
cursor = join12(cursor, part);
|
|
2658
|
+
let stat;
|
|
2659
|
+
try {
|
|
2660
|
+
stat = lstatSync2(cursor);
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
if (error.code === "ENOENT" && (index === parts.length - 1 ? allowLeafMissing : false))
|
|
2663
|
+
return;
|
|
2664
|
+
if (error.code === "ENOENT")
|
|
2665
|
+
fail7("WORKSPACE_PATH_MISSING");
|
|
2666
|
+
throw error;
|
|
2667
|
+
}
|
|
2668
|
+
if (stat.isSymbolicLink())
|
|
2669
|
+
fail7("WORKSPACE_PATH_LINK");
|
|
2670
|
+
if (index < parts.length - 1 && !stat.isDirectory())
|
|
2671
|
+
fail7("WORKSPACE_PATH_INVALID");
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
function revisionOf(stat) {
|
|
2675
|
+
const mtime = stat.mtimeNs !== undefined ? stat.mtimeNs : BigInt(Math.trunc(Number(stat.mtimeMs ?? 0) * 1e6));
|
|
2676
|
+
return createHash7("sha256").update(`${String(stat.dev ?? 0)}:${String(stat.ino ?? 0)}:${String(stat.size)}:${String(mtime)}`).digest("hex").slice(0, 32);
|
|
2677
|
+
}
|
|
2678
|
+
function createCliWorkspace(rootInput, options = {}) {
|
|
2679
|
+
const root = canonicalizePrivatePath(rootInput, { code: "WORKSPACE_ROOT_INVALID", reject: PRIVATE_CONTROL_REJECT, maxLength: Infinity });
|
|
2680
|
+
const identity = lstatSync2(root, { bigint: true });
|
|
2681
|
+
if (!identity.isDirectory() || identity.isSymbolicLink() || realpathSync2(root) !== root)
|
|
2682
|
+
throw new Error("WORKSPACE_ROOT_INVALID");
|
|
2683
|
+
const coordinationRoot = options.coordinationRoot ?? workspaceCoordinationRoot();
|
|
2684
|
+
const checkRoot = () => {
|
|
2685
|
+
const current = lstatSync2(root, { bigint: true });
|
|
2686
|
+
if (!current.isDirectory() || current.dev !== identity.dev || current.ino !== identity.ino || realpathSync2(root) !== root)
|
|
2687
|
+
throw new Error("WORKSPACE_ROOT_CHANGED");
|
|
2688
|
+
};
|
|
2689
|
+
const confined = (target, { allowRoot = false } = {}) => {
|
|
2690
|
+
const rel = relative3(root, target);
|
|
2691
|
+
if (rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute5(rel) || rel === "" && !allowRoot)
|
|
2692
|
+
fail7("WORKSPACE_PATH_ESCAPES");
|
|
2693
|
+
if (rel !== "" && rel.split(sep3).length > MAX_DEPTH)
|
|
2694
|
+
fail7("WORKSPACE_PATH_DEPTH");
|
|
2695
|
+
return target;
|
|
2696
|
+
};
|
|
2697
|
+
const readRevision = async (target) => {
|
|
2698
|
+
confined(target);
|
|
2699
|
+
assertNoLinks(root, target);
|
|
2700
|
+
const stat = await lstat8(target);
|
|
2701
|
+
assertPrivateStat(stat, { kind: "file", links: "single", size: { max: MAX_FILE_BYTES } }, "WORKSPACE_FILE_INVALID");
|
|
2702
|
+
const handle = await openPrivateRead(target, { nonblock: false });
|
|
2703
|
+
try {
|
|
2704
|
+
const stable = await handle.stat();
|
|
2705
|
+
assertPrivateStat(stable, { kind: "file", size: { max: MAX_FILE_BYTES } }, "WORKSPACE_FILE_INVALID");
|
|
2706
|
+
const bytes = await handle.readFile();
|
|
2707
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2708
|
+
return { text, revision: revisionOf(stable) };
|
|
2709
|
+
} finally {
|
|
2710
|
+
await handle.close();
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
const writeRevision = async (target, text, expectedRevision) => {
|
|
2714
|
+
confined(target);
|
|
2715
|
+
if (Buffer.byteLength(text) > MAX_FILE_BYTES)
|
|
2716
|
+
fail7("WORKSPACE_FILE_LIMIT");
|
|
2717
|
+
checkRoot();
|
|
2718
|
+
return withWorkspaceWriteLock(root, coordinationRoot, async () => {
|
|
2719
|
+
const check2 = async () => {
|
|
2720
|
+
checkRoot();
|
|
2721
|
+
assertNoLinks(root, target, { allowLeafMissing: true });
|
|
2722
|
+
let stat;
|
|
2723
|
+
try {
|
|
2724
|
+
stat = await lstat8(target);
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
if (error.code !== "ENOENT")
|
|
2727
|
+
throw error;
|
|
2728
|
+
}
|
|
2729
|
+
if (stat !== undefined && !matchesPrivateStat(stat, { kind: "file", links: "single", size: { max: MAX_FILE_BYTES } }))
|
|
2730
|
+
fail7("WORKSPACE_FILE_INVALID");
|
|
2731
|
+
if (expectedRevision === null && stat !== undefined)
|
|
2732
|
+
fail7("WORKSPACE_REVISION_REQUIRED");
|
|
2733
|
+
if (expectedRevision !== null) {
|
|
2734
|
+
if (stat === undefined)
|
|
2735
|
+
fail7("WORKSPACE_FILE_MISSING");
|
|
2736
|
+
if ((await readRevision(target)).revision !== expectedRevision)
|
|
2737
|
+
fail7("WORKSPACE_REVISION_MISMATCH");
|
|
2738
|
+
}
|
|
2739
|
+
return stat;
|
|
2740
|
+
};
|
|
2741
|
+
const current = await check2();
|
|
2742
|
+
const directory2 = dirname2(target);
|
|
2743
|
+
const temp = join12(directory2, `.xcb-write-${randomBytes4(16).toString("hex")}.tmp`);
|
|
2744
|
+
const staged = await openPrivateWrite(temp);
|
|
2745
|
+
try {
|
|
2746
|
+
await staged.writeFile(text);
|
|
2747
|
+
await staged.chmod(current === undefined ? 384 : current.mode & 511);
|
|
2748
|
+
await staged.sync();
|
|
2749
|
+
await check2();
|
|
2750
|
+
if (expectedRevision === null) {
|
|
2751
|
+
await link(temp, target);
|
|
2752
|
+
await unlink2(temp);
|
|
2753
|
+
} else
|
|
2754
|
+
await rename2(temp, target);
|
|
2755
|
+
await fsyncDirectory(directory2);
|
|
2756
|
+
return { revision: revisionOf(await lstat8(target)) };
|
|
2757
|
+
} finally {
|
|
2758
|
+
await staged.close();
|
|
2759
|
+
await unlink2(temp).catch((error) => {
|
|
2760
|
+
if (error.code !== "ENOENT")
|
|
2761
|
+
throw error;
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
};
|
|
2766
|
+
const listEntries = async (target) => {
|
|
2767
|
+
confined(target, { allowRoot: true });
|
|
2768
|
+
assertNoLinks(root, target);
|
|
2769
|
+
const stat = await lstat8(target);
|
|
2770
|
+
if (!stat.isDirectory())
|
|
2771
|
+
fail7("WORKSPACE_DIRECTORY_INVALID");
|
|
2772
|
+
const entries = await readdir2(target, { withFileTypes: true });
|
|
2773
|
+
const names = [];
|
|
2774
|
+
for (const entry2 of entries.slice(0, MAX_ENTRIES)) {
|
|
2775
|
+
if (entry2.isSymbolicLink())
|
|
2776
|
+
continue;
|
|
2777
|
+
if (!entry2.isDirectory() && !entry2.isFile())
|
|
2778
|
+
continue;
|
|
2779
|
+
names.push(entry2.isDirectory() ? `${entry2.name}/` : entry2.name);
|
|
2780
|
+
}
|
|
2781
|
+
return Object.freeze(names.sort());
|
|
2782
|
+
};
|
|
2783
|
+
const search = async (target, query) => {
|
|
2784
|
+
boundedText(query, 512);
|
|
2785
|
+
if (query.length === 0 || query.includes("\x00"))
|
|
2786
|
+
fail7("WORKSPACE_QUERY_INVALID");
|
|
2787
|
+
const hits = [];
|
|
2788
|
+
const visit = async (directory2, depth) => {
|
|
2789
|
+
if (depth > MAX_DEPTH || hits.length >= 128)
|
|
2790
|
+
return;
|
|
2791
|
+
let entries;
|
|
2792
|
+
try {
|
|
2793
|
+
entries = readdirSync(directory2, { withFileTypes: true });
|
|
2794
|
+
} catch {
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
for (const entry2 of entries.slice(0, MAX_ENTRIES)) {
|
|
2798
|
+
if (hits.length >= 128)
|
|
2799
|
+
return;
|
|
2800
|
+
if (entry2.isSymbolicLink() || entry2.name === "node_modules" || entry2.name === ".git" || entry2.name.startsWith(".xcb-"))
|
|
2801
|
+
continue;
|
|
2802
|
+
const path3 = join12(directory2, entry2.name);
|
|
2803
|
+
if (entry2.isDirectory()) {
|
|
2804
|
+
await visit(path3, depth + 1);
|
|
2805
|
+
continue;
|
|
2806
|
+
}
|
|
2807
|
+
if (!entry2.isFile())
|
|
2808
|
+
continue;
|
|
2809
|
+
let stat;
|
|
2810
|
+
try {
|
|
2811
|
+
stat = lstatSync2(path3);
|
|
2812
|
+
} catch {
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (!matchesPrivateStat(stat, { kind: "file", size: { max: MAX_FILE_BYTES } }))
|
|
2816
|
+
continue;
|
|
2817
|
+
let text;
|
|
2818
|
+
try {
|
|
2819
|
+
text = (await readRevision(path3)).text;
|
|
2820
|
+
} catch {
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
const lines = text.split(`
|
|
2824
|
+
`);
|
|
2825
|
+
for (const [index, line] of lines.entries()) {
|
|
2826
|
+
if (hits.length >= 128)
|
|
2827
|
+
return;
|
|
2828
|
+
if (line.includes(query))
|
|
2829
|
+
hits.push({ path: relative3(root, path3).split(sep3).join("/"), line: index + 1, text: boundedText(line.trim(), 256) });
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
await visit(target === root ? root : confined(target, { allowRoot: true }), 0);
|
|
2834
|
+
return Object.freeze(hits);
|
|
2835
|
+
};
|
|
2836
|
+
return Object.freeze({ root, readRevision, writeRevision, listEntries, search });
|
|
2837
|
+
}
|
|
2838
|
+
var objectSchema = (properties, required) => Object.freeze({
|
|
2839
|
+
type: "object",
|
|
2840
|
+
properties,
|
|
2841
|
+
required: [...required],
|
|
2842
|
+
additionalProperties: false
|
|
2843
|
+
});
|
|
2844
|
+
var requireString = (value, code) => typeof value === "string" ? value : fail7(code);
|
|
2845
|
+
function createCliWorkspaceProfile(workspace, web) {
|
|
2846
|
+
const string = (extra = {}) => Object.freeze({ type: "string", ...extra });
|
|
2847
|
+
return createCapabilityProfile({
|
|
2848
|
+
id: CLI_WORKSPACE_PROFILE_ID,
|
|
2849
|
+
version: CLI_WORKSPACE_PROFILE_VERSION,
|
|
2850
|
+
tools: [
|
|
2851
|
+
{
|
|
2852
|
+
name: "workspace.list",
|
|
2853
|
+
description: "List entries of a directory in the workspace. Paths are workspace-relative; directories end with '/'.",
|
|
2854
|
+
inputSchema: objectSchema({ path: string({ maxLength: 1024 }) }, ["path"]),
|
|
2855
|
+
parseInput(input) {
|
|
2856
|
+
return Object.freeze({ path: requireString(input.path, "WORKSPACE_INPUT_INVALID") });
|
|
2857
|
+
},
|
|
2858
|
+
async execute(input, context) {
|
|
2859
|
+
context.assertActive();
|
|
2860
|
+
const entries = await workspace.listEntries(workspacePath(workspace.root, input.path, { allowRoot: true }));
|
|
2861
|
+
context.assertActive();
|
|
2862
|
+
return { entries: [...entries] };
|
|
2863
|
+
}
|
|
2864
|
+
},
|
|
2865
|
+
{
|
|
2866
|
+
name: "workspace.read",
|
|
2867
|
+
description: "Read a UTF-8 file in the workspace (max 256 KiB) and its revision. Keep the revision to write the file later.",
|
|
2868
|
+
inputSchema: objectSchema({ path: string({ maxLength: 1024 }) }, ["path"]),
|
|
2869
|
+
parseInput(input) {
|
|
2870
|
+
return Object.freeze({ path: requireString(input.path, "WORKSPACE_INPUT_INVALID") });
|
|
2871
|
+
},
|
|
2872
|
+
async execute(input, context) {
|
|
2873
|
+
context.assertActive();
|
|
2874
|
+
const result = await workspace.readRevision(workspacePath(workspace.root, input.path));
|
|
2875
|
+
context.assertActive();
|
|
2876
|
+
return { text: result.text, revision: result.revision };
|
|
2877
|
+
}
|
|
2878
|
+
},
|
|
2879
|
+
{
|
|
2880
|
+
name: "workspace.write",
|
|
2881
|
+
description: "Write a UTF-8 file in the workspace. expectedRevision must be the revision from the last read, or null only for a new file.",
|
|
2882
|
+
inputSchema: objectSchema({ path: string({ maxLength: 1024 }), text: string({ maxLength: MAX_FILE_BYTES }), expectedRevision: Object.freeze({ anyOf: [string(), Object.freeze({ type: "null" })] }) }, ["path", "text", "expectedRevision"]),
|
|
2883
|
+
parseInput(input) {
|
|
2884
|
+
const expectedRevision = input.expectedRevision;
|
|
2885
|
+
if (expectedRevision !== null && typeof expectedRevision !== "string")
|
|
2886
|
+
fail7("WORKSPACE_INPUT_INVALID");
|
|
2887
|
+
return Object.freeze({
|
|
2888
|
+
path: requireString(input.path, "WORKSPACE_INPUT_INVALID"),
|
|
2889
|
+
text: requireString(input.text, "WORKSPACE_INPUT_INVALID"),
|
|
2890
|
+
expectedRevision: expectedRevision ?? null
|
|
2891
|
+
});
|
|
2892
|
+
},
|
|
2893
|
+
async execute(input, context) {
|
|
2894
|
+
context.assertActive();
|
|
2895
|
+
const value = input;
|
|
2896
|
+
const result = await workspace.writeRevision(workspacePath(workspace.root, value.path), boundedText(value.text, MAX_FILE_BYTES, true), value.expectedRevision === null ? null : boundedText(value.expectedRevision, 160));
|
|
2897
|
+
context.assertActive();
|
|
2898
|
+
return { revision: result.revision };
|
|
2899
|
+
}
|
|
2900
|
+
},
|
|
2901
|
+
{
|
|
2902
|
+
name: "workspace.search",
|
|
2903
|
+
description: "Find literal text in workspace files (skips .git, node_modules and symlinks; max 128 matches).",
|
|
2904
|
+
inputSchema: objectSchema({ path: string({ maxLength: 1024 }), query: string({ minLength: 1, maxLength: 512 }) }, ["path", "query"]),
|
|
2905
|
+
parseInput(input) {
|
|
2906
|
+
const query = requireString(input.query, "WORKSPACE_INPUT_INVALID");
|
|
2907
|
+
if (query.length === 0)
|
|
2908
|
+
fail7("WORKSPACE_INPUT_INVALID");
|
|
2909
|
+
return Object.freeze({ path: requireString(input.path, "WORKSPACE_INPUT_INVALID"), query });
|
|
2910
|
+
},
|
|
2911
|
+
async execute(input, context) {
|
|
2912
|
+
context.assertActive();
|
|
2913
|
+
const value = input;
|
|
2914
|
+
const matches = await workspace.search(workspacePath(workspace.root, value.path, { allowRoot: true }), boundedText(value.query, 512));
|
|
2915
|
+
context.assertActive();
|
|
2916
|
+
return { matches: matches.map((match) => Object.freeze({ ...match })) };
|
|
2917
|
+
}
|
|
2918
|
+
},
|
|
2919
|
+
...web === undefined ? [] : [{
|
|
2920
|
+
name: "web.fetch",
|
|
2921
|
+
description: "Fetch bounded public HTTPS text (256 KiB, 15s). Private addresses and authenticated requests are unavailable.",
|
|
2922
|
+
inputSchema: objectSchema({ url: string({ maxLength: 2048 }) }, ["url"]),
|
|
2923
|
+
parseInput(input) {
|
|
2924
|
+
return Object.freeze({ url: requireString(input.url, "WORKSPACE_INPUT_INVALID") });
|
|
2925
|
+
},
|
|
2926
|
+
async execute(input, context) {
|
|
2927
|
+
context.assertActive();
|
|
2928
|
+
const result = await web.fetch(boundedText(input.url, 2048), context.signal);
|
|
2929
|
+
context.assertActive();
|
|
2930
|
+
return { text: result.text };
|
|
2931
|
+
}
|
|
2932
|
+
}]
|
|
2933
|
+
]
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// src/cli/run.ts
|
|
2938
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
2939
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2940
|
+
var MAX_CONTEXT_BYTES = 64 * 1024;
|
|
2941
|
+
var MAX_RUN_MS = 10 * 60 * 1000;
|
|
2942
|
+
var MAX_CLEANUP_MS = 30 * 1000;
|
|
2943
|
+
var MAX_OUTPUT_BYTES = 256 * 1024;
|
|
2944
|
+
function promptWithContext(prompt, prior) {
|
|
2945
|
+
if (prior.length === 0)
|
|
2946
|
+
return prompt;
|
|
2947
|
+
const lines = ["Prior conversation in this session (recorded locally):"];
|
|
2948
|
+
let bytes = 0;
|
|
2949
|
+
const selected = [];
|
|
2950
|
+
for (const entry2 of [...prior].reverse()) {
|
|
2951
|
+
const line = `${entry2.role === "user" ? "User" : "xcb"}: ${entry2.text}`;
|
|
2952
|
+
bytes += Buffer.byteLength(line) + 1;
|
|
2953
|
+
if (bytes > MAX_CONTEXT_BYTES || selected.length >= 40)
|
|
2954
|
+
break;
|
|
2955
|
+
selected.unshift(line);
|
|
2956
|
+
}
|
|
2957
|
+
return [...lines, ...selected, "", "Current request:", prompt].join(`
|
|
2958
|
+
`);
|
|
2959
|
+
}
|
|
2960
|
+
async function recoverHeldLease(leases, provider2, accountId, proveStopped) {
|
|
2961
|
+
if (leases.inspect === undefined || leases.recover === undefined)
|
|
2962
|
+
return;
|
|
2963
|
+
const held = leases.inspect(provider2, accountId);
|
|
2964
|
+
if (held === null)
|
|
2965
|
+
return;
|
|
2966
|
+
if (proveStopped === undefined)
|
|
2967
|
+
throw new Error("ACCOUNT_PROCESS_STOP_UNPROVEN");
|
|
2968
|
+
const recovered = await leases.recover(held, async (lease) => {
|
|
2969
|
+
let timer;
|
|
2970
|
+
try {
|
|
2971
|
+
const proven = await Promise.race([
|
|
2972
|
+
proveStopped(Object.freeze({ ...lease })),
|
|
2973
|
+
new Promise((resolve2) => {
|
|
2974
|
+
timer = setTimeout(() => resolve2(false), 5000);
|
|
2975
|
+
})
|
|
2976
|
+
]);
|
|
2977
|
+
if (proven !== true)
|
|
2978
|
+
return false;
|
|
2979
|
+
const probe = spawnSync5("pgrep", ["-f", `${provider2}-run-${lease.owner}`], { timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
|
|
2980
|
+
return probe.status === 1;
|
|
2981
|
+
} finally {
|
|
2982
|
+
clearTimeout(timer);
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
if (!recovered)
|
|
2986
|
+
throw new Error("ACCOUNT_RECOVERY_CHANGED");
|
|
2987
|
+
}
|
|
2988
|
+
async function runCliTurn(input) {
|
|
2989
|
+
const workspaceId = identifier(input.workspaceId);
|
|
2990
|
+
const accountId = identifier(input.accountId);
|
|
2991
|
+
const model = Object.freeze({ id: boundedText(input.model, 160), reasoningEffort: null, serviceTier: null });
|
|
2992
|
+
const profile = input.profile;
|
|
2993
|
+
const prompt = boundedText(promptWithContext(input.prompt, input.prior), 512 * 1024);
|
|
2994
|
+
let active = true;
|
|
2995
|
+
const attempt = () => {
|
|
2996
|
+
const runId = `run_${randomBytes5(12).toString("hex")}`;
|
|
2997
|
+
const inner = createCapabilityBroker({
|
|
2998
|
+
profile,
|
|
2999
|
+
workspaceId,
|
|
3000
|
+
runId,
|
|
3001
|
+
isActive: () => active && !input.signal.aborted,
|
|
3002
|
+
signal: input.signal
|
|
3003
|
+
});
|
|
3004
|
+
const broker = Object.freeze({
|
|
3005
|
+
profile: inner.profile,
|
|
3006
|
+
workspaceId: inner.workspaceId,
|
|
3007
|
+
runId: inner.runId,
|
|
3008
|
+
assertActive: () => inner.assertActive(),
|
|
3009
|
+
revoke: () => inner.revoke(),
|
|
3010
|
+
close: () => inner.close(),
|
|
3011
|
+
invoke: (name, callInput) => {
|
|
3012
|
+
if (typeof name === "string")
|
|
3013
|
+
input.onTool?.(name, callInput);
|
|
3014
|
+
return inner.invoke(name, callInput);
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
return runAgentTask({ adapters: [input.adapter], leases: input.leases, now: input.now ?? Date.now }, {
|
|
3018
|
+
route: input.adapter.route,
|
|
3019
|
+
accountId,
|
|
3020
|
+
workspaceId,
|
|
3021
|
+
runId,
|
|
3022
|
+
profile: Object.freeze({ id: profile.id, version: profile.version, digest: profile.digest }),
|
|
3023
|
+
model,
|
|
3024
|
+
purpose: "cli-turn",
|
|
3025
|
+
prompt,
|
|
3026
|
+
limits: Object.freeze({ maxRunMs: MAX_RUN_MS, maxCleanupMs: MAX_CLEANUP_MS, maxOutputBytes: MAX_OUTPUT_BYTES }),
|
|
3027
|
+
signal: input.signal
|
|
3028
|
+
}, broker);
|
|
3029
|
+
};
|
|
3030
|
+
try {
|
|
3031
|
+
try {
|
|
3032
|
+
const result = await attempt();
|
|
3033
|
+
return Object.freeze({ result, output: result.output });
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
if (!(error instanceof Error) || error.message !== "ACCOUNT_BUSY_OR_RECOVERY_REQUIRED")
|
|
3036
|
+
throw error;
|
|
3037
|
+
await recoverHeldLease(input.leases, input.adapter.route.provider, accountId, input.proveAccountStopped);
|
|
3038
|
+
const result = await attempt();
|
|
3039
|
+
return Object.freeze({ result, output: result.output });
|
|
3040
|
+
}
|
|
3041
|
+
} finally {
|
|
3042
|
+
active = false;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// src/cli/chat.ts
|
|
3047
|
+
import { isAbsolute as isAbsolute6, resolve as resolve2 } from "node:path";
|
|
3048
|
+
import { mkdir as mkdir9, realpath as realpath6, lstat as lstat9 } from "node:fs/promises";
|
|
3049
|
+
import { join as join13 } from "node:path";
|
|
3050
|
+
|
|
3051
|
+
// src/cli/tui.ts
|
|
3052
|
+
var ESC = "\x1B";
|
|
3053
|
+
var color = (code, text) => process.env.NO_COLOR === undefined && process.stdout.isTTY ? `${ESC}[${code}m${text}${ESC}[0m` : text;
|
|
3054
|
+
var dim = (text) => color(90, text);
|
|
3055
|
+
var bold = (text) => color(1, text);
|
|
3056
|
+
var cyan = (text) => color(36, text);
|
|
3057
|
+
var yellow = (text) => color(33, text);
|
|
3058
|
+
var red = (text) => color(31, text);
|
|
3059
|
+
var green = (text) => color(32, text);
|
|
3060
|
+
var MAX_LINE_BYTES = 16 * 1024;
|
|
3061
|
+
var MAX_HISTORY = 200;
|
|
3062
|
+
|
|
3063
|
+
class LineEditor {
|
|
3064
|
+
history = [];
|
|
3065
|
+
stdin = process.stdin;
|
|
3066
|
+
stdout = process.stdout;
|
|
3067
|
+
interrupted = null;
|
|
3068
|
+
interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
3069
|
+
constructor() {
|
|
3070
|
+
if (this.interactive) {
|
|
3071
|
+
this.stdin.setRawMode(true);
|
|
3072
|
+
this.stdout.write(`${ESC}[?2004h`);
|
|
3073
|
+
}
|
|
3074
|
+
this.stdin.on("data", (chunk) => this.feed(chunk));
|
|
3075
|
+
this.stdin.on("end", this.onEnd);
|
|
3076
|
+
}
|
|
3077
|
+
onEnd = () => {
|
|
3078
|
+
if (this.resolveLine) {
|
|
3079
|
+
const done = this.resolveLine;
|
|
3080
|
+
done(null);
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
onInterrupt(handler) {
|
|
3084
|
+
this.interrupted = handler;
|
|
3085
|
+
}
|
|
3086
|
+
close() {
|
|
3087
|
+
if (this.interactive) {
|
|
3088
|
+
this.stdout.write(`${ESC}[?2004l`);
|
|
3089
|
+
this.stdin.setRawMode(false);
|
|
3090
|
+
}
|
|
3091
|
+
this.stdin.removeAllListeners("data");
|
|
3092
|
+
this.stdin.removeListener("end", this.onEnd);
|
|
3093
|
+
}
|
|
3094
|
+
buffer = "";
|
|
3095
|
+
cursor = 0;
|
|
3096
|
+
historyIndex = -1;
|
|
3097
|
+
draft = "";
|
|
3098
|
+
paste = false;
|
|
3099
|
+
resolveLine = null;
|
|
3100
|
+
async readLine(prompt) {
|
|
3101
|
+
if (this.resolveLine)
|
|
3102
|
+
throw new Error("TUI_BUSY");
|
|
3103
|
+
this.buffer = "";
|
|
3104
|
+
this.cursor = 0;
|
|
3105
|
+
this.historyIndex = -1;
|
|
3106
|
+
this.draft = "";
|
|
3107
|
+
this.stdout.write(prompt);
|
|
3108
|
+
return await new Promise((resolve2) => {
|
|
3109
|
+
this.resolveLine = (line) => {
|
|
3110
|
+
this.resolveLine = null;
|
|
3111
|
+
if (line !== null && line.trim() !== "" && this.history[0] !== line) {
|
|
3112
|
+
this.history.unshift(boundedText(line, MAX_LINE_BYTES));
|
|
3113
|
+
if (this.history.length > MAX_HISTORY)
|
|
3114
|
+
this.history.pop();
|
|
3115
|
+
}
|
|
3116
|
+
this.stdout.write(`
|
|
3117
|
+
`);
|
|
3118
|
+
resolve2(line);
|
|
3119
|
+
};
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
render() {
|
|
3123
|
+
if (!this.interactive)
|
|
3124
|
+
return;
|
|
3125
|
+
const shown = this.buffer;
|
|
3126
|
+
this.stdout.write(`\r${ESC}[K` + dim("›") + " " + shown);
|
|
3127
|
+
const back = shown.length - this.cursor;
|
|
3128
|
+
if (back > 0)
|
|
3129
|
+
this.stdout.write(`${ESC}[${back}D`);
|
|
3130
|
+
}
|
|
3131
|
+
feed(chunk) {
|
|
3132
|
+
let i = 0;
|
|
3133
|
+
const bytes = chunk;
|
|
3134
|
+
while (i < bytes.length) {
|
|
3135
|
+
const b = bytes[i];
|
|
3136
|
+
if (b === 27 && bytes.subarray(i, i + 6).toString("latin1") === `${ESC}[200~`) {
|
|
3137
|
+
this.paste = true;
|
|
3138
|
+
i += 6;
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3141
|
+
if (b === 27 && bytes.subarray(i, i + 6).toString("latin1") === `${ESC}[201~`) {
|
|
3142
|
+
this.paste = false;
|
|
3143
|
+
i += 6;
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
if (b === 3) {
|
|
3147
|
+
this.interrupted?.();
|
|
3148
|
+
if (this.resolveLine) {
|
|
3149
|
+
const done = this.resolveLine;
|
|
3150
|
+
this.buffer = "";
|
|
3151
|
+
this.render();
|
|
3152
|
+
done("");
|
|
3153
|
+
}
|
|
3154
|
+
i += 1;
|
|
3155
|
+
continue;
|
|
3156
|
+
}
|
|
3157
|
+
if (b === 4) {
|
|
3158
|
+
if (this.resolveLine && this.buffer.length === 0) {
|
|
3159
|
+
const done = this.resolveLine;
|
|
3160
|
+
done(null);
|
|
3161
|
+
}
|
|
3162
|
+
i += 1;
|
|
3163
|
+
continue;
|
|
3164
|
+
}
|
|
3165
|
+
if (!this.resolveLine) {
|
|
3166
|
+
i += 1;
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
if (b === 13 || b === 10 && !this.paste) {
|
|
3170
|
+
const done = this.resolveLine, line = this.buffer;
|
|
3171
|
+
this.buffer = "";
|
|
3172
|
+
this.cursor = 0;
|
|
3173
|
+
done(line === null ? null : boundedText(line, MAX_LINE_BYTES));
|
|
3174
|
+
i += 1;
|
|
3175
|
+
continue;
|
|
3176
|
+
}
|
|
3177
|
+
if (b === 127) {
|
|
3178
|
+
if (this.cursor > 0) {
|
|
3179
|
+
this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
|
|
3180
|
+
this.cursor -= 1;
|
|
3181
|
+
this.render();
|
|
3182
|
+
}
|
|
3183
|
+
i += 1;
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
if (b === 1) {
|
|
3187
|
+
this.cursor = 0;
|
|
3188
|
+
this.render();
|
|
3189
|
+
i += 1;
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
if (b === 5) {
|
|
3193
|
+
this.cursor = this.buffer.length;
|
|
3194
|
+
this.render();
|
|
3195
|
+
i += 1;
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
if (b === 11) {
|
|
3199
|
+
this.buffer = this.buffer.slice(0, this.cursor);
|
|
3200
|
+
this.render();
|
|
3201
|
+
i += 1;
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
if (b === 21) {
|
|
3205
|
+
this.buffer = this.buffer.slice(this.cursor);
|
|
3206
|
+
this.cursor = 0;
|
|
3207
|
+
this.render();
|
|
3208
|
+
i += 1;
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3211
|
+
if (b === 23) {
|
|
3212
|
+
const before = this.buffer.slice(0, this.cursor).replace(/\s+$/u, "").replace(/\S+$/u, "");
|
|
3213
|
+
this.buffer = before + this.buffer.slice(this.cursor);
|
|
3214
|
+
this.cursor = before.length;
|
|
3215
|
+
this.render();
|
|
3216
|
+
i += 1;
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
if (b === 27) {
|
|
3220
|
+
const seq = bytes.subarray(i, i + 4).toString("latin1");
|
|
3221
|
+
if (seq.startsWith(`${ESC}[A`) || seq.startsWith(`${ESC}OA`)) {
|
|
3222
|
+
if (this.historyIndex === -1)
|
|
3223
|
+
this.draft = this.buffer;
|
|
3224
|
+
if (this.historyIndex + 1 < this.history.length) {
|
|
3225
|
+
this.historyIndex += 1;
|
|
3226
|
+
this.buffer = this.history[this.historyIndex];
|
|
3227
|
+
this.cursor = this.buffer.length;
|
|
3228
|
+
this.render();
|
|
3229
|
+
}
|
|
3230
|
+
i += 3;
|
|
3231
|
+
continue;
|
|
3232
|
+
}
|
|
3233
|
+
if (seq.startsWith(`${ESC}[B`) || seq.startsWith(`${ESC}OB`)) {
|
|
3234
|
+
if (this.historyIndex > 0) {
|
|
3235
|
+
this.historyIndex -= 1;
|
|
3236
|
+
this.buffer = this.history[this.historyIndex];
|
|
3237
|
+
} else {
|
|
3238
|
+
this.historyIndex = -1;
|
|
3239
|
+
this.buffer = this.draft;
|
|
3240
|
+
}
|
|
3241
|
+
this.cursor = this.buffer.length;
|
|
3242
|
+
this.render();
|
|
3243
|
+
i += 3;
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
if (seq.startsWith(`${ESC}[C`) || seq.startsWith(`${ESC}OC`)) {
|
|
3247
|
+
this.cursor = Math.min(this.buffer.length, this.cursor + 1);
|
|
3248
|
+
this.render();
|
|
3249
|
+
i += 3;
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
if (seq.startsWith(`${ESC}[D`) || seq.startsWith(`${ESC}OD`)) {
|
|
3253
|
+
this.cursor = Math.max(0, this.cursor - 1);
|
|
3254
|
+
this.render();
|
|
3255
|
+
i += 3;
|
|
3256
|
+
continue;
|
|
3257
|
+
}
|
|
3258
|
+
if (seq.startsWith(`${ESC}[H`) || seq.startsWith(`${ESC}[1~`)) {
|
|
3259
|
+
this.cursor = 0;
|
|
3260
|
+
this.render();
|
|
3261
|
+
i += 3;
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
if (seq.startsWith(`${ESC}[F`) || seq.startsWith(`${ESC}[4~`)) {
|
|
3265
|
+
this.cursor = this.buffer.length;
|
|
3266
|
+
this.render();
|
|
3267
|
+
i += 3;
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
i += 1;
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
const rest = bytes.subarray(i);
|
|
3274
|
+
const charLength = b < 128 ? 1 : b < 224 ? 2 : b < 240 ? 3 : 4;
|
|
3275
|
+
const text = rest.subarray(0, charLength).toString("utf8");
|
|
3276
|
+
const insert = text === `
|
|
3277
|
+
` || text === "\r" ? " " : text;
|
|
3278
|
+
if (Buffer.byteLength(this.buffer) + Buffer.byteLength(insert) <= MAX_LINE_BYTES) {
|
|
3279
|
+
this.buffer = this.buffer.slice(0, this.cursor) + insert + this.buffer.slice(this.cursor);
|
|
3280
|
+
this.cursor += insert.length;
|
|
3281
|
+
this.render();
|
|
3282
|
+
}
|
|
3283
|
+
i += charLength;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
function startSpinner(label) {
|
|
3288
|
+
if (!process.stdout.isTTY)
|
|
3289
|
+
return Object.freeze({ stop() {} });
|
|
3290
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3291
|
+
let index = 0;
|
|
3292
|
+
const write = () => process.stdout.write(`\r${ESC}[K${cyan(frames[index % frames.length])} ${dim(label())}`);
|
|
3293
|
+
const timer = setInterval(() => {
|
|
3294
|
+
index += 1;
|
|
3295
|
+
write();
|
|
3296
|
+
}, 80);
|
|
3297
|
+
write();
|
|
3298
|
+
return Object.freeze({
|
|
3299
|
+
stop() {
|
|
3300
|
+
clearInterval(timer);
|
|
3301
|
+
process.stdout.write(`\r${ESC}[K`);
|
|
3302
|
+
}
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
function printTool(name, input) {
|
|
3306
|
+
let detail = "";
|
|
3307
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
3308
|
+
const record2 = input;
|
|
3309
|
+
const first = typeof record2.path === "string" ? record2.path : typeof record2.url === "string" ? record2.url : typeof record2.query === "string" ? record2.query : "";
|
|
3310
|
+
detail = first === "" ? "" : ` ${boundedText(first, 96)}`;
|
|
3311
|
+
}
|
|
3312
|
+
process.stdout.write(`${dim(" ⚙")} ${dim(name)}${dim(detail)}
|
|
3313
|
+
`);
|
|
3314
|
+
}
|
|
3315
|
+
function printRemainingText(output, streamedAll, streamedLast) {
|
|
3316
|
+
const final = output ?? "";
|
|
3317
|
+
if (final === "")
|
|
3318
|
+
return;
|
|
3319
|
+
if (streamedAll === "") {
|
|
3320
|
+
process.stdout.write(`${final}
|
|
3321
|
+
`);
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
const tail = final === streamedLast || final === streamedAll ? "" : final.startsWith(streamedAll) ? final.slice(streamedAll.length).replaceAll(/^\n+/u, "") : final;
|
|
3325
|
+
if (tail !== "")
|
|
3326
|
+
process.stdout.write(`${tail}
|
|
3327
|
+
`);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// src/cli/chat.ts
|
|
3331
|
+
var ACCOUNT_ID = "local";
|
|
3332
|
+
async function resolveWorkspace(raw) {
|
|
3333
|
+
const path3 = isAbsolute6(raw) ? raw : resolve2(raw);
|
|
3334
|
+
try {
|
|
3335
|
+
const actual = await realpath6(path3);
|
|
3336
|
+
const stat = await lstat9(actual);
|
|
3337
|
+
if (!stat.isDirectory())
|
|
3338
|
+
throw new Error("WORKSPACE_INVALID");
|
|
3339
|
+
return actual;
|
|
3340
|
+
} catch (error) {
|
|
3341
|
+
if (error instanceof Error && error.message === "WORKSPACE_INVALID")
|
|
3342
|
+
throw new Error(`not a directory: ${path3}`);
|
|
3343
|
+
throw new Error(`workspace path does not exist: ${path3}`);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
var HELP = `Commands:
|
|
3347
|
+
/help show this help
|
|
3348
|
+
/new start a new session
|
|
3349
|
+
/sessions list local sessions
|
|
3350
|
+
/model <id> switch model (current session only)
|
|
3351
|
+
/quit exit
|
|
3352
|
+
|
|
3353
|
+
Anything else is sent to the provider as a task. Ctrl-C cancels a running turn;
|
|
3354
|
+
Ctrl-D exits. Files stay inside the opened directory; the provider can only
|
|
3355
|
+
list, read, search and write through the brokered workspace tools.`;
|
|
3356
|
+
function describe(state, provider2) {
|
|
3357
|
+
if (state.status === "ready")
|
|
3358
|
+
return "";
|
|
3359
|
+
if (state.detail !== undefined)
|
|
3360
|
+
return state.detail;
|
|
3361
|
+
if (state.status === "binary-missing")
|
|
3362
|
+
return `${provider2} binary not found — install the provider CLI and run \`xcb-compat doctor\`.`;
|
|
3363
|
+
if (state.status === "version-mismatch")
|
|
3364
|
+
return `${provider2} ${state.inspection?.version} found but this build requires the pinned version — run \`xcb-compat doctor\`.`;
|
|
3365
|
+
if (state.status === "sandbox-unavailable")
|
|
3366
|
+
return "linux confinement unavailable — needs bubblewrap (`bwrap`) plus unprivileged user namespaces (Ubuntu 23.10+: `sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0`). Refusing to run unsandboxed.";
|
|
3367
|
+
return `provider not admitted — run \`xcb-compat doctor\`, then \`xcb-compat auth ${provider2}\` if needed.`;
|
|
3368
|
+
}
|
|
3369
|
+
async function runCliChat(options) {
|
|
3370
|
+
const { path: stateRoot } = await ensureCliState();
|
|
3371
|
+
const sessions = await CliSessionStore.open(join13(stateRoot, "sessions"));
|
|
3372
|
+
let workspacePath2;
|
|
3373
|
+
let resumed = null;
|
|
3374
|
+
if (options.sessionId !== undefined) {
|
|
3375
|
+
resumed = sessions.get(options.sessionId);
|
|
3376
|
+
if (resumed === null) {
|
|
3377
|
+
process.stderr.write(`${red("xcb-compat:")} session not found.
|
|
3378
|
+
`);
|
|
3379
|
+
sessions.close();
|
|
3380
|
+
return 2;
|
|
3381
|
+
}
|
|
3382
|
+
try {
|
|
3383
|
+
workspacePath2 = await resolveWorkspace(resumed.workspace);
|
|
3384
|
+
} catch {
|
|
3385
|
+
process.stderr.write(`${red("xcb-compat:")} session workspace is gone: ${resumed.workspace}
|
|
3386
|
+
`);
|
|
3387
|
+
sessions.close();
|
|
3388
|
+
return 2;
|
|
3389
|
+
}
|
|
3390
|
+
} else {
|
|
3391
|
+
try {
|
|
3392
|
+
workspacePath2 = await resolveWorkspace(options.workspace);
|
|
3393
|
+
} catch (error) {
|
|
3394
|
+
process.stderr.write(`${red("xcb-compat:")} ${error instanceof Error ? error.message : "invalid workspace"}
|
|
3395
|
+
`);
|
|
3396
|
+
sessions.close();
|
|
3397
|
+
return 2;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
try {
|
|
3401
|
+
assertWorkspaceStateSeparation(workspacePath2, stateRoot);
|
|
3402
|
+
} catch (error) {
|
|
3403
|
+
sessions.close();
|
|
3404
|
+
throw error;
|
|
3405
|
+
}
|
|
3406
|
+
const providerName = options.provider ?? resumed?.provider ?? "claude";
|
|
3407
|
+
if (resumed !== null && resumed.provider !== providerName) {
|
|
3408
|
+
process.stderr.write(`${red("xcb-compat:")} session ${resumed.id} belongs to provider ${resumed.provider} — resume with \`--provider ${resumed.provider}\`.
|
|
3409
|
+
`);
|
|
3410
|
+
sessions.close();
|
|
3411
|
+
return 2;
|
|
3412
|
+
}
|
|
3413
|
+
await mkdir9(join13(stateRoot, "runs"), { mode: 448, recursive: true });
|
|
3414
|
+
const leases = new SqliteAccountLeases(await openAccountDatabase(join13(stateRoot, "account-leases.sqlite")));
|
|
3415
|
+
const web = createPublicWeb();
|
|
3416
|
+
const workspace = createCliWorkspace(workspacePath2);
|
|
3417
|
+
const profile = createCliWorkspaceProfile(workspace, { fetch: (url, signal) => web.fetchPublic(url, 256 * 1024, signal).then((r) => ({ text: r.text })) });
|
|
3418
|
+
const events = {};
|
|
3419
|
+
const opened = await openCliProvider(stateRoot, providerName, profile, events, { workspaceRoot: workspace.root });
|
|
3420
|
+
if (opened.status !== "ready") {
|
|
3421
|
+
process.stderr.write(`${red("xcb-compat:")} ${describe(opened, providerName)}
|
|
3422
|
+
`);
|
|
3423
|
+
sessions.close();
|
|
3424
|
+
return 2;
|
|
3425
|
+
}
|
|
3426
|
+
if (providerName === "claude") {
|
|
3427
|
+
const auth = await claudeAuthStatus(stateRoot);
|
|
3428
|
+
if (!auth.loggedIn) {
|
|
3429
|
+
process.stderr.write(`${red("xcb-compat:")} not signed in — run ${bold("xcb-compat auth claude")} first.
|
|
3430
|
+
`);
|
|
3431
|
+
sessions.close();
|
|
3432
|
+
return 2;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
if (providerName === "codex") {
|
|
3436
|
+
const codexInspection = await inspectCliBinary("codex");
|
|
3437
|
+
if (codexInspection !== null) {
|
|
3438
|
+
const codex = await codexAuthStatus(stateRoot, codexInspection);
|
|
3439
|
+
if (!codex.loggedIn) {
|
|
3440
|
+
process.stderr.write(`${red("xcb-compat:")} not signed in — run ${bold("xcb-compat auth codex")} first.
|
|
3441
|
+
`);
|
|
3442
|
+
sessions.close();
|
|
3443
|
+
return 2;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
if (providerName === "devin") {
|
|
3448
|
+
const devinInspection = await inspectCliBinary("devin");
|
|
3449
|
+
if (devinInspection !== null) {
|
|
3450
|
+
const devin = await devinAuthStatus(stateRoot, devinInspection);
|
|
3451
|
+
if (!devin.loggedIn) {
|
|
3452
|
+
process.stderr.write(`${red("xcb-compat:")} not signed in — run ${bold("xcb-compat auth devin")} first.
|
|
3453
|
+
`);
|
|
3454
|
+
sessions.close();
|
|
3455
|
+
return 2;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
const model = boundedText(options.model ?? (providerName === "claude" ? CLI_CLAUDE_DEFAULT_MODEL : providerName === "devin" ? CLI_DEVIN_DEFAULT_MODEL : CLI_CODEX_DEFAULT_MODEL), 160);
|
|
3460
|
+
let session;
|
|
3461
|
+
if (resumed !== null) {
|
|
3462
|
+
session = resumed;
|
|
3463
|
+
} else {
|
|
3464
|
+
session = await sessions.create({ provider: providerName, accountId: ACCOUNT_ID, workspace: workspacePath2, model, now: Date.now() });
|
|
3465
|
+
}
|
|
3466
|
+
const editor = new LineEditor;
|
|
3467
|
+
const turn = { controller: null };
|
|
3468
|
+
editor.onInterrupt(() => turn.controller?.abort());
|
|
3469
|
+
let currentModel = options.model ?? session.model;
|
|
3470
|
+
process.stdout.write(`${bold("xcb")} ${dim("·")} ${cyan(providerName)} ${dim(currentModel)} ${dim("·")} ${workspacePath2}
|
|
3471
|
+
`);
|
|
3472
|
+
process.stdout.write(dim(`session ${session.id} — /help for commands, Ctrl-D to exit
|
|
3473
|
+
|
|
3474
|
+
`));
|
|
3475
|
+
try {
|
|
3476
|
+
for (;; ) {
|
|
3477
|
+
const line = await editor.readLine("");
|
|
3478
|
+
if (line === null)
|
|
3479
|
+
break;
|
|
3480
|
+
const text = line.trim();
|
|
3481
|
+
if (text === "")
|
|
3482
|
+
continue;
|
|
3483
|
+
if (text.startsWith("/")) {
|
|
3484
|
+
const [command, ...rest] = text.slice(1).split(/\s+/u);
|
|
3485
|
+
if (command === "quit" || command === "exit")
|
|
3486
|
+
break;
|
|
3487
|
+
if (command === "help") {
|
|
3488
|
+
process.stdout.write(`${HELP}
|
|
3489
|
+
|
|
3490
|
+
`);
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
if (command === "new") {
|
|
3494
|
+
session = await sessions.create({ provider: providerName, accountId: ACCOUNT_ID, workspace: workspacePath2, model: currentModel, now: Date.now() });
|
|
3495
|
+
process.stdout.write(dim(`session ${session.id}
|
|
3496
|
+
|
|
3497
|
+
`));
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
if (command === "sessions") {
|
|
3501
|
+
for (const item of sessions.list(20)) {
|
|
3502
|
+
process.stdout.write(`${item.id === session.id ? green("→") : " "} ${item.id} ${dim(item.provider)} ${dim(item.title || "(untitled)")} ${dim(new Date(item.lastActiveAt).toISOString())}
|
|
3503
|
+
`);
|
|
3504
|
+
}
|
|
3505
|
+
process.stdout.write(`
|
|
3506
|
+
`);
|
|
3507
|
+
continue;
|
|
3508
|
+
}
|
|
3509
|
+
if (command === "model") {
|
|
3510
|
+
const next = rest.join(" ").trim();
|
|
3511
|
+
if (next === "") {
|
|
3512
|
+
process.stdout.write(dim(`model: ${currentModel}
|
|
3513
|
+
|
|
3514
|
+
`));
|
|
3515
|
+
continue;
|
|
3516
|
+
}
|
|
3517
|
+
currentModel = boundedText(next, 160);
|
|
3518
|
+
process.stdout.write(dim(`model: ${currentModel} (new sessions keep their recorded model)
|
|
3519
|
+
|
|
3520
|
+
`));
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
process.stdout.write(`${yellow("?")} unknown command /${boundedText(command ?? "", 32)} — /help
|
|
3524
|
+
|
|
3525
|
+
`);
|
|
3526
|
+
continue;
|
|
3527
|
+
}
|
|
3528
|
+
const controller = new AbortController;
|
|
3529
|
+
turn.controller = controller;
|
|
3530
|
+
const spinner = startSpinner(() => `${providerName} is thinking`);
|
|
3531
|
+
let toolCalls = 0;
|
|
3532
|
+
let streamedAll = "";
|
|
3533
|
+
let streamedLast = "";
|
|
3534
|
+
let providerError = null;
|
|
3535
|
+
events.onAssistantText = (block) => {
|
|
3536
|
+
spinner.stop();
|
|
3537
|
+
if (!process.stdout.isTTY)
|
|
3538
|
+
return;
|
|
3539
|
+
streamedAll += (streamedAll === "" ? "" : `
|
|
3540
|
+
`) + block;
|
|
3541
|
+
streamedLast = block;
|
|
3542
|
+
process.stdout.write(`${block}
|
|
3543
|
+
`);
|
|
3544
|
+
};
|
|
3545
|
+
events.onProviderError = (text2) => {
|
|
3546
|
+
providerError = text2;
|
|
3547
|
+
};
|
|
3548
|
+
try {
|
|
3549
|
+
const prior = await sessions.transcript(session.id);
|
|
3550
|
+
const { result, output } = await runCliTurn({
|
|
3551
|
+
adapter: opened.adapter,
|
|
3552
|
+
leases,
|
|
3553
|
+
profile,
|
|
3554
|
+
accountId: ACCOUNT_ID,
|
|
3555
|
+
workspaceId: session.id,
|
|
3556
|
+
model: currentModel,
|
|
3557
|
+
prompt: text,
|
|
3558
|
+
prior,
|
|
3559
|
+
signal: controller.signal,
|
|
3560
|
+
onTool: (name, input) => {
|
|
3561
|
+
toolCalls += 1;
|
|
3562
|
+
if (toolCalls <= 40) {
|
|
3563
|
+
spinner.stop();
|
|
3564
|
+
printTool(name, input);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
});
|
|
3568
|
+
spinner.stop();
|
|
3569
|
+
const entries = [{ role: "user", text, at: Date.now() }];
|
|
3570
|
+
if (output !== null)
|
|
3571
|
+
entries.push({ role: "assistant", text: output, at: Date.now() });
|
|
3572
|
+
session = await sessions.record(session, entries, Date.now());
|
|
3573
|
+
if (result.outcome.status !== "completed") {
|
|
3574
|
+
process.stdout.write(`${red("✗")} ${dim(boundedText(result.outcome.code ?? "run failed", 120))}
|
|
3575
|
+
`);
|
|
3576
|
+
if (providerError !== null)
|
|
3577
|
+
process.stdout.write(`${dim(providerError)}
|
|
3578
|
+
`);
|
|
3579
|
+
process.stdout.write(`
|
|
3580
|
+
`);
|
|
3581
|
+
} else {
|
|
3582
|
+
printRemainingText(output, streamedAll, streamedLast);
|
|
3583
|
+
process.stdout.write(`
|
|
3584
|
+
`);
|
|
3585
|
+
}
|
|
3586
|
+
} catch (error) {
|
|
3587
|
+
spinner.stop();
|
|
3588
|
+
const code = error instanceof Error ? boundedText(error.message, 160) : "CLI_RUN_FAILED";
|
|
3589
|
+
process.stdout.write(`${red("✗")} ${dim(controller.signal.aborted ? "cancelled" : code)}
|
|
3590
|
+
`);
|
|
3591
|
+
if (providerError !== null)
|
|
3592
|
+
process.stdout.write(`${dim(providerError)}
|
|
3593
|
+
`);
|
|
3594
|
+
process.stdout.write(`
|
|
3595
|
+
`);
|
|
3596
|
+
} finally {
|
|
3597
|
+
turn.controller = null;
|
|
3598
|
+
events.onAssistantText = undefined;
|
|
3599
|
+
events.onProviderError = undefined;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
} finally {
|
|
3603
|
+
editor.close();
|
|
3604
|
+
if (opened.status === "ready" && opened.close !== undefined)
|
|
3605
|
+
await opened.close().catch(() => {});
|
|
3606
|
+
sessions.close();
|
|
3607
|
+
}
|
|
3608
|
+
return 0;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/cli.ts
|
|
3612
|
+
var VERSION = "0.9.1";
|
|
3613
|
+
var USAGE = `xcb-compat: the TypeScript compatibility CLI for xcb, which routes coding tasks
|
|
3614
|
+
across the Claude, Codex, and Devin subscriptions you already pay for
|
|
3615
|
+
(the native Rust CLI installs separately as \`xcb\`)
|
|
3616
|
+
|
|
3617
|
+
Usage:
|
|
3618
|
+
xcb-compat [path] open the chat in a workspace (default: .)
|
|
3619
|
+
xcb-compat auth claude sign in with your Claude subscription
|
|
3620
|
+
xcb-compat auth codex sign in with your ChatGPT subscription
|
|
3621
|
+
xcb-compat auth devin sign in with your Devin account
|
|
3622
|
+
xcb-compat auth status show stored sign-in state
|
|
3623
|
+
xcb-compat auth logout [p] remove the stored credential (default: claude)
|
|
3624
|
+
xcb-compat doctor inspect provider binaries and admit this runtime
|
|
3625
|
+
xcb-compat sessions list local sessions
|
|
3626
|
+
xcb-compat sessions rm <id> remove one session and its transcript
|
|
3627
|
+
xcb-compat sessions prune remove sessions idle over 30 days (or N days)
|
|
3628
|
+
xcb-compat resume [id] continue a session (default: most recent)
|
|
3629
|
+
xcb-compat run [-p text] run one task headlessly (or pipe the task on stdin)
|
|
3630
|
+
xcb-compat judge [status] show judgment-provider (jev) state
|
|
3631
|
+
xcb-compat judge token store the jev API key read from stdin (piped, never echoed)
|
|
3632
|
+
xcb-compat judge logout remove the stored jev API key
|
|
3633
|
+
xcb-compat judge test ask the judge a bounded question batch (live call)
|
|
3634
|
+
xcb-compat migrate copy legacy AgentMixer state into ~/.xcb (keeps the original)
|
|
3635
|
+
xcb-compat --version
|
|
3636
|
+
|
|
3637
|
+
Options:
|
|
3638
|
+
--provider <claude|codex|devin|auto> pick the provider for run/chat/resume (default claude; resume uses session provider)
|
|
3639
|
+
--model <id> model for this run or session
|
|
3640
|
+
--cwd <path> workspace for run (default: current directory)
|
|
3641
|
+
|
|
3642
|
+
Environment:
|
|
3643
|
+
${CLI_CLAUDE_ENV} pin an exact claude executable path
|
|
3644
|
+
${CLI_CODEX_ENV} pin an exact codex executable path
|
|
3645
|
+
${CLI_DEVIN_ENV} pin an exact devin executable path
|
|
3646
|
+
${JUDGE_KEY_ENV} jev API key (also ${JUDGE_KEY_VENDOR_ENV}); overrides the vaulted key
|
|
3647
|
+
XCB_STATE override the state root (default ~/.xcb)
|
|
3648
|
+
`;
|
|
3649
|
+
function parseFlags(args) {
|
|
3650
|
+
const positional = [];
|
|
3651
|
+
let provider2 = "claude", model, prompt, cwd;
|
|
3652
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
3653
|
+
const arg = args[i];
|
|
3654
|
+
if (arg === "--provider" || arg === "--model" || arg === "-p" || arg === "--cwd") {
|
|
3655
|
+
const value = args[++i];
|
|
3656
|
+
if (value === undefined)
|
|
3657
|
+
fail8(`option ${arg} requires a value`);
|
|
3658
|
+
if (arg === "--provider") {
|
|
3659
|
+
if (value !== "claude" && value !== "codex" && value !== "devin" && value !== "auto") {
|
|
3660
|
+
fail8(`unknown provider ${value} — supported: claude, codex, devin, auto`);
|
|
3661
|
+
}
|
|
3662
|
+
provider2 = value;
|
|
3663
|
+
} else if (arg === "--model")
|
|
3664
|
+
model = boundedText(value, 160);
|
|
3665
|
+
else if (arg === "--cwd")
|
|
3666
|
+
cwd = value;
|
|
3667
|
+
else
|
|
3668
|
+
prompt = boundedText(value, 512 * 1024);
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3671
|
+
if (arg.startsWith("--"))
|
|
3672
|
+
fail8(`unknown option ${arg}`);
|
|
3673
|
+
positional.push(arg);
|
|
3674
|
+
}
|
|
3675
|
+
return { provider: provider2, providerExplicit: args.includes("--provider"), model, positional, prompt, cwd };
|
|
3676
|
+
}
|
|
3677
|
+
var fail8 = (message) => {
|
|
3678
|
+
process.stderr.write(`${red("xcb-compat:")} ${message}
|
|
3679
|
+
`);
|
|
3680
|
+
process.exit(2);
|
|
3681
|
+
};
|
|
3682
|
+
async function cliProfileFor(workspace) {
|
|
3683
|
+
const resolved = resolve3(workspace);
|
|
3684
|
+
let actual;
|
|
3685
|
+
try {
|
|
3686
|
+
actual = await import("node:fs/promises").then((fs) => fs.realpath(resolved));
|
|
3687
|
+
} catch {
|
|
3688
|
+
fail8(`workspace path does not exist: ${resolved}`);
|
|
3689
|
+
}
|
|
3690
|
+
const ws = createCliWorkspace(actual);
|
|
3691
|
+
const web = createPublicWeb();
|
|
3692
|
+
const profile = createCliWorkspaceProfile(ws, { fetch: (url, signal) => web.fetchPublic(url, 262144, signal).then((r) => ({ text: r.text })) });
|
|
3693
|
+
return { profile, workspace: ws };
|
|
3694
|
+
}
|
|
3695
|
+
var PROVIDER_PIN_ENV = { claude: CLI_CLAUDE_ENV, codex: CLI_CODEX_ENV, devin: CLI_DEVIN_ENV };
|
|
3696
|
+
async function commandDoctor(stateRoot) {
|
|
3697
|
+
const { profile } = await cliProfileFor(process.cwd());
|
|
3698
|
+
let admitted = 0;
|
|
3699
|
+
for (const provider2 of ["claude", "codex", "devin"]) {
|
|
3700
|
+
const admission = await admitCliProvider(stateRoot, provider2, profile);
|
|
3701
|
+
if (admission.inspection === null) {
|
|
3702
|
+
process.stdout.write(`${dim("○")} ${provider2}: not found (checked ${dim("$" + PROVIDER_PIN_ENV[provider2])}, PATH, known locations)
|
|
3703
|
+
`);
|
|
3704
|
+
continue;
|
|
3705
|
+
}
|
|
3706
|
+
const detail = admission.record !== null ? green(admission.detail) : yellow(admission.detail);
|
|
3707
|
+
process.stdout.write(`${admission.record !== null ? green("✓") : yellow("!")} ${provider2}: ${admission.inspection.version} ${dim(admission.inspection.sha256.slice(0, 16) + "…")} — ${detail}
|
|
3708
|
+
`);
|
|
3709
|
+
if (admission.record !== null)
|
|
3710
|
+
admitted += 1;
|
|
3711
|
+
}
|
|
3712
|
+
if (process.platform === "darwin") {
|
|
3713
|
+
const seatbelt = await seatbeltAvailable();
|
|
3714
|
+
process.stdout.write(`${seatbelt ? green("✓") : yellow("!")} sandbox: seatbelt ${seatbelt ? "available" : "unavailable"} ${dim("(claude runs confined; availability is not attestation)")}
|
|
3715
|
+
`);
|
|
3716
|
+
}
|
|
3717
|
+
const judgeKey = await resolveJudgeKey(stateRoot).catch(() => null);
|
|
3718
|
+
let judgeBlocked = false;
|
|
3719
|
+
if (judgeKey !== null) {
|
|
3720
|
+
try {
|
|
3721
|
+
checkJudgeKeyTarget(judgeKey.source, process.env[JUDGE_URL_ENV]);
|
|
3722
|
+
} catch {
|
|
3723
|
+
judgeBlocked = true;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
process.stdout.write(`${judgeKey === null ? dim("○") : judgeBlocked ? yellow("!") : green("✓")} judge: ${judgeKey === null ? `no jev key — ${dim(`--provider auto\` needs one; pipe it into \`xcb-compat judge token\``)}` : judgeBlocked ? "blocked — vaulted key cannot be used with a custom endpoint" : `key from ${judgeKey.source} — ${dim("`--provider auto` can route")}`}
|
|
3727
|
+
`);
|
|
3728
|
+
return admitted > 0 ? 0 : 1;
|
|
3729
|
+
}
|
|
3730
|
+
async function commandAuth(provider2, stateRoot) {
|
|
3731
|
+
if (provider2 === "codex") {
|
|
3732
|
+
const inspection2 = await inspectCliBinary("codex");
|
|
3733
|
+
if (inspection2 === null)
|
|
3734
|
+
return fail8("codex binary not found — install Codex CLI, then retry.");
|
|
3735
|
+
if (!inspection2.versionMatches)
|
|
3736
|
+
return fail8(`codex ${inspection2.version} found; this build admits only the pinned version — run \`xcb-compat doctor\`.`);
|
|
3737
|
+
const status2 = await codexAuthStatus(stateRoot, inspection2);
|
|
3738
|
+
if (!status2.admitted)
|
|
3739
|
+
return fail8("codex is not admitted — run `xcb-compat doctor` first.");
|
|
3740
|
+
if (status2.loggedIn) {
|
|
3741
|
+
process.stdout.write(`${green("✓")} codex: already signed in (${status2.planType ?? "ChatGPT"})
|
|
3742
|
+
`);
|
|
3743
|
+
return 0;
|
|
3744
|
+
}
|
|
3745
|
+
process.stdout.write(`${dim("Starting Codex device-code sign-in…")}
|
|
3746
|
+
`);
|
|
3747
|
+
const snapshot = await codexLogin(stateRoot, inspection2, (challenge) => {
|
|
3748
|
+
if (challenge.type === "chatgptDeviceCode") {
|
|
3749
|
+
process.stdout.write(`
|
|
3750
|
+
Open ${challenge.verificationUrl} and enter code: ${challenge.userCode}
|
|
3751
|
+
|
|
3752
|
+
`);
|
|
3753
|
+
} else {
|
|
3754
|
+
process.stdout.write(`
|
|
3755
|
+
Open ${challenge.authUrl} to sign in.
|
|
3756
|
+
|
|
3757
|
+
`);
|
|
3758
|
+
}
|
|
3759
|
+
});
|
|
3760
|
+
process.stdout.write(`${green("✓")} codex: signed in (${snapshot.planType ?? "ChatGPT"})
|
|
3761
|
+
`);
|
|
3762
|
+
return 0;
|
|
3763
|
+
}
|
|
3764
|
+
if (provider2 === "devin") {
|
|
3765
|
+
const inspection2 = await inspectCliBinary("devin");
|
|
3766
|
+
if (inspection2 === null)
|
|
3767
|
+
return fail8("devin binary not found — install the Devin CLI, then retry.");
|
|
3768
|
+
if (!inspection2.versionMatches)
|
|
3769
|
+
return fail8(`devin ${inspection2.version} found; this build requires devin >= ${CLI_DEVIN_MIN_VERSION} — run \`xcb-compat doctor\`.`);
|
|
3770
|
+
const existing = await devinAuthStatus(stateRoot, inspection2);
|
|
3771
|
+
if (existing.loggedIn) {
|
|
3772
|
+
process.stdout.write(`${green("✓")} devin: already signed in (${existing.planType ?? "Devin"})
|
|
3773
|
+
`);
|
|
3774
|
+
return 0;
|
|
3775
|
+
}
|
|
3776
|
+
process.stdout.write(`${dim("Opening Devin sign-in (credentials are stored under ~/.xcb only)…")}
|
|
3777
|
+
`);
|
|
3778
|
+
await devinLogin(stateRoot, inspection2);
|
|
3779
|
+
const status2 = await devinAuthStatus(stateRoot, inspection2);
|
|
3780
|
+
if (!status2.loggedIn)
|
|
3781
|
+
return fail8("sign-in did not produce credentials — re-run `xcb-compat auth devin`.");
|
|
3782
|
+
process.stdout.write(`${green("✓")} devin: signed in (${status2.planType ?? "Devin"})
|
|
3783
|
+
`);
|
|
3784
|
+
return 0;
|
|
3785
|
+
}
|
|
3786
|
+
if (provider2 !== "claude")
|
|
3787
|
+
return fail8(`unknown provider ${provider2} — supported: claude, codex, devin`);
|
|
3788
|
+
const inspection = await inspectCliBinary("claude");
|
|
3789
|
+
if (inspection === null)
|
|
3790
|
+
return fail8("claude binary not found — install Claude Code, then retry.");
|
|
3791
|
+
if (!inspection.versionMatches)
|
|
3792
|
+
return fail8(`claude ${inspection.version} found; this build admits only the pinned version — run \`xcb-compat doctor\`.`);
|
|
3793
|
+
process.stdout.write(`${dim("Opening Claude sign-in (credentials are stored under ~/.xcb only)…")}
|
|
3794
|
+
`);
|
|
3795
|
+
await claudeLogin(stateRoot, inspection);
|
|
3796
|
+
const status = await claudeAuthStatus(stateRoot);
|
|
3797
|
+
if (!status.loggedIn)
|
|
3798
|
+
return fail8("sign-in did not produce credentials — re-run `xcb-compat auth claude`.");
|
|
3799
|
+
process.stdout.write(`${green("✓")} signed in (${status.authMethod ?? "claude.ai"})
|
|
3800
|
+
`);
|
|
3801
|
+
return 0;
|
|
3802
|
+
}
|
|
3803
|
+
async function commandAuthStatus(stateRoot) {
|
|
3804
|
+
let any = false;
|
|
3805
|
+
const claude = await claudeAuthStatus(stateRoot);
|
|
3806
|
+
process.stdout.write(`claude: ${claude.loggedIn ? `signed in (${claude.authMethod ?? "claude.ai"})` : "signed out"}
|
|
3807
|
+
`);
|
|
3808
|
+
if (claude.loggedIn)
|
|
3809
|
+
any = true;
|
|
3810
|
+
const codexInspection = await inspectCliBinary("codex");
|
|
3811
|
+
if (codexInspection !== null) {
|
|
3812
|
+
try {
|
|
3813
|
+
const codex = await codexAuthStatus(stateRoot, codexInspection);
|
|
3814
|
+
process.stdout.write(`codex: ${codex.loggedIn ? `signed in (${codex.planType ?? "ChatGPT"})` : codex.admitted ? "signed out" : "not admitted"}
|
|
3815
|
+
`);
|
|
3816
|
+
if (codex.loggedIn)
|
|
3817
|
+
any = true;
|
|
3818
|
+
} catch {
|
|
3819
|
+
process.stdout.write(`codex: error checking status
|
|
3820
|
+
`);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
const devinInspection = await inspectCliBinary("devin");
|
|
3824
|
+
if (devinInspection !== null) {
|
|
3825
|
+
try {
|
|
3826
|
+
const devin = await devinAuthStatus(stateRoot, devinInspection);
|
|
3827
|
+
process.stdout.write(`devin: ${devin.loggedIn ? `signed in (${devin.planType ?? "Devin"})` : "signed out"}
|
|
3828
|
+
`);
|
|
3829
|
+
if (devin.loggedIn)
|
|
3830
|
+
any = true;
|
|
3831
|
+
} catch {
|
|
3832
|
+
process.stdout.write(`devin: error checking status
|
|
3833
|
+
`);
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return any ? 0 : 1;
|
|
3837
|
+
}
|
|
3838
|
+
async function commandAuthLogout(provider2, stateRoot) {
|
|
3839
|
+
if (provider2 === "codex") {
|
|
3840
|
+
const inspection = await inspectCliBinary("codex");
|
|
3841
|
+
if (inspection === null)
|
|
3842
|
+
return fail8("codex binary not found.");
|
|
3843
|
+
await codexLogout(stateRoot, inspection);
|
|
3844
|
+
process.stdout.write(`codex: signed out
|
|
3845
|
+
`);
|
|
3846
|
+
return 0;
|
|
3847
|
+
}
|
|
3848
|
+
if (provider2 === "devin") {
|
|
3849
|
+
const inspection = await inspectCliBinary("devin");
|
|
3850
|
+
if (inspection === null)
|
|
3851
|
+
return fail8("devin binary not found.");
|
|
3852
|
+
await devinLogout(stateRoot, inspection);
|
|
3853
|
+
process.stdout.write(`devin: signed out
|
|
3854
|
+
`);
|
|
3855
|
+
return 0;
|
|
3856
|
+
}
|
|
3857
|
+
if (provider2 !== undefined && provider2 !== "claude")
|
|
3858
|
+
return fail8(`unknown provider ${provider2} — supported: claude, codex, devin`);
|
|
3859
|
+
await clearClaudeOAuthToken(stateRoot);
|
|
3860
|
+
process.stdout.write(`claude: signed out
|
|
3861
|
+
`);
|
|
3862
|
+
return 0;
|
|
3863
|
+
}
|
|
3864
|
+
async function commandJudge(sub, stateRoot) {
|
|
3865
|
+
if (sub === "token") {
|
|
3866
|
+
if (process.stdin.isTTY)
|
|
3867
|
+
fail8("usage: pipe the jev API key on stdin — e.g. `pbpaste | xcb-compat judge token` (never a command argument).");
|
|
3868
|
+
const key2 = (await readStdin()).trim();
|
|
3869
|
+
if (key2 === "")
|
|
3870
|
+
fail8("empty input — pipe the jev API key on stdin.");
|
|
3871
|
+
await storeJudgeKey(stateRoot, key2).catch((error) => {
|
|
3872
|
+
if (error instanceof Error && error.message === "JUDGE_KEY_EXISTS") {
|
|
3873
|
+
fail8("a jev key is already stored — run `xcb-compat judge logout` first to replace it.");
|
|
3874
|
+
}
|
|
3875
|
+
if (error instanceof Error && error.message === "JUDGE_KEY_INVALID") {
|
|
3876
|
+
fail8("that does not look like a jev API key — check the copied value.");
|
|
3877
|
+
}
|
|
3878
|
+
throw error;
|
|
3879
|
+
});
|
|
3880
|
+
process.stdout.write(`${green("✓")} judge key stored in ${join14(stateRoot, JUDGE_TOKEN_FILE)} (mode 0600)
|
|
3881
|
+
`);
|
|
3882
|
+
return 0;
|
|
3883
|
+
}
|
|
3884
|
+
if (sub === "logout") {
|
|
3885
|
+
const removed = await removeJudgeKey(stateRoot);
|
|
3886
|
+
process.stdout.write(removed ? `judge key removed
|
|
3887
|
+
` : `no stored judge key
|
|
3888
|
+
`);
|
|
3889
|
+
return 0;
|
|
3890
|
+
}
|
|
3891
|
+
if (sub === "test") {
|
|
3892
|
+
const judge = await resolveJudge({ stateRoot, enabled: true });
|
|
3893
|
+
if (judge === null) {
|
|
3894
|
+
return fail8(`no jev API key — pipe one into \`xcb-compat judge token\` or set ${JUDGE_KEY_ENV}.`);
|
|
3895
|
+
}
|
|
3896
|
+
const started = Date.now();
|
|
3897
|
+
const answers = await judge.ask("xcb-compat judge connectivity probe", {
|
|
3898
|
+
probe: { type: "noul", instructions: "Is the sky blue on a clear day?" },
|
|
3899
|
+
pick: { type: "choice", instructions: "Which option names a color?", criteria: { red: "a color", spoon: "not a color" } },
|
|
3900
|
+
rate: { type: "score", instructions: "How true is the claim that water is wet? Rate on the ordered criteria scale.", criteria: ["false", "partly true", "true"] }
|
|
3901
|
+
});
|
|
3902
|
+
const probe = answers.answers.probe, pick = answers.answers.pick, rate = answers.answers.rate;
|
|
3903
|
+
if (probe?.type !== "noul" || pick?.type !== "choice" || rate?.type !== "score") {
|
|
3904
|
+
return fail8("judge returned a malformed answer shape.");
|
|
3905
|
+
}
|
|
3906
|
+
process.stdout.write(`${green("✓")} judge answered in ${Date.now() - started}ms ${dim(`(model ${answers.model ?? "unknown"}, p=${probe.noul.toFixed(3)}, choice=${pick.choice}@${pick.confidence.toFixed(3)}, score=${rate.score.toFixed(3)}@${rate.confidence.toFixed(3)})`)}
|
|
3907
|
+
`);
|
|
3908
|
+
return 0;
|
|
3909
|
+
}
|
|
3910
|
+
if (sub !== undefined && sub !== "status")
|
|
3911
|
+
return fail8("usage: xcb-compat judge [status|token|logout|test]");
|
|
3912
|
+
const key = await resolveJudgeKey(stateRoot).catch(() => null);
|
|
3913
|
+
if (key !== null) {
|
|
3914
|
+
try {
|
|
3915
|
+
checkJudgeKeyTarget(key.source, process.env[JUDGE_URL_ENV]);
|
|
3916
|
+
} catch {
|
|
3917
|
+
return fail8("vaulted judge key is bound to the canonical System One endpoint; use an environment key for a custom endpoint.");
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
const vaulted = await hasJudgeKey(stateRoot);
|
|
3921
|
+
const source = key === null ? "none" : key.source === "env" ? "environment" : "vault";
|
|
3922
|
+
process.stdout.write(`judge: ${key === null ? "no key configured" : `key from ${source}`}${vaulted && key?.source !== "vault" ? dim(" (vault file also present)") : ""}
|
|
3923
|
+
`);
|
|
3924
|
+
process.stdout.write(`${dim(" env: ")}${JUDGE_KEY_ENV} or ${JUDGE_KEY_VENDOR_ENV}${dim(" vault: ")}${join14(stateRoot, JUDGE_TOKEN_FILE)}
|
|
3925
|
+
`);
|
|
3926
|
+
process.stdout.write(`${dim(" use --provider auto on `xcb-compat run` to route a task through the judge")}
|
|
3927
|
+
`);
|
|
3928
|
+
return key === null ? 1 : 0;
|
|
3929
|
+
}
|
|
3930
|
+
async function latestSessionId(stateRoot) {
|
|
3931
|
+
const sessions = await CliSessionStore.open(join14(stateRoot, "sessions"));
|
|
3932
|
+
try {
|
|
3933
|
+
return sessions.list(1)[0]?.id;
|
|
3934
|
+
} finally {
|
|
3935
|
+
sessions.close();
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
async function commandSessions(stateRoot, rest) {
|
|
3939
|
+
const [sub, ...args] = rest;
|
|
3940
|
+
const sessions = await CliSessionStore.open(join14(stateRoot, "sessions"));
|
|
3941
|
+
try {
|
|
3942
|
+
if (sub === undefined) {
|
|
3943
|
+
for (const session of sessions.list(64)) {
|
|
3944
|
+
process.stdout.write(`${session.id} ${dim(session.provider)} ${dim(session.model)} ${session.title || "(untitled)"} ${dim(new Date(session.lastActiveAt).toISOString())}
|
|
3945
|
+
`);
|
|
3946
|
+
}
|
|
3947
|
+
return 0;
|
|
3948
|
+
}
|
|
3949
|
+
if (sub === "rm") {
|
|
3950
|
+
if (args.length !== 1)
|
|
3951
|
+
return fail8("usage: xcb-compat sessions rm <id>");
|
|
3952
|
+
if (!await sessions.remove(args[0]))
|
|
3953
|
+
return fail8(`session not found: ${args[0]}`);
|
|
3954
|
+
process.stdout.write(`removed ${args[0]}
|
|
3955
|
+
`);
|
|
3956
|
+
return 0;
|
|
3957
|
+
}
|
|
3958
|
+
if (sub === "prune") {
|
|
3959
|
+
if (args.length > 1)
|
|
3960
|
+
return fail8("usage: xcb-compat sessions prune [days]");
|
|
3961
|
+
const days = args[0] === undefined ? 30 : Number(args[0]);
|
|
3962
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650)
|
|
3963
|
+
return fail8(`invalid days: ${args[0]}`);
|
|
3964
|
+
const removed = await sessions.prune(Date.now() - days * 86400000);
|
|
3965
|
+
process.stdout.write(`pruned ${removed} session${removed === 1 ? "" : "s"} idle over ${days} day${days === 1 ? "" : "s"}
|
|
3966
|
+
`);
|
|
3967
|
+
return 0;
|
|
3968
|
+
}
|
|
3969
|
+
return fail8(`unknown sessions subcommand ${sub} — supported: rm, prune`);
|
|
3970
|
+
} finally {
|
|
3971
|
+
sessions.close();
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
async function autoProviderCandidates(stateRoot) {
|
|
3975
|
+
const candidates = [];
|
|
3976
|
+
for (const provider2 of ["claude", "codex"]) {
|
|
3977
|
+
if (provider2 === "codex" && cliCodexHostDiagnostic() !== null)
|
|
3978
|
+
continue;
|
|
3979
|
+
const inspection = await inspectCliBinary(provider2);
|
|
3980
|
+
if (inspection === null || !inspection.versionMatches)
|
|
3981
|
+
continue;
|
|
3982
|
+
const record2 = await readCliQualification(stateRoot, provider2);
|
|
3983
|
+
if (record2 === null || record2.executableSha256 !== inspection.sha256 || record2.executablePath !== inspection.executablePath)
|
|
3984
|
+
continue;
|
|
3985
|
+
const authed = provider2 === "claude" ? (await claudeAuthStatus(stateRoot)).loggedIn : (await codexAuthStatus(stateRoot, inspection)).loggedIn;
|
|
3986
|
+
if (authed)
|
|
3987
|
+
candidates.push({ provider: provider2, label: `${provider2} ${inspection.version}` });
|
|
3988
|
+
}
|
|
3989
|
+
return candidates;
|
|
3990
|
+
}
|
|
3991
|
+
async function routeAutoProvider(stateRoot, prompt) {
|
|
3992
|
+
const candidates = await autoProviderCandidates(stateRoot);
|
|
3993
|
+
if (candidates.length === 0)
|
|
3994
|
+
fail8("no admitted, signed-in providers — run `xcb-compat doctor` and `xcb-compat auth <provider>` first.");
|
|
3995
|
+
if (candidates.length === 1)
|
|
3996
|
+
return candidates[0].provider;
|
|
3997
|
+
const judge = await resolveJudge({ stateRoot, enabled: true });
|
|
3998
|
+
if (judge === null) {
|
|
3999
|
+
return fail8(`--provider auto needs a jev API key — pipe one into \`xcb-compat judge token\` or set ${JUDGE_KEY_ENV}.`);
|
|
4000
|
+
}
|
|
4001
|
+
const criteria = {};
|
|
4002
|
+
candidates.forEach((candidate, index) => {
|
|
4003
|
+
criteria[`route_${index}`] = candidate.label;
|
|
4004
|
+
});
|
|
4005
|
+
let answers;
|
|
4006
|
+
try {
|
|
4007
|
+
answers = await judge.ask({ task: boundedText(prompt, 32768) }, {
|
|
4008
|
+
route: {
|
|
4009
|
+
type: "choice",
|
|
4010
|
+
instructions: "Choose the best coding-agent provider for this task. Consider reliability for the task's apparent complexity. Return exactly one option key.",
|
|
4011
|
+
criteria
|
|
4012
|
+
}
|
|
4013
|
+
});
|
|
4014
|
+
} catch (error) {
|
|
4015
|
+
return fail8(`judge routing failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
4016
|
+
}
|
|
4017
|
+
const answer = answers.answers.route;
|
|
4018
|
+
if (answer === undefined || answer.type !== "choice" || !(answer.choice in criteria)) {
|
|
4019
|
+
return fail8("judge routing failed: the judge returned no usable choice — pick a provider explicitly.");
|
|
4020
|
+
}
|
|
4021
|
+
const winner = candidates[Number(answer.choice.slice("route_".length))];
|
|
4022
|
+
process.stderr.write(`${dim(`judge routed to ${winner.provider} (${winner.label})`)}
|
|
4023
|
+
`);
|
|
4024
|
+
return winner.provider;
|
|
4025
|
+
}
|
|
4026
|
+
async function commandRun(prompt, workspace, stateRoot, provider2, model) {
|
|
4027
|
+
const prepared = await cliProfileFor(workspace);
|
|
4028
|
+
assertWorkspaceStateSeparation(prepared.workspace.root, stateRoot);
|
|
4029
|
+
const { profile, workspace: ws } = prepared;
|
|
4030
|
+
if (provider2 === "auto")
|
|
4031
|
+
provider2 = await routeAutoProvider(stateRoot, prompt);
|
|
4032
|
+
const events = {};
|
|
4033
|
+
const opened = await openCliProvider(stateRoot, provider2, profile, events, { workspaceRoot: ws.root });
|
|
4034
|
+
if (opened.status !== "ready")
|
|
4035
|
+
return fail8(opened.detail ?? `provider not admitted — run \`xcb-compat doctor\` and \`xcb-compat auth ${provider2}\` first.`);
|
|
4036
|
+
if (provider2 === "claude") {
|
|
4037
|
+
const auth = await claudeAuthStatus(stateRoot);
|
|
4038
|
+
if (!auth.loggedIn)
|
|
4039
|
+
return fail8("not signed in — run `xcb-compat auth claude` first.");
|
|
4040
|
+
}
|
|
4041
|
+
if (provider2 === "codex") {
|
|
4042
|
+
const inspection = await inspectCliBinary("codex");
|
|
4043
|
+
if (inspection !== null) {
|
|
4044
|
+
const codex = await codexAuthStatus(stateRoot, inspection);
|
|
4045
|
+
if (!codex.loggedIn)
|
|
4046
|
+
return fail8("not signed in — run `xcb-compat auth codex` first.");
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
if (provider2 === "devin") {
|
|
4050
|
+
const inspection = await inspectCliBinary("devin");
|
|
4051
|
+
if (inspection !== null) {
|
|
4052
|
+
const devin = await devinAuthStatus(stateRoot, inspection);
|
|
4053
|
+
if (!devin.loggedIn)
|
|
4054
|
+
return fail8("not signed in — run `xcb-compat auth devin` first.");
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
const leases = new SqliteAccountLeases(await openAccountDatabase(join14(stateRoot, "account-leases.sqlite")));
|
|
4058
|
+
const sessions = await CliSessionStore.open(join14(stateRoot, "sessions"));
|
|
4059
|
+
const runModel = model ?? (provider2 === "claude" ? CLI_CLAUDE_DEFAULT_MODEL : provider2 === "devin" ? CLI_DEVIN_DEFAULT_MODEL : CLI_CODEX_DEFAULT_MODEL);
|
|
4060
|
+
let streamedAll = "", streamedLast = "", providerError = null;
|
|
4061
|
+
events.onAssistantText = (block) => {
|
|
4062
|
+
if (!process.stdout.isTTY)
|
|
4063
|
+
return;
|
|
4064
|
+
streamedAll += (streamedAll === "" ? "" : `
|
|
4065
|
+
`) + block;
|
|
4066
|
+
streamedLast = block;
|
|
4067
|
+
process.stdout.write(`${block}
|
|
4068
|
+
`);
|
|
4069
|
+
};
|
|
4070
|
+
events.onProviderError = (text) => {
|
|
4071
|
+
providerError = text;
|
|
4072
|
+
};
|
|
4073
|
+
try {
|
|
4074
|
+
const session = await sessions.create({ provider: provider2, accountId: "local", workspace: resolve3(workspace), model: runModel, now: Date.now() });
|
|
4075
|
+
const { result, output } = await runCliTurn({
|
|
4076
|
+
adapter: opened.adapter,
|
|
4077
|
+
leases,
|
|
4078
|
+
profile,
|
|
4079
|
+
accountId: "local",
|
|
4080
|
+
workspaceId: session.id,
|
|
4081
|
+
model: runModel,
|
|
4082
|
+
prompt,
|
|
4083
|
+
prior: [],
|
|
4084
|
+
signal: AbortSignal.timeout(600000),
|
|
4085
|
+
onTool: (name) => process.stderr.write(dim(` ⚙ ${name}
|
|
4086
|
+
`))
|
|
4087
|
+
});
|
|
4088
|
+
printRemainingText(output, streamedAll, streamedLast);
|
|
4089
|
+
if (result.outcome.status !== "completed") {
|
|
4090
|
+
process.stderr.write(`${red("xcb-compat:")} ${result.outcome.code ?? "run failed"}${providerError === null ? "" : ` — ${providerError}`}
|
|
4091
|
+
`);
|
|
4092
|
+
return 1;
|
|
4093
|
+
}
|
|
4094
|
+
await sessions.record(session, [
|
|
4095
|
+
{ role: "user", text: prompt, at: Date.now() },
|
|
4096
|
+
...output === null ? [] : [{ role: "assistant", text: output, at: Date.now() }]
|
|
4097
|
+
], Date.now());
|
|
4098
|
+
return 0;
|
|
4099
|
+
} finally {
|
|
4100
|
+
sessions.close();
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
async function readStdin() {
|
|
4104
|
+
const chunks = [];
|
|
4105
|
+
for await (const chunk of process.stdin)
|
|
4106
|
+
chunks.push(chunk);
|
|
4107
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
4108
|
+
}
|
|
4109
|
+
async function main(argv) {
|
|
4110
|
+
const [command, ...rest] = argv;
|
|
4111
|
+
if (command === "--help" || command === "help" || command === "-h") {
|
|
4112
|
+
process.stdout.write(USAGE);
|
|
4113
|
+
return 0;
|
|
4114
|
+
}
|
|
4115
|
+
if (command === "--version" || command === "-v") {
|
|
4116
|
+
process.stdout.write(`${VERSION}
|
|
4117
|
+
`);
|
|
4118
|
+
return 0;
|
|
4119
|
+
}
|
|
4120
|
+
if (typeof Bun === "undefined" && (Number(process.versions.node?.split(".")[0]) || 0) < 24) {
|
|
4121
|
+
return fail8("requires node ≥ 24 or bun ≥ 1.3.14");
|
|
4122
|
+
}
|
|
4123
|
+
const { path: stateRoot } = await ensureCliState();
|
|
4124
|
+
if (command === "migrate") {
|
|
4125
|
+
if (rest.length !== 0)
|
|
4126
|
+
return fail8("usage: xcb-compat migrate");
|
|
4127
|
+
const migrated = await migrateLegacyState(stateRoot);
|
|
4128
|
+
process.stdout.write(`${green("✓")} migrated ${migrated.entries} entries from ${migrated.source}
|
|
4129
|
+
`);
|
|
4130
|
+
process.stdout.write(`${dim("the legacy directory was left untouched — remove it yourself when ready")}
|
|
4131
|
+
`);
|
|
4132
|
+
return 0;
|
|
4133
|
+
}
|
|
4134
|
+
if (command === "doctor")
|
|
4135
|
+
return await commandDoctor(stateRoot);
|
|
4136
|
+
if (command === "auth") {
|
|
4137
|
+
const sub = rest[0];
|
|
4138
|
+
if (sub === undefined || sub === "status")
|
|
4139
|
+
return await commandAuthStatus(stateRoot);
|
|
4140
|
+
if (sub === "logout")
|
|
4141
|
+
return await commandAuthLogout(rest[1], stateRoot);
|
|
4142
|
+
if (rest.length > 1)
|
|
4143
|
+
return fail8("usage: xcb-compat auth <claude|codex|status|logout [provider]>");
|
|
4144
|
+
return await commandAuth(sub, stateRoot);
|
|
4145
|
+
}
|
|
4146
|
+
if (command === "judge") {
|
|
4147
|
+
if (rest.length > 1)
|
|
4148
|
+
return fail8("usage: xcb-compat judge [status|token|logout|test]");
|
|
4149
|
+
return await commandJudge(rest[0], stateRoot);
|
|
4150
|
+
}
|
|
4151
|
+
if (command === "sessions")
|
|
4152
|
+
return await commandSessions(stateRoot, rest);
|
|
4153
|
+
if (command === "run") {
|
|
4154
|
+
const flags2 = parseFlags(rest);
|
|
4155
|
+
let prompt = flags2.prompt ?? (flags2.positional.length ? flags2.positional.join(" ") : undefined);
|
|
4156
|
+
if (prompt === undefined || prompt.trim() === "") {
|
|
4157
|
+
if (process.stdin.isTTY)
|
|
4158
|
+
fail8("usage: xcb-compat run -p <task> (or pipe the task on stdin)");
|
|
4159
|
+
prompt = await readStdin();
|
|
4160
|
+
}
|
|
4161
|
+
return await commandRun(boundedText(prompt, 524288), flags2.cwd ?? process.cwd(), stateRoot, flags2.provider, flags2.model);
|
|
4162
|
+
}
|
|
4163
|
+
if (command === "resume") {
|
|
4164
|
+
const flags2 = parseFlags(rest);
|
|
4165
|
+
const provider3 = flags2.provider === "auto" ? fail8("--provider auto routes a known task — use it with `xcb-compat run`; a chat session binds one provider up front.") : flags2.provider;
|
|
4166
|
+
if (flags2.positional.length > 1)
|
|
4167
|
+
return fail8("usage: xcb-compat resume [session-id]");
|
|
4168
|
+
const id = flags2.positional[0] ?? await latestSessionId(stateRoot);
|
|
4169
|
+
if (id === undefined)
|
|
4170
|
+
return fail8("no sessions yet — start one with `xcb-compat`");
|
|
4171
|
+
return await runCliChat({ workspace: process.cwd(), sessionId: id, ...flags2.providerExplicit ? { provider: provider3 } : {}, ...flags2.model === undefined ? {} : { model: flags2.model } });
|
|
4172
|
+
}
|
|
4173
|
+
const chatArgv = command === "chat" ? rest : argv;
|
|
4174
|
+
const flags = parseFlags(chatArgv);
|
|
4175
|
+
const provider2 = flags.provider === "auto" ? fail8("--provider auto routes a known task — use it with `xcb-compat run`; a chat session binds one provider up front.") : flags.provider;
|
|
4176
|
+
if (flags.prompt !== undefined)
|
|
4177
|
+
return fail8("-p is only valid with `xcb-compat run`");
|
|
4178
|
+
const workspace = flags.positional[0] ?? ".";
|
|
4179
|
+
if (flags.positional.length > 1)
|
|
4180
|
+
return fail8("usage: xcb-compat [path]");
|
|
4181
|
+
return await runCliChat({ workspace, provider: provider2, ...flags.model === undefined ? {} : { model: flags.model } });
|
|
4182
|
+
}
|
|
4183
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
4184
|
+
process.stderr.write(`${red("xcb-compat:")} ${error instanceof Error ? error.message : "unexpected failure"}
|
|
4185
|
+
`);
|
|
4186
|
+
process.exit(1);
|
|
4187
|
+
});
|
|
4188
|
+
export {
|
|
4189
|
+
main
|
|
4190
|
+
};
|