@tiangong-ai/cli 0.0.20 → 0.0.22

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 (47) hide show
  1. package/AGENTS.md +3 -2
  2. package/README.md +158 -13
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +11 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/research/orchestration.js +166 -11
  7. package/dist/research/orchestration.js.map +1 -1
  8. package/dist/research/workspace/broker.js +407 -31
  9. package/dist/research/workspace/broker.js.map +1 -1
  10. package/dist/research/workspace/capabilities.js +65 -0
  11. package/dist/research/workspace/capabilities.js.map +1 -1
  12. package/dist/research/workspace/constants.d.ts +1 -1
  13. package/dist/research/workspace/constants.js +4 -0
  14. package/dist/research/workspace/constants.js.map +1 -1
  15. package/dist/research/workspace/evidence.d.ts +32 -0
  16. package/dist/research/workspace/evidence.js +235 -0
  17. package/dist/research/workspace/evidence.js.map +1 -0
  18. package/dist/research/workspace/executor.d.ts +11 -1
  19. package/dist/research/workspace/executor.js +678 -105
  20. package/dist/research/workspace/executor.js.map +1 -1
  21. package/dist/research/workspace/input-plan.d.ts +5 -0
  22. package/dist/research/workspace/input-plan.js +319 -0
  23. package/dist/research/workspace/input-plan.js.map +1 -0
  24. package/dist/research/workspace/journal.js +2 -1
  25. package/dist/research/workspace/journal.js.map +1 -1
  26. package/dist/research/workspace/preflight.d.ts +108 -0
  27. package/dist/research/workspace/preflight.js +261 -0
  28. package/dist/research/workspace/preflight.js.map +1 -0
  29. package/dist/research/workspace/projects.d.ts +5 -2
  30. package/dist/research/workspace/projects.js +290 -6
  31. package/dist/research/workspace/projects.js.map +1 -1
  32. package/dist/research/workspace/runtime.d.ts +5 -1
  33. package/dist/research/workspace/runtime.js +1314 -196
  34. package/dist/research/workspace/runtime.js.map +1 -1
  35. package/dist/research/workspace/sanitization.d.ts +5 -0
  36. package/dist/research/workspace/sanitization.js +72 -0
  37. package/dist/research/workspace/sanitization.js.map +1 -0
  38. package/dist/research/workspace/schemas.d.ts +17 -0
  39. package/dist/research/workspace/schemas.js +342 -0
  40. package/dist/research/workspace/schemas.js.map +1 -0
  41. package/dist/research/workspace/storage.js +4 -0
  42. package/dist/research/workspace/storage.js.map +1 -1
  43. package/dist/research/workspace/types.d.ts +160 -0
  44. package/dist/research/workspace/workspace.d.ts +14 -3
  45. package/dist/research/workspace/workspace.js +374 -12
  46. package/dist/research/workspace/workspace.js.map +1 -1
  47. package/package.json +2 -1
@@ -1,47 +1,156 @@
1
1
  import { spawn } from "node:child_process";
2
- import { access, mkdir, realpath, writeFile } from "node:fs/promises";
3
2
  import { constants as fsConstants } from "node:fs";
4
- import { homedir, platform } from "node:os";
5
- import { isAbsolute, join } from "node:path";
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";
6
7
  import { CliError } from "../../errors.js";
8
+ import { configuredResearchSecrets, isSensitiveEnvironmentName, sanitizeResearchText, } from "./sanitization.js";
9
+ import { isObject, sha256File } from "./storage.js";
7
10
  const MAX_CAPTURE_BYTES = 5 * 1024 * 1024;
8
- const CREDENTIAL_ENV_NAME = /(^|[_-])(authorization|auth|cookie|credential|password|passwd|private[_-]?key|secret|token|access[_-]?key|api[_-]?key)([_-]|$)/i;
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";
9
48
  export async function executeAgent(request) {
10
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
+ }
11
62
  const temporaryDirectory = join(request.capsuleRoot, "tmp");
12
63
  await mkdir(temporaryDirectory, { recursive: true, mode: 0o700 });
13
- const secrets = configuredSecrets(request.environment);
14
- const invocation = await buildInvocation(request.route, request.prompt, request.brokerUrl, request.capsuleRoot);
15
- const sandboxed = await sandboxInvocation(invocation.binary, invocation.args, request.capsuleRoot, request.projectRoot, request.workspaceRoot);
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);
16
86
  const started = process.hrtime.bigint();
17
- const completed = await spawnCaptured({
18
- binary: sandboxed.binary,
19
- args: sandboxed.args,
20
- cwd: request.projectRoot,
21
- env: sanitizedEnvironment(request.environment, temporaryDirectory),
22
- timeoutMs: request.timeoutSeconds * 1000,
23
- });
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
+ }
24
107
  const wallSeconds = Number(process.hrtime.bigint() - started) / 1_000_000_000;
25
- const parsed = parseAgentResult(request.route.agent, completed.stdout, completed.stderr);
108
+ const parsed = parseAgentResult(request.route, completed.stdout, completed.stderr);
26
109
  const redacted = redactResult(parsed.stdout, parsed.stderr, secrets);
27
110
  const exitCode = redacted.exposed
28
111
  ? 86
29
- : completed.exitCode !== 0
30
- ? completed.exitCode
31
- : parsed.parseFailed
32
- ? 3
33
- : 0;
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);
34
125
  return {
35
126
  exitCode,
36
127
  stdout: redacted.stdout,
37
- stderr: redacted.stderr,
38
- tokens: redacted.exposed ? 0 : parsed.tokens,
39
- costUsd: redacted.exposed ? 0 : parsed.costUsd,
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,
40
136
  wallSeconds,
137
+ model: parsed.model ?? request.route.model,
138
+ runtime: { ...runtime, model: parsed.model ?? request.route.model },
139
+ telemetry: sanitizeExecutionTelemetry(parsed.telemetry, secrets),
41
140
  };
42
141
  }
43
- async function buildInvocation(route, prompt, brokerUrl, capsuleRoot) {
44
- if (route.agent === "codex") {
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";
45
154
  const args = [
46
155
  "exec",
47
156
  "--ignore-user-config",
@@ -52,67 +161,90 @@ async function buildInvocation(route, prompt, brokerUrl, capsuleRoot) {
52
161
  "--color",
53
162
  "never",
54
163
  "--sandbox",
55
- "workspace-write",
164
+ "read-only",
56
165
  "-c",
57
166
  'web_search="disabled"',
167
+ "--output-schema",
168
+ outputSchemaPath,
58
169
  "--json",
59
170
  ];
60
- if (brokerUrl) {
61
- args.push("-c", `mcp_servers.research_broker.url=${JSON.stringify(brokerUrl)}`, "-c", "mcp_servers.research_broker.required=true", "-c", 'mcp_servers.research_broker.enabled_tools=["fetch_candidate_source"]');
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"]');
62
178
  }
63
- if (route.model)
64
- args.push("--model", route.model);
65
- args.push(prompt);
66
- return { binary: route.binary, args };
179
+ if (request.route.model)
180
+ args.push("--model", request.route.model);
181
+ args.push(request.prompt);
182
+ return { binary: resolvedBinary, args };
67
183
  }
184
+ const toolPolicy = request.toolPolicy ?? "workspace-read";
68
185
  const args = [
69
186
  "-p",
70
- prompt,
187
+ request.prompt,
71
188
  "--output-format",
72
189
  "json",
73
190
  "--permission-mode",
74
- "acceptEdits",
191
+ "default",
75
192
  "--no-session-persistence",
193
+ "--max-turns",
194
+ String(request.maxTurns),
76
195
  "--setting-sources",
77
196
  "",
78
197
  "--no-chrome",
79
198
  "--disable-slash-commands",
80
199
  "--tools",
81
- "Read,Write,Edit,Glob,Grep",
200
+ toolPolicy === "none" ? "" : "Read,Glob,Grep",
201
+ "--effort",
202
+ request.route.effort ?? DEFAULT_AGENT_EFFORT,
82
203
  ];
83
- const allowedTools = ["Read", "Write", "Edit", "Glob", "Grep"];
84
- if (brokerUrl) {
85
- const mcpConfigPath = join(capsuleRoot, "research-mcp.json");
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");
86
210
  await writeFile(mcpConfigPath, `${JSON.stringify({
87
211
  mcpServers: {
88
- research_broker: { type: "http", url: brokerUrl },
212
+ research_broker: { type: "http", url: request.brokerUrl },
89
213
  },
90
214
  })}\n`, { encoding: "utf8", mode: 0o600 });
91
215
  allowedTools.push("mcp__research_broker__fetch_candidate_source");
92
216
  args.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
93
217
  }
94
218
  args.push("--allowedTools", allowedTools.join(","));
95
- if (route.model)
96
- args.push("--model", route.model);
97
- return { binary: route.binary, args };
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 };
98
224
  }
99
- async function sandboxInvocation(binary, args, capsuleRoot, projectRoot, workspaceRoot) {
225
+ async function sandboxInvocation(binary, args, capsuleRoot, projectRoot, workspaceRoot, targetBinary) {
100
226
  const configuredCredentialPath = join(workspaceRoot, ".tiangong-research", ".env");
101
227
  const workspaceCredentialPath = await realpath(configuredCredentialPath).catch(() => configuredCredentialPath);
102
228
  if (platform() === "darwin") {
103
229
  const sandbox = "/usr/bin/sandbox-exec";
104
230
  await requireExecutable(sandbox, "macOS sandbox-exec");
105
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(" ");
106
235
  const policy = [
107
236
  "(version 1)",
108
237
  "(deny default)",
109
238
  "(allow process*)",
110
239
  "(allow network*)",
111
- `(allow file-read* (require-not (literal ${sandboxString(workspaceCredentialPath)})))`,
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"))',
112
244
  "(allow sysctl-read)",
113
245
  "(allow mach-lookup)",
114
246
  "(allow ipc-posix*)",
115
- `(allow file-write* (literal "/dev/null") (subpath ${sandboxString(capsuleRoot)}))`,
247
+ `(allow file-write* (literal "/dev/dtracehelper") (literal "/dev/null") (subpath ${sandboxString(canonicalCapsuleRoot)}))`,
116
248
  "",
117
249
  ].join("\n");
118
250
  await writeFile(profile, policy, { encoding: "utf8", mode: 0o600 });
@@ -130,44 +262,244 @@ async function sandboxInvocation(binary, args, capsuleRoot, projectRoot, workspa
130
262
  exitCode: 3,
131
263
  });
132
264
  }
133
- const sandboxArgs = [
134
- "--die-with-parent",
135
- "--new-session",
136
- "--ro-bind",
137
- "/",
138
- "/",
139
- "--bind",
140
- capsuleRoot,
141
- capsuleRoot,
142
- "--proc",
143
- "/proc",
144
- "--dev",
145
- "/dev",
146
- "--chdir",
147
- projectRoot,
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),
148
275
  ];
149
- if (await pathIsReadable(workspaceCredentialPath)) {
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))) {
150
289
  sandboxArgs.push("--ro-bind", "/dev/null", workspaceCredentialPath);
151
290
  }
152
291
  sandboxArgs.push(binary, ...args);
153
- return {
154
- binary: bubblewrap,
155
- args: sandboxArgs,
156
- };
292
+ return { binary: bubblewrap, args: sandboxArgs };
157
293
  }
158
294
  throw new CliError(`Unsupported research execution platform: ${platform()}`, {
159
295
  code: "RESEARCH_SANDBOX_UNAVAILABLE",
160
296
  exitCode: 3,
161
297
  });
162
298
  }
163
- async function pathIsReadable(path) {
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;
164
361
  try {
165
- await access(path, fsConstants.R_OK);
166
- return true;
362
+ value = JSON.parse(await readFile(path, "utf8"));
167
363
  }
168
364
  catch {
169
- return false;
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
+ }
170
498
  }
499
+ throw new CliError(`Configured executable is unavailable: ${binary}`, {
500
+ code: "RESEARCH_EXECUTOR_UNAVAILABLE",
501
+ exitCode: 3,
502
+ });
171
503
  }
172
504
  async function spawnCaptured(input) {
173
505
  return new Promise((resolvePromise, reject) => {
@@ -181,6 +513,7 @@ async function spawnCaptured(input) {
181
513
  const stdout = [];
182
514
  const stderr = [];
183
515
  let capturedBytes = 0;
516
+ let captureExceeded = false;
184
517
  let settled = false;
185
518
  const terminate = () => {
186
519
  if (!child.pid)
@@ -198,7 +531,8 @@ async function spawnCaptured(input) {
198
531
  const timer = setTimeout(() => terminate(), input.timeoutMs);
199
532
  const capture = (target, chunk) => {
200
533
  capturedBytes += chunk.length;
201
- if (capturedBytes > MAX_CAPTURE_BYTES) {
534
+ if (capturedBytes > input.maxCaptureBytes) {
535
+ captureExceeded = true;
202
536
  terminate();
203
537
  return;
204
538
  }
@@ -223,25 +557,50 @@ async function spawnCaptured(input) {
223
557
  exitCode: typeof code === "number" ? code : 70,
224
558
  stdout: Buffer.concat(stdout).toString("utf8"),
225
559
  stderr: `${Buffer.concat(stderr).toString("utf8")}${detail}`,
560
+ captureExceeded,
226
561
  });
227
562
  });
228
563
  });
229
564
  }
230
- function parseAgentResult(agent, stdout, stderr) {
231
- if (agent === "codex") {
232
- let tokens = 0;
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;
233
571
  const messages = [];
234
572
  let parsedEvents = 0;
573
+ const eventCounts = {};
574
+ const itemCounts = {};
575
+ let toolCalls = 0;
576
+ let reasoningOutputTokens = 0;
577
+ const providerErrors = [];
235
578
  for (const line of stdout.split(/\r?\n/)) {
236
579
  if (!line.trim())
237
580
  continue;
238
581
  try {
239
582
  const event = JSON.parse(line);
240
583
  parsedEvents += 1;
241
- if (event.type === "turn.completed" && isRecord(event.usage)) {
242
- tokens = numeric(event.usage.input_tokens) + numeric(event.usage.output_tokens);
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);
243
593
  }
244
- if (event.type === "item.completed" && isRecord(event.item)) {
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
+ }
245
604
  if (event.item.type === "agent_message" && typeof event.item.text === "string") {
246
605
  messages.push(event.item.text);
247
606
  }
@@ -252,63 +611,217 @@ function parseAgentResult(agent, stdout, stderr) {
252
611
  }
253
612
  }
254
613
  return {
255
- stdout: messages.length ? messages.join("\n") : stdout,
614
+ stdout: messages.at(-1) ?? stdout,
256
615
  stderr,
257
- tokens,
616
+ inputTokens,
617
+ cachedInputTokens,
618
+ outputTokens,
258
619
  costUsd: 0,
259
- parseFailed: parsedEvents === 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
+ },
260
630
  };
261
631
  }
262
632
  try {
263
633
  const value = JSON.parse(stdout);
264
- const usage = isRecord(value.usage) ? value.usage : {};
634
+ const usage = isObject(value.usage) ? value.usage : {};
265
635
  return {
266
636
  stdout: typeof value.result === "string" ? value.result : stdout,
267
637
  stderr,
268
- tokens: numeric(usage.input_tokens) +
269
- numeric(usage.output_tokens) +
270
- numeric(usage.cache_creation_input_tokens) +
271
- numeric(usage.cache_read_input_tokens),
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),
272
641
  costUsd: numeric(value.total_cost_usd),
273
- parseFailed: false,
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
+ },
274
652
  };
275
653
  }
276
654
  catch {
277
- return { stdout, stderr, tokens: 0, costUsd: 0, parseFailed: true };
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
+ };
278
673
  }
279
674
  }
280
- function sanitizedEnvironment(source, temporaryDirectory) {
675
+ function sanitizedEnvironment(source, temporaryDirectory, capsuleHome, route, targetBinary, capsuleAuthEnvironment) {
281
676
  const environment = {};
282
677
  for (const [name, value] of Object.entries(source)) {
283
- if (!value || CREDENTIAL_ENV_NAME.test(name))
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) {
284
684
  continue;
685
+ }
285
686
  environment[name] = value;
286
687
  }
287
- environment.HOME = homedir();
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
+ }
288
708
  environment.NO_COLOR = "1";
709
+ if (route.wrapperTargetBinary)
710
+ environment[WRAPPER_TARGET_ENV] = targetBinary;
289
711
  environment.TMPDIR = temporaryDirectory;
290
712
  environment.TMP = temporaryDirectory;
291
713
  environment.TEMP = temporaryDirectory;
292
714
  return environment;
293
715
  }
294
- function configuredSecrets(source) {
295
- return [
296
- ...new Set(Object.entries(source)
297
- .filter(([name, value]) => CREDENTIAL_ENV_NAME.test(name) && typeof value === "string" && value.length >= 8)
298
- .map(([, value]) => value)),
299
- ].sort((left, right) => right.length - left.length);
300
- }
301
716
  function redactResult(stdout, stderr, secrets) {
302
- let safeStdout = stdout;
303
- let safeStderr = stderr;
304
717
  let exposed = false;
305
718
  for (const secret of secrets) {
306
- if (safeStdout.includes(secret) || safeStderr.includes(secret))
719
+ if (stdout.includes(secret) || stderr.includes(secret))
307
720
  exposed = true;
308
- safeStdout = safeStdout.replaceAll(secret, "[REDACTED]");
309
- safeStderr = safeStderr.replaceAll(secret, "[REDACTED]");
310
721
  }
311
- return { stdout: safeStdout, stderr: safeStderr, exposed };
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
+ }
312
825
  }
313
826
  function validateAgentBinary(route) {
314
827
  if (!isAbsolute(route.binary) && route.binary !== route.agent) {
@@ -317,6 +830,19 @@ function validateAgentBinary(route) {
317
830
  exitCode: 2,
318
831
  });
319
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
+ }
320
846
  }
321
847
  async function requireExecutable(path, label) {
322
848
  try {
@@ -344,10 +870,57 @@ async function firstExecutable(candidates) {
344
870
  function sandboxString(value) {
345
871
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
346
872
  }
347
- function isRecord(value) {
348
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
349
- }
350
873
  function numeric(value) {
351
874
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
352
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
+ }
353
926
  //# sourceMappingURL=executor.js.map