@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.
- package/AGENTS.md +3 -2
- package/README.md +158 -13
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/research/orchestration.js +166 -11
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/broker.js +407 -31
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/capabilities.js +65 -0
- package/dist/research/workspace/capabilities.js.map +1 -1
- package/dist/research/workspace/constants.d.ts +1 -1
- package/dist/research/workspace/constants.js +4 -0
- package/dist/research/workspace/constants.js.map +1 -1
- package/dist/research/workspace/evidence.d.ts +32 -0
- package/dist/research/workspace/evidence.js +235 -0
- package/dist/research/workspace/evidence.js.map +1 -0
- package/dist/research/workspace/executor.d.ts +11 -1
- package/dist/research/workspace/executor.js +678 -105
- package/dist/research/workspace/executor.js.map +1 -1
- package/dist/research/workspace/input-plan.d.ts +5 -0
- package/dist/research/workspace/input-plan.js +319 -0
- package/dist/research/workspace/input-plan.js.map +1 -0
- package/dist/research/workspace/journal.js +2 -1
- package/dist/research/workspace/journal.js.map +1 -1
- package/dist/research/workspace/preflight.d.ts +108 -0
- package/dist/research/workspace/preflight.js +261 -0
- package/dist/research/workspace/preflight.js.map +1 -0
- package/dist/research/workspace/projects.d.ts +5 -2
- package/dist/research/workspace/projects.js +290 -6
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +5 -1
- package/dist/research/workspace/runtime.js +1314 -196
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.d.ts +5 -0
- package/dist/research/workspace/sanitization.js +72 -0
- package/dist/research/workspace/sanitization.js.map +1 -0
- package/dist/research/workspace/schemas.d.ts +17 -0
- package/dist/research/workspace/schemas.js +342 -0
- package/dist/research/workspace/schemas.js.map +1 -0
- package/dist/research/workspace/storage.js +4 -0
- package/dist/research/workspace/storage.js.map +1 -1
- package/dist/research/workspace/types.d.ts +160 -0
- package/dist/research/workspace/workspace.d.ts +14 -3
- package/dist/research/workspace/workspace.js +374 -12
- package/dist/research/workspace/workspace.js.map +1 -1
- 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 {
|
|
5
|
-
import {
|
|
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
|
|
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
|
|
14
|
-
const
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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.
|
|
30
|
-
?
|
|
31
|
-
:
|
|
32
|
-
?
|
|
33
|
-
:
|
|
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:
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
"
|
|
164
|
+
"read-only",
|
|
56
165
|
"-c",
|
|
57
166
|
'web_search="disabled"',
|
|
167
|
+
"--output-schema",
|
|
168
|
+
outputSchemaPath,
|
|
58
169
|
"--json",
|
|
59
170
|
];
|
|
60
|
-
|
|
61
|
-
args.push("
|
|
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:
|
|
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
|
-
"
|
|
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,
|
|
200
|
+
toolPolicy === "none" ? "" : "Read,Glob,Grep",
|
|
201
|
+
"--effort",
|
|
202
|
+
request.route.effort ?? DEFAULT_AGENT_EFFORT,
|
|
82
203
|
];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 (
|
|
96
|
-
args.push("--
|
|
97
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
"--ro-bind",
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
166
|
-
return true;
|
|
362
|
+
value = JSON.parse(await readFile(path, "utf8"));
|
|
167
363
|
}
|
|
168
364
|
catch {
|
|
169
|
-
|
|
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 >
|
|
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(
|
|
231
|
-
if (agent === "codex") {
|
|
232
|
-
let
|
|
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 === "
|
|
242
|
-
|
|
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.
|
|
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.
|
|
614
|
+
stdout: messages.at(-1) ?? stdout,
|
|
256
615
|
stderr,
|
|
257
|
-
|
|
616
|
+
inputTokens,
|
|
617
|
+
cachedInputTokens,
|
|
618
|
+
outputTokens,
|
|
258
619
|
costUsd: 0,
|
|
259
|
-
|
|
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 =
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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 {
|
|
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 ||
|
|
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
|
-
|
|
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 (
|
|
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 {
|
|
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
|