@mono-agent/agent-runtime 0.15.3 → 0.16.0
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/MIGRATION.md +41 -13
- package/README.md +43 -6
- package/package.json +7 -3
- package/src/agent/tools/agent-tool.js +894 -0
- package/src/agent/tools/bash.js +241 -123
- package/src/agent/tools/exec.js +238 -0
- package/src/agent/tools/index.js +10 -3
- package/src/agent/tools/node-repl.js +231 -95
- package/src/agent/tools/pi-bridge.js +115 -24
- package/src/agent/tools/shared/process-runner.js +162 -0
- package/src/agent/tools/shared/semaphore.js +73 -0
- package/src/agent/tools/web-browser-render.js +221 -0
- package/src/agent/tools/web-controller.js +160 -0
- package/src/agent/tools/web-fetch.js +653 -68
- package/src/agent/tools/web-search.js +568 -16
- package/src/ai/pi-interop.js +7 -5
- package/src/ai/pi-oauth-compat.js +193 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +37 -0
- package/src/ai/providers/pi-native/turn-runner.js +73 -8
- package/src/ai/providers/pi-native.js +67 -7
- package/src/ai/runtime/router.js +310 -166
- package/src/ai/types.js +54 -2
- package/src/pi-auth.js +2 -2
- package/src/runtime.js +58 -1
- package/types/agent/tools/agent-tool.d.ts +80 -0
- package/types/agent/tools/bash.d.ts +55 -7
- package/types/agent/tools/exec.d.ts +53 -0
- package/types/agent/tools/index.d.ts +5 -3
- package/types/agent/tools/node-repl.d.ts +28 -3
- package/types/agent/tools/pi-bridge.d.ts +6 -2
- package/types/agent/tools/shared/process-runner.d.ts +33 -0
- package/types/agent/tools/shared/semaphore.d.ts +29 -0
- package/types/agent/tools/web-browser-render.d.ts +16 -0
- package/types/agent/tools/web-controller.d.ts +20 -0
- package/types/agent/tools/web-fetch.d.ts +74 -5
- package/types/agent/tools/web-search.d.ts +81 -5
- package/types/ai/pi-oauth-compat.d.ts +57 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +33 -2
- package/types/ai/providers/pi-native.d.ts +12 -0
- package/types/ai/runtime/router.d.ts +23 -3
- package/types/ai/types.d.ts +174 -4
- package/types/ai/backend.d.ts +0 -57
- package/types/ai/registry.d.ts +0 -1
package/src/agent/tools/bash.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { passthroughSandbox } from "../sandbox-seam.js";
|
|
4
5
|
import { DEFAULT_MAX_BASH_OUTPUT_CHARS } from "./shared/constants.js";
|
|
@@ -8,157 +9,274 @@ import {
|
|
|
8
9
|
isWorkdirAllowed,
|
|
9
10
|
workspaceRoot,
|
|
10
11
|
} from "./shared/path-resolver.js";
|
|
12
|
+
import {
|
|
13
|
+
combinedProcessOutput,
|
|
14
|
+
DEFAULT_PROCESS_BUFFER_BYTES,
|
|
15
|
+
runPreparedProcess,
|
|
16
|
+
} from "./shared/process-runner.js";
|
|
11
17
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
12
18
|
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
13
19
|
|
|
14
|
-
const DEFAULT_BASH_TIMEOUT_MS =
|
|
15
|
-
const
|
|
16
|
-
|
|
20
|
+
const DEFAULT_BASH_TIMEOUT_MS = 120_000;
|
|
21
|
+
const BASH_STARTUP_ENV_KEYS = new Set([
|
|
22
|
+
"BASHOPTS",
|
|
23
|
+
"BASH_COMPAT",
|
|
24
|
+
"BASH_XTRACEFD",
|
|
25
|
+
"CDPATH",
|
|
26
|
+
"GLOBIGNORE",
|
|
27
|
+
"POSIXLY_CORRECT",
|
|
28
|
+
"PROMPT_COMMAND",
|
|
29
|
+
"PS4",
|
|
30
|
+
"SHELLOPTS",
|
|
31
|
+
]);
|
|
17
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Legacy Bash timeout normalization. Values up to 600 are seconds; larger
|
|
35
|
+
* values are milliseconds. New callers should use `timeout_ms`.
|
|
36
|
+
*/
|
|
18
37
|
export function normalizeBashTimeoutMs(value, fallback = DEFAULT_BASH_TIMEOUT_MS) {
|
|
19
|
-
const cap =
|
|
20
|
-
? Math.floor(Number(fallback))
|
|
21
|
-
: DEFAULT_BASH_TIMEOUT_MS;
|
|
38
|
+
const cap = finitePositiveInteger(fallback, DEFAULT_BASH_TIMEOUT_MS);
|
|
22
39
|
const n = Number(value);
|
|
23
40
|
if (!Number.isFinite(n) || n <= 0) return cap;
|
|
24
41
|
const floored = Math.floor(n);
|
|
25
|
-
const
|
|
26
|
-
return Math.max(
|
|
42
|
+
const milliseconds = floored <= 600 ? floored * 1_000 : floored;
|
|
43
|
+
return Math.max(1_000, Math.min(milliseconds, cap));
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
function appendChunk(chunks, chunk, state) {
|
|
39
|
-
state.bytes += chunk.length;
|
|
40
|
-
if (state.bytes > state.maxBufferBytes) {
|
|
41
|
-
state.bufferExceeded = true;
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
chunks.push(chunk);
|
|
45
|
-
return true;
|
|
46
|
+
/**
|
|
47
|
+
* Exact millisecond timeout used by Bash.timeout_ms and Exec.timeout_ms.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeProcessTimeoutMs(value, fallback = DEFAULT_BASH_TIMEOUT_MS) {
|
|
50
|
+
const cap = finitePositiveInteger(fallback, DEFAULT_BASH_TIMEOUT_MS);
|
|
51
|
+
const n = Number(value);
|
|
52
|
+
if (!Number.isFinite(n) || n <= 0) return cap;
|
|
53
|
+
return Math.max(1, Math.min(Math.floor(n), cap));
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const stdout = [];
|
|
57
|
-
const stderr = [];
|
|
58
|
-
const state = {
|
|
59
|
-
aborted: false,
|
|
60
|
-
bufferExceeded: false,
|
|
61
|
-
bytes: 0,
|
|
62
|
-
maxBufferBytes,
|
|
63
|
-
spawnError: null,
|
|
64
|
-
timedOut: false,
|
|
65
|
-
};
|
|
66
|
-
let killTimer = null;
|
|
67
|
-
let settled = false;
|
|
68
|
-
|
|
69
|
-
function terminate() {
|
|
70
|
-
killProcessGroup(child, "SIGTERM");
|
|
71
|
-
if (!killTimer) {
|
|
72
|
-
killTimer = setTimeout(() => killProcessGroup(child, "SIGKILL"), KILL_GRACE_MS);
|
|
73
|
-
killTimer.unref?.();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const timeoutTimer = setTimeout(() => {
|
|
78
|
-
state.timedOut = true;
|
|
79
|
-
terminate();
|
|
80
|
-
}, timeoutMs);
|
|
81
|
-
timeoutTimer.unref?.();
|
|
82
|
-
|
|
83
|
-
const onAbort = () => {
|
|
84
|
-
state.aborted = true;
|
|
85
|
-
terminate();
|
|
86
|
-
};
|
|
87
|
-
if (signal?.aborted) onAbort();
|
|
88
|
-
else signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
89
|
-
|
|
90
|
-
child.stdout?.on("data", (chunk) => {
|
|
91
|
-
if (!appendChunk(stdout, chunk, state)) terminate();
|
|
92
|
-
});
|
|
93
|
-
child.stderr?.on("data", (chunk) => {
|
|
94
|
-
if (!appendChunk(stderr, chunk, state)) terminate();
|
|
95
|
-
});
|
|
96
|
-
child.once("error", (err) => {
|
|
97
|
-
state.spawnError = err;
|
|
98
|
-
});
|
|
99
|
-
child.once("close", (code, closeSignal) => {
|
|
100
|
-
if (settled) return;
|
|
101
|
-
settled = true;
|
|
102
|
-
clearTimeout(timeoutTimer);
|
|
103
|
-
if (killTimer) clearTimeout(killTimer);
|
|
104
|
-
signal?.removeEventListener?.("abort", onAbort);
|
|
105
|
-
resolve({
|
|
106
|
-
code,
|
|
107
|
-
signal: closeSignal,
|
|
108
|
-
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
109
|
-
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
110
|
-
...state,
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
});
|
|
56
|
+
/**
|
|
57
|
+
* Compatibility wrapper retained for direct callers and tests.
|
|
58
|
+
*
|
|
59
|
+
* @param {{command: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string}} params
|
|
60
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
61
|
+
*/
|
|
62
|
+
export async function bashToolImpl(params, options = {}) {
|
|
63
|
+
return (await bashToolRun(params, options)).text;
|
|
114
64
|
}
|
|
115
65
|
|
|
116
66
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
67
|
+
* Structured Bash execution used by the Pi bridge.
|
|
68
|
+
*
|
|
69
|
+
* @param {{command: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string}} params
|
|
70
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
119
71
|
*/
|
|
120
|
-
export async function
|
|
72
|
+
export async function bashToolRun(
|
|
73
|
+
{
|
|
74
|
+
command,
|
|
75
|
+
timeout,
|
|
76
|
+
timeout_ms,
|
|
77
|
+
max_output_chars,
|
|
78
|
+
workdir,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
signal,
|
|
82
|
+
sandboxPolicy,
|
|
83
|
+
sandboxEngine,
|
|
84
|
+
ctx,
|
|
85
|
+
} = {},
|
|
86
|
+
) {
|
|
87
|
+
const startedAt = Date.now();
|
|
88
|
+
if (typeof command !== "string") {
|
|
89
|
+
return failed("Error: Bash command must be a string.", "invalid_command", startedAt);
|
|
90
|
+
}
|
|
91
|
+
if (command.includes("\0")) {
|
|
92
|
+
return failed("Error: Bash command must not contain NUL characters.", "invalid_command", startedAt);
|
|
93
|
+
}
|
|
121
94
|
const resolvedCtx = ctx ?? readToolRuntime();
|
|
122
95
|
const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
|
|
123
96
|
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
124
|
-
const pathOptions = { sandboxPolicy: policy, ctx };
|
|
125
|
-
if (workdir && !isWorkdirAllowed(workdir, pathOptions))
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
97
|
+
const pathOptions = { sandboxPolicy: policy, ctx: resolvedCtx };
|
|
98
|
+
if (workdir && !isWorkdirAllowed(workdir, pathOptions)) {
|
|
99
|
+
return failed(`Error: Working directory not allowed: ${workdir}`, "workdir_denied", startedAt);
|
|
100
|
+
}
|
|
101
|
+
const cwd = workspaceRoot(workdir, resolvedCtx);
|
|
102
|
+
if (!isPathAllowed(cwd, workdir, pathOptions)) {
|
|
103
|
+
return failed(`Error: Working directory not allowed: ${cwd}`, "workdir_denied", startedAt);
|
|
104
|
+
}
|
|
105
|
+
if (!existsSync(cwd)) {
|
|
106
|
+
return failed(`Error: Working directory not found: ${cwd}`, "workdir_not_found", startedAt);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const maxChars = finitePositiveInteger(max_output_chars, DEFAULT_MAX_BASH_OUTPUT_CHARS);
|
|
110
|
+
const legacyTimeoutUsed = timeout_ms === undefined && timeout !== undefined;
|
|
111
|
+
const timeoutMs = timeout_ms === undefined
|
|
112
|
+
? normalizeBashTimeoutMs(timeout, DEFAULT_BASH_TIMEOUT_MS)
|
|
113
|
+
: normalizeProcessTimeoutMs(timeout_ms, DEFAULT_BASH_TIMEOUT_MS);
|
|
131
114
|
let prepared;
|
|
132
115
|
try {
|
|
133
116
|
prepared = await sandbox.prepareCommand({
|
|
134
117
|
policy,
|
|
135
|
-
engine: sandboxEngine ?? undefined,
|
|
136
|
-
command: {
|
|
118
|
+
engine: sandboxEngine ?? resolvedCtx.sandboxEngine ?? undefined,
|
|
119
|
+
command: {
|
|
120
|
+
command: "/bin/bash",
|
|
121
|
+
args: ["--noprofile", "--norc", "-c", command],
|
|
122
|
+
cwd,
|
|
123
|
+
env: cleanBashEnvironment(),
|
|
124
|
+
},
|
|
137
125
|
});
|
|
138
|
-
} catch (
|
|
139
|
-
return `Error: ${
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return failed(`Error: ${error?.message || String(error)}`, "sandbox_prepare_failed", startedAt);
|
|
140
128
|
}
|
|
129
|
+
|
|
141
130
|
let result;
|
|
131
|
+
let cleanupError;
|
|
142
132
|
try {
|
|
143
|
-
result = await
|
|
133
|
+
result = await runPreparedProcess(prepared, {
|
|
134
|
+
timeoutMs,
|
|
135
|
+
signal,
|
|
136
|
+
maxBufferBytes: DEFAULT_PROCESS_BUFFER_BYTES,
|
|
137
|
+
});
|
|
144
138
|
} finally {
|
|
145
|
-
|
|
139
|
+
try {
|
|
140
|
+
await prepared.cleanup?.();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
cleanupError = error;
|
|
143
|
+
}
|
|
146
144
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
|
|
146
|
+
const baseOutcome = processOutcome(result, {
|
|
147
|
+
status: "ok",
|
|
148
|
+
code: "ok",
|
|
149
|
+
legacyTimeoutUsed,
|
|
150
|
+
});
|
|
151
|
+
const partial = combinedProcessOutput(result);
|
|
152
|
+
if (result.timedOut) {
|
|
153
|
+
return failedWithOutcome(
|
|
154
|
+
withPartial(`Error: Command timed out after ${timeoutMs}ms`, partial),
|
|
155
|
+
{ ...baseOutcome, status: "error", code: "timeout", retryable: false, timedOut: true },
|
|
154
156
|
maxChars,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
resolvedCtx,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (result.aborted) {
|
|
161
|
+
return failedWithOutcome(
|
|
162
|
+
withPartial("Error: Command aborted", partial),
|
|
163
|
+
{ ...baseOutcome, status: "error", code: "aborted", retryable: false },
|
|
164
|
+
maxChars,
|
|
165
|
+
resolvedCtx,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (result.bufferExceeded) {
|
|
169
|
+
return failedWithOutcome(
|
|
170
|
+
withPartial(`Error: Command output exceeded ${DEFAULT_PROCESS_BUFFER_BYTES} bytes`, partial),
|
|
171
|
+
{ ...baseOutcome, status: "error", code: "output_limit", retryable: false, truncated: true },
|
|
172
|
+
maxChars,
|
|
173
|
+
resolvedCtx,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (result.spawnError) {
|
|
177
|
+
return failedWithOutcome(
|
|
178
|
+
withPartial(`Exit code 1:\n${result.spawnError.message}`, partial),
|
|
179
|
+
{ ...baseOutcome, status: "error", code: "spawn_error", retryable: false, exitCode: 1 },
|
|
180
|
+
maxChars,
|
|
181
|
+
resolvedCtx,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (result.code !== null && result.code !== 0) {
|
|
185
|
+
return failedWithOutcome(
|
|
186
|
+
withPartial(`Exit code ${result.code}`, partial),
|
|
187
|
+
{ ...baseOutcome, status: "error", code: "nonzero_exit", retryable: false },
|
|
188
|
+
maxChars,
|
|
189
|
+
resolvedCtx,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (result.signal) {
|
|
193
|
+
return failedWithOutcome(
|
|
194
|
+
withPartial(`Exit code 1:\nCommand terminated by ${result.signal}`, partial),
|
|
195
|
+
{ ...baseOutcome, status: "error", code: "signal", retryable: false, exitCode: 1 },
|
|
196
|
+
maxChars,
|
|
197
|
+
resolvedCtx,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (cleanupError) {
|
|
201
|
+
return failedWithOutcome(
|
|
202
|
+
withPartial(`Error: Sandbox cleanup failed: ${cleanupError?.message || String(cleanupError)}`, partial),
|
|
203
|
+
{ ...baseOutcome, status: "error", code: "cleanup_failed", retryable: false },
|
|
204
|
+
maxChars,
|
|
205
|
+
resolvedCtx,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return completed(partial, baseOutcome, maxChars, "Bash", resolvedCtx);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cleanBashEnvironment() {
|
|
213
|
+
const env = {
|
|
214
|
+
BASH_ENV: "/dev/null",
|
|
215
|
+
ENV: "/dev/null",
|
|
216
|
+
};
|
|
217
|
+
for (const key of Object.keys(process.env)) {
|
|
218
|
+
if (BASH_STARTUP_ENV_KEYS.has(key) || key.startsWith("BASH_FUNC_")) {
|
|
219
|
+
env[key] = undefined;
|
|
220
|
+
}
|
|
158
221
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
222
|
+
return env;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function finitePositiveInteger(value, fallback) {
|
|
226
|
+
const number = Number(value);
|
|
227
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function processOutcome(result, extra = {}) {
|
|
231
|
+
return {
|
|
232
|
+
status: "ok",
|
|
233
|
+
code: "ok",
|
|
234
|
+
retryable: false,
|
|
235
|
+
attempts: 1,
|
|
236
|
+
durationMs: Number(result.durationMs) || 0,
|
|
237
|
+
bytes: Number(result.bytes) || 0,
|
|
238
|
+
truncated: !!result.truncated,
|
|
239
|
+
exitCode: result.code,
|
|
240
|
+
signal: result.signal,
|
|
241
|
+
timedOut: !!result.timedOut,
|
|
242
|
+
...extra,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function withPartial(message, output) {
|
|
247
|
+
return output && output !== "(no output)" ? `${message}\n\nPartial output:\n${output}` : message;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function completed(text, outcome, maxChars, label, ctx) {
|
|
251
|
+
const raw = String(text || "(no output)");
|
|
252
|
+
const truncated = raw.length > maxChars || outcome.truncated;
|
|
253
|
+
return {
|
|
254
|
+
text: capChars(raw, { label, maxChars, strategy: "head_tail", ctx }),
|
|
255
|
+
outcome: { ...outcome, truncated },
|
|
256
|
+
error: false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function failed(text, code, startedAt) {
|
|
261
|
+
return {
|
|
262
|
+
text,
|
|
263
|
+
outcome: {
|
|
264
|
+
status: "error",
|
|
265
|
+
code,
|
|
266
|
+
retryable: false,
|
|
267
|
+
attempts: 1,
|
|
268
|
+
durationMs: Date.now() - startedAt,
|
|
269
|
+
bytes: 0,
|
|
270
|
+
truncated: false,
|
|
271
|
+
exitCode: null,
|
|
272
|
+
signal: null,
|
|
273
|
+
timedOut: false,
|
|
274
|
+
},
|
|
275
|
+
error: true,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function failedWithOutcome(text, outcome, maxChars, ctx) {
|
|
280
|
+
const completedResult = completed(text, outcome, maxChars, "Bash", ctx);
|
|
281
|
+
return { ...completedResult, error: true };
|
|
164
282
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { passthroughSandbox } from "../sandbox-seam.js";
|
|
5
|
+
import { DEFAULT_MAX_BASH_OUTPUT_CHARS } from "./shared/constants.js";
|
|
6
|
+
import { normalizeProcessTimeoutMs } from "./bash.js";
|
|
7
|
+
import { capChars } from "./shared/output-truncation.js";
|
|
8
|
+
import {
|
|
9
|
+
isPathAllowed,
|
|
10
|
+
isWorkdirAllowed,
|
|
11
|
+
workspaceRoot,
|
|
12
|
+
} from "./shared/path-resolver.js";
|
|
13
|
+
import {
|
|
14
|
+
combinedProcessOutput,
|
|
15
|
+
DEFAULT_PROCESS_BUFFER_BYTES,
|
|
16
|
+
runPreparedProcess,
|
|
17
|
+
} from "./shared/process-runner.js";
|
|
18
|
+
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
19
|
+
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_EXEC_TIMEOUT_MS = 120_000;
|
|
22
|
+
const MAX_EXEC_ARGS = 256;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{executable: string, args?: string[], workdir?: string, timeout_ms?: number, max_output_chars?: number}} params
|
|
26
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
27
|
+
*/
|
|
28
|
+
export async function execToolImpl(params, options = {}) {
|
|
29
|
+
return (await execToolRun(params, options)).text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Execute an argv vector directly, without shell parsing.
|
|
34
|
+
*
|
|
35
|
+
* @param {{executable: string, args?: string[], workdir?: string, timeout_ms?: number, max_output_chars?: number}} params
|
|
36
|
+
* @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
37
|
+
*/
|
|
38
|
+
export async function execToolRun(
|
|
39
|
+
{
|
|
40
|
+
executable,
|
|
41
|
+
args = [],
|
|
42
|
+
workdir,
|
|
43
|
+
timeout_ms,
|
|
44
|
+
max_output_chars,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
signal,
|
|
48
|
+
sandboxPolicy,
|
|
49
|
+
sandboxEngine,
|
|
50
|
+
ctx,
|
|
51
|
+
} = {},
|
|
52
|
+
) {
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
const executableProblem = validateExecutable(executable);
|
|
55
|
+
if (executableProblem) return failed(executableProblem, "invalid_executable", startedAt);
|
|
56
|
+
const argsProblem = validateArgs(args);
|
|
57
|
+
if (argsProblem) return failed(argsProblem, "invalid_args", startedAt);
|
|
58
|
+
|
|
59
|
+
const resolvedCtx = ctx ?? readToolRuntime();
|
|
60
|
+
const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
|
|
61
|
+
const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
|
|
62
|
+
const pathOptions = { sandboxPolicy: policy, ctx: resolvedCtx };
|
|
63
|
+
if (workdir && !isWorkdirAllowed(workdir, pathOptions)) {
|
|
64
|
+
return failed(`Error: Working directory not allowed: ${workdir}`, "workdir_denied", startedAt);
|
|
65
|
+
}
|
|
66
|
+
const cwd = workspaceRoot(workdir, resolvedCtx);
|
|
67
|
+
if (!isPathAllowed(cwd, workdir, pathOptions)) {
|
|
68
|
+
return failed(`Error: Working directory not allowed: ${cwd}`, "workdir_denied", startedAt);
|
|
69
|
+
}
|
|
70
|
+
if (!existsSync(cwd)) {
|
|
71
|
+
return failed(`Error: Working directory not found: ${cwd}`, "workdir_not_found", startedAt);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const timeoutMs = normalizeProcessTimeoutMs(timeout_ms, DEFAULT_EXEC_TIMEOUT_MS);
|
|
75
|
+
const maxChars = positiveInteger(max_output_chars, DEFAULT_MAX_BASH_OUTPUT_CHARS);
|
|
76
|
+
let prepared;
|
|
77
|
+
try {
|
|
78
|
+
prepared = await sandbox.prepareCommand({
|
|
79
|
+
policy,
|
|
80
|
+
engine: sandboxEngine ?? resolvedCtx.sandboxEngine ?? undefined,
|
|
81
|
+
command: {
|
|
82
|
+
command: executable,
|
|
83
|
+
args: [...args],
|
|
84
|
+
cwd,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return failed(`Error: ${error?.message || String(error)}`, "sandbox_prepare_failed", startedAt);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let result;
|
|
92
|
+
let cleanupError;
|
|
93
|
+
try {
|
|
94
|
+
result = await runPreparedProcess(prepared, {
|
|
95
|
+
timeoutMs,
|
|
96
|
+
signal,
|
|
97
|
+
maxBufferBytes: DEFAULT_PROCESS_BUFFER_BYTES,
|
|
98
|
+
});
|
|
99
|
+
} finally {
|
|
100
|
+
try {
|
|
101
|
+
await prepared.cleanup?.();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
cleanupError = error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const outcome = {
|
|
108
|
+
status: "ok",
|
|
109
|
+
code: "ok",
|
|
110
|
+
retryable: false,
|
|
111
|
+
attempts: 1,
|
|
112
|
+
durationMs: Number(result.durationMs) || 0,
|
|
113
|
+
bytes: Number(result.bytes) || 0,
|
|
114
|
+
truncated: !!result.truncated,
|
|
115
|
+
exitCode: result.code,
|
|
116
|
+
signal: result.signal,
|
|
117
|
+
timedOut: !!result.timedOut,
|
|
118
|
+
};
|
|
119
|
+
const partial = combinedProcessOutput(result);
|
|
120
|
+
if (result.timedOut) {
|
|
121
|
+
return finishError(
|
|
122
|
+
withPartial(`Error: Process timed out after ${timeoutMs}ms`, partial),
|
|
123
|
+
{ ...outcome, status: "error", code: "timeout", timedOut: true },
|
|
124
|
+
maxChars,
|
|
125
|
+
resolvedCtx,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (result.aborted) {
|
|
129
|
+
return finishError(
|
|
130
|
+
withPartial("Error: Process aborted", partial),
|
|
131
|
+
{ ...outcome, status: "error", code: "aborted" },
|
|
132
|
+
maxChars,
|
|
133
|
+
resolvedCtx,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (result.bufferExceeded) {
|
|
137
|
+
return finishError(
|
|
138
|
+
withPartial(`Error: Process output exceeded ${DEFAULT_PROCESS_BUFFER_BYTES} bytes`, partial),
|
|
139
|
+
{ ...outcome, status: "error", code: "output_limit", truncated: true },
|
|
140
|
+
maxChars,
|
|
141
|
+
resolvedCtx,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (result.spawnError) {
|
|
145
|
+
return finishError(
|
|
146
|
+
withPartial(`Exit code 1:\n${result.spawnError.message}`, partial),
|
|
147
|
+
{ ...outcome, status: "error", code: "spawn_error", exitCode: 1 },
|
|
148
|
+
maxChars,
|
|
149
|
+
resolvedCtx,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (result.code !== null && result.code !== 0) {
|
|
153
|
+
return finishError(
|
|
154
|
+
withPartial(`Exit code ${result.code}`, partial),
|
|
155
|
+
{ ...outcome, status: "error", code: "nonzero_exit" },
|
|
156
|
+
maxChars,
|
|
157
|
+
resolvedCtx,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (result.signal) {
|
|
161
|
+
return finishError(
|
|
162
|
+
withPartial(`Exit code 1:\nProcess terminated by ${result.signal}`, partial),
|
|
163
|
+
{ ...outcome, status: "error", code: "signal", exitCode: 1 },
|
|
164
|
+
maxChars,
|
|
165
|
+
resolvedCtx,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (cleanupError) {
|
|
169
|
+
return finishError(
|
|
170
|
+
withPartial(`Error: Sandbox cleanup failed: ${cleanupError?.message || String(cleanupError)}`, partial),
|
|
171
|
+
{ ...outcome, status: "error", code: "cleanup_failed" },
|
|
172
|
+
maxChars,
|
|
173
|
+
resolvedCtx,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return finish(partial, outcome, maxChars, resolvedCtx, false);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function validateExecutable(value) {
|
|
180
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
181
|
+
return "Error: Exec executable must be a non-empty string.";
|
|
182
|
+
}
|
|
183
|
+
if (value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
184
|
+
return "Error: Exec executable contains an invalid character.";
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function validateArgs(value) {
|
|
190
|
+
if (!Array.isArray(value)) return "Error: Exec args must be an array of strings.";
|
|
191
|
+
if (value.length > MAX_EXEC_ARGS) return `Error: Exec accepts at most ${MAX_EXEC_ARGS} arguments.`;
|
|
192
|
+
if (value.some((entry) => typeof entry !== "string" || entry.includes("\0"))) {
|
|
193
|
+
return "Error: Exec args must contain only strings without NUL characters.";
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function positiveInteger(value, fallback) {
|
|
199
|
+
const number = Number(value);
|
|
200
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function withPartial(message, output) {
|
|
204
|
+
return output && output !== "(no output)" ? `${message}\n\nPartial output:\n${output}` : message;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function finish(text, outcome, maxChars, ctx, error) {
|
|
208
|
+
const raw = String(text || "(no output)");
|
|
209
|
+
const truncated = raw.length > maxChars || outcome.truncated;
|
|
210
|
+
return {
|
|
211
|
+
text: capChars(raw, { label: "Exec", maxChars, strategy: "head_tail", ctx }),
|
|
212
|
+
outcome: { ...outcome, truncated },
|
|
213
|
+
error,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function finishError(text, outcome, maxChars, ctx) {
|
|
218
|
+
return finish(text, outcome, maxChars, ctx, true);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function failed(text, code, startedAt) {
|
|
222
|
+
return {
|
|
223
|
+
text,
|
|
224
|
+
outcome: {
|
|
225
|
+
status: "error",
|
|
226
|
+
code,
|
|
227
|
+
retryable: false,
|
|
228
|
+
attempts: 1,
|
|
229
|
+
durationMs: Date.now() - startedAt,
|
|
230
|
+
bytes: 0,
|
|
231
|
+
truncated: false,
|
|
232
|
+
exitCode: null,
|
|
233
|
+
signal: null,
|
|
234
|
+
timedOut: false,
|
|
235
|
+
},
|
|
236
|
+
error: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
package/src/agent/tools/index.js
CHANGED
|
@@ -9,9 +9,16 @@ export { writeToolImpl } from "./write.js";
|
|
|
9
9
|
export { editToolImpl } from "./edit.js";
|
|
10
10
|
export { globToolImpl } from "./glob.js";
|
|
11
11
|
export { grepToolImpl } from "./grep.js";
|
|
12
|
-
export {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export {
|
|
13
|
+
bashToolImpl,
|
|
14
|
+
bashToolRun,
|
|
15
|
+
normalizeBashTimeoutMs,
|
|
16
|
+
normalizeProcessTimeoutMs,
|
|
17
|
+
} from "./bash.js";
|
|
18
|
+
export { execToolImpl, execToolRun } from "./exec.js";
|
|
19
|
+
export { webFetchToolImpl, performWebFetch } from "./web-fetch.js";
|
|
20
|
+
export { webSearchToolImpl, performWebSearch } from "./web-search.js";
|
|
21
|
+
export { createWebToolController } from "./web-controller.js";
|
|
15
22
|
|
|
16
23
|
export { isPathAllowed, isWorkdirAllowed } from "./shared/path-resolver.js";
|
|
17
24
|
export { resolveRgPath } from "./shared/ripgrep.js";
|