@tiangong-ai/cli 0.0.19 → 0.0.21

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.
Files changed (58) hide show
  1. package/AGENTS.md +8 -2
  2. package/README.md +209 -4
  3. package/dist/cli.js +2 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/research/commands.js +6 -0
  6. package/dist/research/commands.js.map +1 -1
  7. package/dist/research/orchestration.d.ts +3 -0
  8. package/dist/research/orchestration.js +391 -0
  9. package/dist/research/orchestration.js.map +1 -0
  10. package/dist/research/workspace/broker.d.ts +5 -0
  11. package/dist/research/workspace/broker.js +729 -0
  12. package/dist/research/workspace/broker.js.map +1 -0
  13. package/dist/research/workspace/capabilities.d.ts +10 -0
  14. package/dist/research/workspace/capabilities.js +356 -0
  15. package/dist/research/workspace/capabilities.js.map +1 -0
  16. package/dist/research/workspace/constants.d.ts +8 -0
  17. package/dist/research/workspace/constants.js +41 -0
  18. package/dist/research/workspace/constants.js.map +1 -0
  19. package/dist/research/workspace/context.d.ts +3 -0
  20. package/dist/research/workspace/context.js +77 -0
  21. package/dist/research/workspace/context.js.map +1 -0
  22. package/dist/research/workspace/evidence.d.ts +32 -0
  23. package/dist/research/workspace/evidence.js +235 -0
  24. package/dist/research/workspace/evidence.js.map +1 -0
  25. package/dist/research/workspace/executor.d.ts +22 -0
  26. package/dist/research/workspace/executor.js +926 -0
  27. package/dist/research/workspace/executor.js.map +1 -0
  28. package/dist/research/workspace/input-plan.d.ts +5 -0
  29. package/dist/research/workspace/input-plan.js +319 -0
  30. package/dist/research/workspace/input-plan.js.map +1 -0
  31. package/dist/research/workspace/journal.d.ts +7 -0
  32. package/dist/research/workspace/journal.js +105 -0
  33. package/dist/research/workspace/journal.js.map +1 -0
  34. package/dist/research/workspace/preflight.d.ts +108 -0
  35. package/dist/research/workspace/preflight.js +261 -0
  36. package/dist/research/workspace/preflight.js.map +1 -0
  37. package/dist/research/workspace/projects.d.ts +12 -0
  38. package/dist/research/workspace/projects.js +514 -0
  39. package/dist/research/workspace/projects.js.map +1 -0
  40. package/dist/research/workspace/runtime.d.ts +31 -0
  41. package/dist/research/workspace/runtime.js +1637 -0
  42. package/dist/research/workspace/runtime.js.map +1 -0
  43. package/dist/research/workspace/sanitization.d.ts +5 -0
  44. package/dist/research/workspace/sanitization.js +72 -0
  45. package/dist/research/workspace/sanitization.js.map +1 -0
  46. package/dist/research/workspace/schemas.d.ts +17 -0
  47. package/dist/research/workspace/schemas.js +342 -0
  48. package/dist/research/workspace/schemas.js.map +1 -0
  49. package/dist/research/workspace/storage.d.ts +25 -0
  50. package/dist/research/workspace/storage.js +222 -0
  51. package/dist/research/workspace/storage.js.map +1 -0
  52. package/dist/research/workspace/types.d.ts +341 -0
  53. package/dist/research/workspace/types.js +2 -0
  54. package/dist/research/workspace/types.js.map +1 -0
  55. package/dist/research/workspace/workspace.d.ts +23 -0
  56. package/dist/research/workspace/workspace.js +702 -0
  57. package/dist/research/workspace/workspace.js.map +1 -0
  58. package/package.json +4 -2
@@ -0,0 +1,926 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { access, chmod, copyFile, lstat, mkdir, readFile, realpath, writeFile, } from "node:fs/promises";
4
+ import { arch, homedir, platform } from "node:os";
5
+ import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { CliError } from "../../errors.js";
8
+ import { configuredResearchSecrets, isSensitiveEnvironmentName, sanitizeResearchText, } from "./sanitization.js";
9
+ import { isObject, sha256File } from "./storage.js";
10
+ const MAX_CAPTURE_BYTES = 5 * 1024 * 1024;
11
+ const MIN_CAPTURE_BYTES = 64 * 1024;
12
+ const BYTES_PER_OUTPUT_TOKEN = 16;
13
+ const EXECUTOR_WRAPPER_PATH = fileURLToPath(import.meta.url);
14
+ const WRAPPER_TARGET_ENV = "TIANGONG_RESEARCH_AGENT_BINARY";
15
+ const CODEX_DISABLED_FEATURES = [
16
+ "apps",
17
+ "auth_elicitation",
18
+ "browser_use",
19
+ "computer_use",
20
+ "goals",
21
+ "guardian_approval",
22
+ "hooks",
23
+ "image_generation",
24
+ "in_app_browser",
25
+ "mentions_v2",
26
+ "multi_agent",
27
+ "personality",
28
+ "plugins",
29
+ "remote_plugin",
30
+ "skill_mcp_dependency_install",
31
+ "skill_search",
32
+ "tool_call_mcp_elicitation",
33
+ "tool_suggest",
34
+ "workspace_dependencies",
35
+ ];
36
+ const ROUTE_AUTH_ENVIRONMENT = {
37
+ codex: ["OPENAI_API_KEY"],
38
+ claude: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"],
39
+ };
40
+ const CLAUDE_SETTINGS_ENVIRONMENT = [
41
+ "ANTHROPIC_API_KEY",
42
+ "ANTHROPIC_AUTH_TOKEN",
43
+ "CLAUDE_CODE_OAUTH_TOKEN",
44
+ "ANTHROPIC_BASE_URL",
45
+ ];
46
+ const DEFAULT_AGENT_EFFORT = "low";
47
+ const DEFAULT_CODEX_VERBOSITY = "low";
48
+ export async function executeAgent(request) {
49
+ validateAgentBinary(request.route);
50
+ if (!Number.isInteger(request.maxOutputTokens) || request.maxOutputTokens < 1) {
51
+ throw new CliError("Agent max output tokens must be a positive integer.", {
52
+ code: "RESEARCH_EXECUTOR_INVALID",
53
+ exitCode: 2,
54
+ });
55
+ }
56
+ if (!Number.isInteger(request.maxTurns) || request.maxTurns < 1) {
57
+ throw new CliError("Agent max turns must be a positive integer.", {
58
+ code: "RESEARCH_EXECUTOR_INVALID",
59
+ exitCode: 2,
60
+ });
61
+ }
62
+ const temporaryDirectory = join(request.capsuleRoot, "tmp");
63
+ await mkdir(temporaryDirectory, { recursive: true, mode: 0o700 });
64
+ const executables = await resolveAgentExecutables(request.route, request.environment.PATH);
65
+ const runtime = await fingerprintResolvedBinary(request.route, executables.launcher, executables.target, request.environment.PATH);
66
+ if (request.expectedRuntime && !sameRuntimeFingerprint(runtime, request.expectedRuntime)) {
67
+ throw new CliError(`Configured ${request.route.agent} runtime drifted after the successful doctor smoke.`, {
68
+ code: "RESEARCH_EXECUTOR_DRIFT",
69
+ exitCode: 3,
70
+ details: {
71
+ agent: request.route.agent,
72
+ expectedBinarySha256: request.expectedRuntime.binarySha256,
73
+ actualBinarySha256: runtime.binarySha256,
74
+ expectedWrapperSha256: request.expectedRuntime.wrapperSha256,
75
+ actualWrapperSha256: runtime.wrapperSha256,
76
+ expectedAdapterSha256: request.expectedRuntime.adapterSha256,
77
+ actualAdapterSha256: runtime.adapterSha256,
78
+ },
79
+ });
80
+ }
81
+ const capsuleHome = join(request.capsuleRoot, "home");
82
+ const capsuleAuth = await prepareCapsuleHome(request.route, capsuleHome, request.environment);
83
+ const secrets = [...configuredResearchSecrets(request.environment), ...capsuleAuth.secrets];
84
+ const invocation = await buildInvocation(request, executables.launcher, join(request.capsuleRoot, `${request.purpose}-output-schema.json`));
85
+ const sandboxed = await sandboxInvocation(invocation.binary, invocation.args, request.capsuleRoot, request.projectRoot, request.workspaceRoot, executables.target);
86
+ const started = process.hrtime.bigint();
87
+ let completed;
88
+ try {
89
+ completed = await spawnCaptured({
90
+ binary: sandboxed.binary,
91
+ args: sandboxed.args,
92
+ cwd: request.projectRoot,
93
+ env: sanitizedEnvironment(request.environment, temporaryDirectory, capsuleHome, request.route, executables.target, capsuleAuth.environment),
94
+ timeoutMs: request.timeoutSeconds * 1000,
95
+ maxCaptureBytes: Math.min(MAX_CAPTURE_BYTES, Math.max(MIN_CAPTURE_BYTES, request.maxOutputTokens * BYTES_PER_OUTPUT_TOKEN)),
96
+ });
97
+ }
98
+ catch (error) {
99
+ if (error.code === "ENOENT") {
100
+ throw new CliError(`Configured ${request.route.agent} executable is unavailable.`, {
101
+ code: "RESEARCH_EXECUTOR_UNAVAILABLE",
102
+ exitCode: 3,
103
+ });
104
+ }
105
+ throw error;
106
+ }
107
+ const wallSeconds = Number(process.hrtime.bigint() - started) / 1_000_000_000;
108
+ const parsed = parseAgentResult(request.route, completed.stdout, completed.stderr);
109
+ const redacted = redactResult(parsed.stdout, parsed.stderr, secrets);
110
+ const exitCode = redacted.exposed
111
+ ? 86
112
+ : completed.captureExceeded
113
+ ? 75
114
+ : completed.exitCode !== 0
115
+ ? completed.exitCode
116
+ : parsed.parseFailed
117
+ ? 3
118
+ : 0;
119
+ const inputTokens = redacted.exposed ? 0 : parsed.inputTokens;
120
+ const cachedInputTokens = redacted.exposed ? 0 : parsed.cachedInputTokens;
121
+ const outputTokens = redacted.exposed ? 0 : parsed.outputTokens;
122
+ const tokens = inputTokens + cachedInputTokens + outputTokens;
123
+ const providerCost = redacted.exposed ? 0 : parsed.costUsd;
124
+ const costUsd = providerCost > 0 ? providerCost : estimateCost(request.route, parsed);
125
+ return {
126
+ exitCode,
127
+ stdout: redacted.stdout,
128
+ stderr: completed.captureExceeded
129
+ ? `${redacted.stderr}\nagent output exceeded the configured capture limit`.trim()
130
+ : redacted.stderr,
131
+ tokens,
132
+ inputTokens,
133
+ cachedInputTokens,
134
+ outputTokens,
135
+ costUsd,
136
+ wallSeconds,
137
+ model: parsed.model ?? request.route.model,
138
+ runtime: { ...runtime, model: parsed.model ?? request.route.model },
139
+ telemetry: sanitizeExecutionTelemetry(parsed.telemetry, secrets),
140
+ };
141
+ }
142
+ export async function fingerprintAgentRoute(route, environment) {
143
+ validateAgentBinary(route);
144
+ const executables = await resolveAgentExecutables(route, environment.PATH);
145
+ return fingerprintResolvedBinary(route, executables.launcher, executables.target, environment.PATH);
146
+ }
147
+ async function buildInvocation(request, resolvedBinary, outputSchemaPath) {
148
+ await writeFile(outputSchemaPath, `${JSON.stringify(request.outputSchema, null, 2)}\n`, {
149
+ encoding: "utf8",
150
+ mode: 0o600,
151
+ });
152
+ if (request.route.agent === "codex") {
153
+ const toolPolicy = request.toolPolicy ?? "workspace-read";
154
+ const args = [
155
+ "exec",
156
+ "--ignore-user-config",
157
+ "--ignore-rules",
158
+ "--strict-config",
159
+ "--skip-git-repo-check",
160
+ "--ephemeral",
161
+ "--color",
162
+ "never",
163
+ "--sandbox",
164
+ "read-only",
165
+ "-c",
166
+ 'web_search="disabled"',
167
+ "--output-schema",
168
+ outputSchemaPath,
169
+ "--json",
170
+ ];
171
+ for (const feature of CODEX_DISABLED_FEATURES)
172
+ args.push("--disable", feature);
173
+ if (toolPolicy === "none")
174
+ args.push("--disable", "shell_tool", "--disable", "unified_exec");
175
+ args.push("-c", "include_apps_instructions=false", "-c", "include_collaboration_mode_instructions=false", "-c", "include_environment_context=false", "-c", "include_permissions_instructions=false", "-c", `model_reasoning_effort=${JSON.stringify(request.route.effort ?? DEFAULT_AGENT_EFFORT)}`, "-c", `model_verbosity=${JSON.stringify(request.route.verbosity ?? DEFAULT_CODEX_VERBOSITY)}`);
176
+ if (request.brokerUrl) {
177
+ args.push("-c", `mcp_servers.research_broker.url=${JSON.stringify(request.brokerUrl)}`, "-c", "mcp_servers.research_broker.required=true", "-c", 'mcp_servers.research_broker.enabled_tools=["fetch_candidate_source"]');
178
+ }
179
+ if (request.route.model)
180
+ args.push("--model", request.route.model);
181
+ args.push(request.prompt);
182
+ return { binary: resolvedBinary, args };
183
+ }
184
+ const toolPolicy = request.toolPolicy ?? "workspace-read";
185
+ const args = [
186
+ "-p",
187
+ request.prompt,
188
+ "--output-format",
189
+ "json",
190
+ "--permission-mode",
191
+ "default",
192
+ "--no-session-persistence",
193
+ "--max-turns",
194
+ String(request.maxTurns),
195
+ "--setting-sources",
196
+ "",
197
+ "--no-chrome",
198
+ "--disable-slash-commands",
199
+ "--tools",
200
+ toolPolicy === "none" ? "" : "Read,Glob,Grep",
201
+ "--effort",
202
+ request.route.effort ?? DEFAULT_AGENT_EFFORT,
203
+ ];
204
+ if (request.purpose !== "repair") {
205
+ args.splice(4, 0, "--json-schema", JSON.stringify(request.outputSchema));
206
+ }
207
+ const allowedTools = toolPolicy === "none" ? [] : ["Read", "Glob", "Grep"];
208
+ if (request.brokerUrl) {
209
+ const mcpConfigPath = join(request.capsuleRoot, "research-mcp.json");
210
+ await writeFile(mcpConfigPath, `${JSON.stringify({
211
+ mcpServers: {
212
+ research_broker: { type: "http", url: request.brokerUrl },
213
+ },
214
+ })}\n`, { encoding: "utf8", mode: 0o600 });
215
+ allowedTools.push("mcp__research_broker__fetch_candidate_source");
216
+ args.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
217
+ }
218
+ args.push("--allowedTools", allowedTools.join(","));
219
+ if (request.maxCostUsd > 0)
220
+ args.push("--max-budget-usd", String(request.maxCostUsd));
221
+ if (request.route.model)
222
+ args.push("--model", request.route.model);
223
+ return { binary: resolvedBinary, args };
224
+ }
225
+ async function sandboxInvocation(binary, args, capsuleRoot, projectRoot, workspaceRoot, targetBinary) {
226
+ const configuredCredentialPath = join(workspaceRoot, ".tiangong-research", ".env");
227
+ const workspaceCredentialPath = await realpath(configuredCredentialPath).catch(() => configuredCredentialPath);
228
+ if (platform() === "darwin") {
229
+ const sandbox = "/usr/bin/sandbox-exec";
230
+ await requireExecutable(sandbox, "macOS sandbox-exec");
231
+ const profile = join(capsuleRoot, "execution.sb");
232
+ const canonicalCapsuleRoot = await realpath(capsuleRoot);
233
+ const readRoots = await existingReadRoots(binary, targetBinary, canonicalCapsuleRoot);
234
+ const readClauses = readRoots.map((path) => `(subpath ${sandboxString(path)})`).join(" ");
235
+ const policy = [
236
+ "(version 1)",
237
+ "(deny default)",
238
+ "(allow process*)",
239
+ "(allow network*)",
240
+ "(allow file-read-metadata)",
241
+ `(allow file-read* ${readClauses} (literal "/") (literal "/var") (literal "/dev/dtracehelper") (literal "/dev/null") (literal "/dev/urandom"))`,
242
+ `(deny file-read* (literal ${sandboxString(workspaceCredentialPath)}))`,
243
+ '(allow file-ioctl (literal "/dev/dtracehelper"))',
244
+ "(allow sysctl-read)",
245
+ "(allow mach-lookup)",
246
+ "(allow ipc-posix*)",
247
+ `(allow file-write* (literal "/dev/dtracehelper") (literal "/dev/null") (subpath ${sandboxString(canonicalCapsuleRoot)}))`,
248
+ "",
249
+ ].join("\n");
250
+ await writeFile(profile, policy, { encoding: "utf8", mode: 0o600 });
251
+ return { binary: sandbox, args: ["-f", profile, binary, ...args] };
252
+ }
253
+ if (platform() === "linux") {
254
+ const bubblewrap = await firstExecutable([
255
+ "/usr/bin/bwrap",
256
+ "/usr/local/bin/bwrap",
257
+ "/bin/bwrap",
258
+ ]);
259
+ if (!bubblewrap) {
260
+ throw new CliError("Linux research execution requires Bubblewrap.", {
261
+ code: "RESEARCH_SANDBOX_UNAVAILABLE",
262
+ exitCode: 3,
263
+ });
264
+ }
265
+ const sandboxArgs = ["--die-with-parent", "--new-session"];
266
+ const systemRoots = await existingLinuxSystemRoots();
267
+ for (const path of systemRoots) {
268
+ sandboxArgs.push("--ro-bind", path, path);
269
+ }
270
+ const createdDirectories = new Set(systemRoots);
271
+ const runtimeRoots = [
272
+ executableReadRoot(binary),
273
+ executableReadRoot(targetBinary),
274
+ executableReadRoot(process.execPath),
275
+ ];
276
+ for (const runtimeRoot of [...new Set(runtimeRoots)]) {
277
+ if (runtimeRoot.startsWith(`${capsuleRoot}/`) ||
278
+ systemRoots.some((root) => runtimeRoot === root || runtimeRoot.startsWith(`${root}/`))) {
279
+ continue;
280
+ }
281
+ await appendBubblewrapParentDirectories(sandboxArgs, runtimeRoot, createdDirectories);
282
+ sandboxArgs.push("--ro-bind", runtimeRoot, runtimeRoot);
283
+ createdDirectories.add(runtimeRoot);
284
+ }
285
+ await appendBubblewrapParentDirectories(sandboxArgs, capsuleRoot, createdDirectories);
286
+ sandboxArgs.push("--bind", capsuleRoot, capsuleRoot, "--proc", "/proc", "--dev", "/dev", "--chdir", projectRoot);
287
+ if (workspaceCredentialPath.startsWith(`${capsuleRoot}/`) &&
288
+ (await pathIsReadable(workspaceCredentialPath))) {
289
+ sandboxArgs.push("--ro-bind", "/dev/null", workspaceCredentialPath);
290
+ }
291
+ sandboxArgs.push(binary, ...args);
292
+ return { binary: bubblewrap, args: sandboxArgs };
293
+ }
294
+ throw new CliError(`Unsupported research execution platform: ${platform()}`, {
295
+ code: "RESEARCH_SANDBOX_UNAVAILABLE",
296
+ exitCode: 3,
297
+ });
298
+ }
299
+ async function prepareCapsuleHome(route, capsuleHome, environment) {
300
+ await mkdir(capsuleHome, { recursive: true, mode: 0o700 });
301
+ const sourceHome = environment.HOME && isAbsolute(environment.HOME) ? environment.HOME : homedir();
302
+ const claudeConfigRoot = environment.CLAUDE_CONFIG_DIR && isAbsolute(environment.CLAUDE_CONFIG_DIR)
303
+ ? environment.CLAUDE_CONFIG_DIR
304
+ : join(sourceHome, ".claude");
305
+ const candidates = route.agent === "codex"
306
+ ? [
307
+ {
308
+ source: join(environment.CODEX_HOME && isAbsolute(environment.CODEX_HOME)
309
+ ? environment.CODEX_HOME
310
+ : join(sourceHome, ".codex"), "auth.json"),
311
+ destination: join(capsuleHome, ".codex", "auth.json"),
312
+ },
313
+ ]
314
+ : [
315
+ {
316
+ source: join(claudeConfigRoot, ".credentials.json"),
317
+ destination: join(capsuleHome, ".claude", ".credentials.json"),
318
+ },
319
+ ];
320
+ const secrets = [];
321
+ for (const candidate of candidates) {
322
+ const info = await lstat(candidate.source).catch(() => undefined);
323
+ if (!info)
324
+ continue;
325
+ if (!info.isFile() || info.isSymbolicLink()) {
326
+ throw new CliError(`Configured ${route.agent} authentication material is not a regular file.`, {
327
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
328
+ exitCode: 3,
329
+ });
330
+ }
331
+ assertOwnerOnlyAuthenticationFile(info.mode, route.agent);
332
+ await mkdir(dirname(candidate.destination), { recursive: true, mode: 0o700 });
333
+ await copyFile(candidate.source, candidate.destination, fsConstants.COPYFILE_EXCL);
334
+ await chmod(candidate.destination, 0o600);
335
+ secrets.push(...authSecretValues(await readFile(candidate.destination, "utf8")));
336
+ }
337
+ const settingsEnvironment = route.agent === "claude"
338
+ ? await readClaudeSettingsEnvironment(join(claudeConfigRoot, "settings.json"))
339
+ : {};
340
+ for (const name of ROUTE_AUTH_ENVIRONMENT[route.agent]) {
341
+ const value = settingsEnvironment[name];
342
+ if (value && value.length >= 8)
343
+ secrets.push(value);
344
+ }
345
+ return {
346
+ secrets: [...new Set(secrets)].sort((left, right) => right.length - left.length),
347
+ environment: settingsEnvironment,
348
+ };
349
+ }
350
+ async function readClaudeSettingsEnvironment(path) {
351
+ const info = await lstat(path).catch(() => undefined);
352
+ if (!info)
353
+ return {};
354
+ if (!info.isFile() || info.isSymbolicLink()) {
355
+ throw new CliError("Claude settings authentication source is not a regular file.", {
356
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
357
+ exitCode: 3,
358
+ });
359
+ }
360
+ let value;
361
+ try {
362
+ value = JSON.parse(await readFile(path, "utf8"));
363
+ }
364
+ catch {
365
+ throw new CliError("Claude settings authentication source is not valid JSON.", {
366
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
367
+ exitCode: 3,
368
+ });
369
+ }
370
+ if (!isObject(value) || !isObject(value.env))
371
+ return {};
372
+ const selected = {};
373
+ for (const name of CLAUDE_SETTINGS_ENVIRONMENT) {
374
+ const candidate = value.env[name];
375
+ if (candidate === undefined)
376
+ continue;
377
+ if (typeof candidate !== "string" || candidate.length === 0) {
378
+ throw new CliError(`Claude settings ${name} must be a non-empty string.`, {
379
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
380
+ exitCode: 3,
381
+ });
382
+ }
383
+ if (ROUTE_AUTH_ENVIRONMENT.claude.includes(name) && candidate.length < 8) {
384
+ throw new CliError(`Claude settings ${name} is too short to be valid.`, {
385
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
386
+ exitCode: 3,
387
+ });
388
+ }
389
+ if (name === "ANTHROPIC_BASE_URL")
390
+ assertSafeClaudeBaseUrl(candidate);
391
+ selected[name] = candidate;
392
+ }
393
+ if (Object.keys(selected).some((name) => ROUTE_AUTH_ENVIRONMENT.claude.includes(name))) {
394
+ assertOwnerOnlyAuthenticationFile(info.mode, "claude");
395
+ }
396
+ return selected;
397
+ }
398
+ function assertSafeClaudeBaseUrl(value) {
399
+ let url;
400
+ try {
401
+ url = new URL(value);
402
+ }
403
+ catch {
404
+ throw new CliError("Claude ANTHROPIC_BASE_URL must be a valid HTTPS URL.", {
405
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
406
+ exitCode: 3,
407
+ });
408
+ }
409
+ if (url.protocol !== "https:" || url.username || url.password) {
410
+ throw new CliError("Claude ANTHROPIC_BASE_URL must use HTTPS without embedded credentials.", {
411
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
412
+ exitCode: 3,
413
+ });
414
+ }
415
+ }
416
+ function assertOwnerOnlyAuthenticationFile(mode, agent) {
417
+ if (platform() !== "win32" && (mode & 0o077) !== 0) {
418
+ throw new CliError(`Configured ${agent} authentication material must be owner-only (0600).`, {
419
+ code: "RESEARCH_EXECUTOR_AUTH_INVALID",
420
+ exitCode: 3,
421
+ });
422
+ }
423
+ }
424
+ async function fingerprintResolvedBinary(route, launcher, target, pathValue) {
425
+ const versionEnvironment = {
426
+ PATH: pathValue ?? process.env.PATH,
427
+ NO_COLOR: "1",
428
+ };
429
+ if (route.wrapperTargetBinary)
430
+ versionEnvironment[WRAPPER_TARGET_ENV] = target;
431
+ const version = await spawnCaptured({
432
+ binary: launcher,
433
+ args: ["--version"],
434
+ cwd: dirname(launcher),
435
+ env: versionEnvironment,
436
+ timeoutMs: 10_000,
437
+ maxCaptureBytes: 64 * 1024,
438
+ }).catch((error) => {
439
+ throw new CliError(`Could not inspect the configured ${route.agent} executable.`, {
440
+ code: "RESEARCH_EXECUTOR_UNAVAILABLE",
441
+ exitCode: 3,
442
+ details: {
443
+ error: sanitizeResearchText(error instanceof Error ? error.message : String(error)),
444
+ },
445
+ });
446
+ });
447
+ const binaryVersion = `${version.stdout}\n${version.stderr}`.trim().split(/\r?\n/, 1)[0] ?? "";
448
+ if (version.exitCode !== 0 || !binaryVersion) {
449
+ throw new CliError(`Configured ${route.agent} executable did not report a version.`, {
450
+ code: "RESEARCH_EXECUTOR_UNAVAILABLE",
451
+ exitCode: 3,
452
+ });
453
+ }
454
+ return {
455
+ agent: route.agent,
456
+ model: route.model,
457
+ effort: route.effort ?? DEFAULT_AGENT_EFFORT,
458
+ verbosity: route.agent === "codex" ? (route.verbosity ?? DEFAULT_CODEX_VERBOSITY) : null,
459
+ binarySha256: await sha256File(target),
460
+ wrapperSha256: await sha256File(launcher),
461
+ adapterSha256: await sha256File(EXECUTOR_WRAPPER_PATH),
462
+ binaryVersion: sanitizeResearchText(binaryVersion).slice(0, 300),
463
+ platform: platform(),
464
+ architecture: arch(),
465
+ };
466
+ }
467
+ async function resolveAgentExecutables(route, pathValue) {
468
+ const launcher = await resolveExecutable(route.binary, pathValue);
469
+ const target = route.wrapperTargetBinary
470
+ ? await resolveExecutable(route.wrapperTargetBinary, pathValue)
471
+ : launcher;
472
+ if (launcher === target && route.wrapperTargetBinary) {
473
+ throw new CliError("Agent wrapper and wrapper target resolve to the same executable.", {
474
+ code: "RESEARCH_EXECUTOR_INVALID",
475
+ exitCode: 2,
476
+ });
477
+ }
478
+ return { launcher, target };
479
+ }
480
+ async function resolveExecutable(binary, pathValue) {
481
+ const candidates = isAbsolute(binary)
482
+ ? [binary]
483
+ : (pathValue ?? process.env.PATH ?? "")
484
+ .split(delimiter)
485
+ .filter(Boolean)
486
+ .map((directory) => resolve(directory, binary));
487
+ for (const candidate of candidates) {
488
+ try {
489
+ await access(candidate, fsConstants.X_OK);
490
+ const info = await lstat(candidate);
491
+ if (!info.isFile() && !info.isSymbolicLink())
492
+ continue;
493
+ return await realpath(candidate);
494
+ }
495
+ catch {
496
+ continue;
497
+ }
498
+ }
499
+ throw new CliError(`Configured executable is unavailable: ${binary}`, {
500
+ code: "RESEARCH_EXECUTOR_UNAVAILABLE",
501
+ exitCode: 3,
502
+ });
503
+ }
504
+ async function spawnCaptured(input) {
505
+ return new Promise((resolvePromise, reject) => {
506
+ const child = spawn(input.binary, input.args, {
507
+ cwd: input.cwd,
508
+ env: input.env,
509
+ shell: false,
510
+ detached: platform() !== "win32",
511
+ stdio: ["ignore", "pipe", "pipe"],
512
+ });
513
+ const stdout = [];
514
+ const stderr = [];
515
+ let capturedBytes = 0;
516
+ let captureExceeded = false;
517
+ let settled = false;
518
+ const terminate = () => {
519
+ if (!child.pid)
520
+ return;
521
+ try {
522
+ if (platform() === "win32")
523
+ child.kill("SIGKILL");
524
+ else
525
+ process.kill(-child.pid, "SIGKILL");
526
+ }
527
+ catch {
528
+ child.kill("SIGKILL");
529
+ }
530
+ };
531
+ const timer = setTimeout(() => terminate(), input.timeoutMs);
532
+ const capture = (target, chunk) => {
533
+ capturedBytes += chunk.length;
534
+ if (capturedBytes > input.maxCaptureBytes) {
535
+ captureExceeded = true;
536
+ terminate();
537
+ return;
538
+ }
539
+ target.push(chunk);
540
+ };
541
+ child.stdout.on("data", (chunk) => capture(stdout, chunk));
542
+ child.stderr.on("data", (chunk) => capture(stderr, chunk));
543
+ child.on("error", (error) => {
544
+ if (settled)
545
+ return;
546
+ settled = true;
547
+ clearTimeout(timer);
548
+ reject(error);
549
+ });
550
+ child.on("close", (code, signal) => {
551
+ if (settled)
552
+ return;
553
+ settled = true;
554
+ clearTimeout(timer);
555
+ const detail = signal ? `\nprocess terminated by ${signal}` : "";
556
+ resolvePromise({
557
+ exitCode: typeof code === "number" ? code : 70,
558
+ stdout: Buffer.concat(stdout).toString("utf8"),
559
+ stderr: `${Buffer.concat(stderr).toString("utf8")}${detail}`,
560
+ captureExceeded,
561
+ });
562
+ });
563
+ });
564
+ }
565
+ function parseAgentResult(route, stdout, stderr) {
566
+ if (route.agent === "codex") {
567
+ let inputTokens = 0;
568
+ let cachedInputTokens = 0;
569
+ let outputTokens = 0;
570
+ let model = route.model;
571
+ const messages = [];
572
+ let parsedEvents = 0;
573
+ const eventCounts = {};
574
+ const itemCounts = {};
575
+ let toolCalls = 0;
576
+ let reasoningOutputTokens = 0;
577
+ const providerErrors = [];
578
+ for (const line of stdout.split(/\r?\n/)) {
579
+ if (!line.trim())
580
+ continue;
581
+ try {
582
+ const event = JSON.parse(line);
583
+ parsedEvents += 1;
584
+ if (typeof event.type === "string")
585
+ incrementCount(eventCounts, event.type);
586
+ if (event.type === "error")
587
+ appendProviderError(providerErrors, event);
588
+ if (event.type === "turn.completed" && isObject(event.usage)) {
589
+ cachedInputTokens = numeric(event.usage.cached_input_tokens);
590
+ inputTokens = Math.max(0, numeric(event.usage.input_tokens) - cachedInputTokens);
591
+ outputTokens = numeric(event.usage.output_tokens);
592
+ reasoningOutputTokens = numeric(event.usage.reasoning_output_tokens);
593
+ }
594
+ if (typeof event.model === "string")
595
+ model = event.model;
596
+ if (event.type === "item.completed" && isObject(event.item)) {
597
+ if (typeof event.item.type === "string") {
598
+ incrementCount(itemCounts, event.item.type);
599
+ if (isToolItemType(event.item.type))
600
+ toolCalls += 1;
601
+ if (event.item.type === "error")
602
+ appendProviderError(providerErrors, event.item);
603
+ }
604
+ if (event.item.type === "agent_message" && typeof event.item.text === "string") {
605
+ messages.push(event.item.text);
606
+ }
607
+ }
608
+ }
609
+ catch {
610
+ continue;
611
+ }
612
+ }
613
+ return {
614
+ stdout: messages.at(-1) ?? stdout,
615
+ stderr,
616
+ inputTokens,
617
+ cachedInputTokens,
618
+ outputTokens,
619
+ costUsd: 0,
620
+ model,
621
+ parseFailed: parsedEvents === 0 || messages.length === 0,
622
+ telemetry: {
623
+ eventCounts,
624
+ itemCounts,
625
+ toolCalls,
626
+ providerTurns: eventCounts["turn.completed"] ?? eventCounts["turn.started"] ?? null,
627
+ reasoningOutputTokens,
628
+ providerErrors,
629
+ },
630
+ };
631
+ }
632
+ try {
633
+ const value = JSON.parse(stdout);
634
+ const usage = isObject(value.usage) ? value.usage : {};
635
+ return {
636
+ stdout: typeof value.result === "string" ? value.result : stdout,
637
+ stderr,
638
+ inputTokens: numeric(usage.input_tokens),
639
+ cachedInputTokens: numeric(usage.cache_creation_input_tokens) + numeric(usage.cache_read_input_tokens),
640
+ outputTokens: numeric(usage.output_tokens),
641
+ costUsd: numeric(value.total_cost_usd),
642
+ model: typeof value.model === "string" ? value.model : route.model,
643
+ parseFailed: typeof value.result !== "string",
644
+ telemetry: {
645
+ eventCounts: { result: 1 },
646
+ itemCounts: {},
647
+ toolCalls: 0,
648
+ providerTurns: numeric(value.num_turns) || null,
649
+ reasoningOutputTokens: numeric(usage.reasoning_output_tokens),
650
+ providerErrors: [],
651
+ },
652
+ };
653
+ }
654
+ catch {
655
+ return {
656
+ stdout,
657
+ stderr,
658
+ inputTokens: 0,
659
+ cachedInputTokens: 0,
660
+ outputTokens: 0,
661
+ costUsd: 0,
662
+ model: route.model,
663
+ parseFailed: true,
664
+ telemetry: {
665
+ eventCounts: {},
666
+ itemCounts: {},
667
+ toolCalls: 0,
668
+ providerTurns: null,
669
+ reasoningOutputTokens: 0,
670
+ providerErrors: [],
671
+ },
672
+ };
673
+ }
674
+ }
675
+ function sanitizedEnvironment(source, temporaryDirectory, capsuleHome, route, targetBinary, capsuleAuthEnvironment) {
676
+ const environment = {};
677
+ for (const [name, value] of Object.entries(source)) {
678
+ if (!value || isSensitiveEnvironmentName(name))
679
+ continue;
680
+ if (name === "HOME" ||
681
+ name === "CODEX_HOME" ||
682
+ name === "CLAUDE_CONFIG_DIR" ||
683
+ name === WRAPPER_TARGET_ENV) {
684
+ continue;
685
+ }
686
+ environment[name] = value;
687
+ }
688
+ for (const [name, value] of Object.entries(capsuleAuthEnvironment)) {
689
+ if (value)
690
+ environment[name] = value;
691
+ }
692
+ const explicitRouteEnvironment = route.agent === "claude" ? CLAUDE_SETTINGS_ENVIRONMENT : ROUTE_AUTH_ENVIRONMENT[route.agent];
693
+ for (const name of explicitRouteEnvironment) {
694
+ const value = source[name];
695
+ if (value)
696
+ environment[name] = value;
697
+ }
698
+ environment.HOME = capsuleHome;
699
+ if (route.agent === "codex") {
700
+ environment.CODEX_HOME = join(capsuleHome, ".codex");
701
+ }
702
+ else {
703
+ environment.CLAUDE_CONFIG_DIR = join(capsuleHome, ".claude");
704
+ environment.CLAUDE_TMPDIR = temporaryDirectory;
705
+ environment.CLAUDE_CODE_TMPDIR = temporaryDirectory;
706
+ environment.BUN_TMPDIR = temporaryDirectory;
707
+ }
708
+ environment.NO_COLOR = "1";
709
+ if (route.wrapperTargetBinary)
710
+ environment[WRAPPER_TARGET_ENV] = targetBinary;
711
+ environment.TMPDIR = temporaryDirectory;
712
+ environment.TMP = temporaryDirectory;
713
+ environment.TEMP = temporaryDirectory;
714
+ return environment;
715
+ }
716
+ function redactResult(stdout, stderr, secrets) {
717
+ let exposed = false;
718
+ for (const secret of secrets) {
719
+ if (stdout.includes(secret) || stderr.includes(secret))
720
+ exposed = true;
721
+ }
722
+ return {
723
+ stdout: sanitizeResearchText(stdout, secrets),
724
+ stderr: sanitizeResearchText(stderr, secrets),
725
+ exposed,
726
+ };
727
+ }
728
+ function estimateCost(route, usage) {
729
+ if (!route.pricing)
730
+ return 0;
731
+ return roundMoney((usage.inputTokens * route.pricing.inputUsdPerMillionTokens +
732
+ usage.cachedInputTokens * route.pricing.cachedInputUsdPerMillionTokens +
733
+ usage.outputTokens * route.pricing.outputUsdPerMillionTokens) /
734
+ 1_000_000);
735
+ }
736
+ function authSecretValues(content) {
737
+ try {
738
+ const value = JSON.parse(content);
739
+ const secrets = new Set();
740
+ collectSensitiveJsonValues(value, "", secrets);
741
+ return [...secrets].sort((left, right) => right.length - left.length);
742
+ }
743
+ catch {
744
+ return [];
745
+ }
746
+ }
747
+ function collectSensitiveJsonValues(value, key, target) {
748
+ if (typeof value === "string") {
749
+ if (isSensitiveEnvironmentName(key) && value.length >= 8)
750
+ target.add(value);
751
+ return;
752
+ }
753
+ if (Array.isArray(value)) {
754
+ for (const item of value)
755
+ collectSensitiveJsonValues(item, key, target);
756
+ return;
757
+ }
758
+ if (!isObject(value))
759
+ return;
760
+ for (const [childKey, item] of Object.entries(value)) {
761
+ collectSensitiveJsonValues(item, childKey, target);
762
+ }
763
+ }
764
+ async function existingReadRoots(launcher, target, capsuleRoot) {
765
+ const candidates = [
766
+ capsuleRoot,
767
+ "/System",
768
+ "/usr",
769
+ "/bin",
770
+ "/sbin",
771
+ "/private/etc/hosts",
772
+ "/private/etc/resolv.conf",
773
+ "/private/etc/ssl",
774
+ "/private/var/db/timezone",
775
+ "/private/var/run",
776
+ "/private/var/select",
777
+ executableReadRoot(launcher),
778
+ executableReadRoot(target),
779
+ executableReadRoot(process.execPath),
780
+ ];
781
+ const result = [];
782
+ for (const candidate of candidates) {
783
+ if (result.some((root) => candidate === root || candidate.startsWith(`${root}/`)))
784
+ continue;
785
+ if (await lstat(candidate).catch(() => undefined))
786
+ result.push(candidate);
787
+ }
788
+ return result;
789
+ }
790
+ function executableReadRoot(binary) {
791
+ const parent = dirname(binary);
792
+ return dirname(parent);
793
+ }
794
+ async function existingLinuxSystemRoots() {
795
+ const candidates = ["/usr", "/bin", "/sbin", "/lib", "/lib64", "/etc", "/opt"];
796
+ const roots = [];
797
+ for (const candidate of candidates) {
798
+ if (await lstat(candidate).catch(() => undefined))
799
+ roots.push(candidate);
800
+ }
801
+ return roots;
802
+ }
803
+ async function appendBubblewrapParentDirectories(args, target, created) {
804
+ const parts = resolve(target).split("/").filter(Boolean);
805
+ let current = "";
806
+ for (const part of parts.slice(0, -1)) {
807
+ current += `/${part}`;
808
+ if (!(await lstat(current).catch(() => undefined)))
809
+ continue;
810
+ if ([...created].some((directory) => current === directory || current.startsWith(`${directory}/`))) {
811
+ continue;
812
+ }
813
+ args.push("--dir", current);
814
+ created.add(current);
815
+ }
816
+ }
817
+ async function pathIsReadable(path) {
818
+ try {
819
+ await access(path, fsConstants.R_OK);
820
+ return true;
821
+ }
822
+ catch {
823
+ return false;
824
+ }
825
+ }
826
+ function validateAgentBinary(route) {
827
+ if (!isAbsolute(route.binary) && route.binary !== route.agent) {
828
+ throw new CliError(`Configured ${route.agent} binary must be absolute or exactly '${route.agent}'.`, {
829
+ code: "RESEARCH_EXECUTOR_INVALID",
830
+ exitCode: 2,
831
+ });
832
+ }
833
+ if (route.wrapperTargetBinary &&
834
+ (!isAbsolute(route.binary) || !isAbsolute(route.wrapperTargetBinary))) {
835
+ throw new CliError("An agent wrapper and its wrapperTargetBinary must both use explicit absolute paths.", {
836
+ code: "RESEARCH_EXECUTOR_INVALID",
837
+ exitCode: 2,
838
+ });
839
+ }
840
+ if (route.wrapperTargetBinary === route.binary) {
841
+ throw new CliError("Agent wrapperTargetBinary must differ from the wrapper path.", {
842
+ code: "RESEARCH_EXECUTOR_INVALID",
843
+ exitCode: 2,
844
+ });
845
+ }
846
+ }
847
+ async function requireExecutable(path, label) {
848
+ try {
849
+ await access(path, fsConstants.X_OK);
850
+ }
851
+ catch {
852
+ throw new CliError(`${label} is unavailable: ${path}`, {
853
+ code: "RESEARCH_SANDBOX_UNAVAILABLE",
854
+ exitCode: 3,
855
+ });
856
+ }
857
+ }
858
+ async function firstExecutable(candidates) {
859
+ for (const candidate of candidates) {
860
+ try {
861
+ await access(candidate, fsConstants.X_OK);
862
+ return candidate;
863
+ }
864
+ catch {
865
+ continue;
866
+ }
867
+ }
868
+ return undefined;
869
+ }
870
+ function sandboxString(value) {
871
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
872
+ }
873
+ function numeric(value) {
874
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
875
+ }
876
+ function roundMoney(value) {
877
+ return Math.round(value * 1_000_000) / 1_000_000;
878
+ }
879
+ function incrementCount(target, key) {
880
+ target[key] = (target[key] ?? 0) + 1;
881
+ }
882
+ function appendProviderError(target, value) {
883
+ if (target.length >= 10)
884
+ return;
885
+ const candidate = [value.message, value.error, value.detail]
886
+ .map((item) => {
887
+ if (typeof item === "string")
888
+ return item;
889
+ if (isObject(item))
890
+ return JSON.stringify(item);
891
+ return "";
892
+ })
893
+ .find(Boolean);
894
+ if (!candidate)
895
+ return;
896
+ const bounded = candidate.slice(0, 1_000);
897
+ if (!target.includes(bounded))
898
+ target.push(bounded);
899
+ }
900
+ function sanitizeExecutionTelemetry(telemetry, secrets) {
901
+ return {
902
+ ...telemetry,
903
+ providerErrors: telemetry.providerErrors.map((error) => sanitizeResearchText(error, secrets).slice(0, 1_000)),
904
+ };
905
+ }
906
+ function isToolItemType(value) {
907
+ return (value === "command_execution" ||
908
+ value === "mcp_tool_call" ||
909
+ value === "web_search" ||
910
+ value === "dynamic_tool_call" ||
911
+ value === "tool_call" ||
912
+ value.endsWith("_tool_call"));
913
+ }
914
+ function sameRuntimeFingerprint(actual, expected) {
915
+ return (actual.agent === expected.agent &&
916
+ actual.model === expected.model &&
917
+ actual.effort === expected.effort &&
918
+ actual.verbosity === expected.verbosity &&
919
+ actual.binarySha256 === expected.binarySha256 &&
920
+ actual.wrapperSha256 === expected.wrapperSha256 &&
921
+ actual.adapterSha256 === expected.adapterSha256 &&
922
+ actual.binaryVersion === expected.binaryVersion &&
923
+ actual.platform === expected.platform &&
924
+ actual.architecture === expected.architecture);
925
+ }
926
+ //# sourceMappingURL=executor.js.map