@agent-sh/harness-bash 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,1009 @@
1
+ 'use strict';
2
+
3
+ var path4 = require('path');
4
+ var harnessCore = require('@agent-sh/harness-core');
5
+ var child_process = require('child_process');
6
+ var fs = require('fs');
7
+ var os = require('os');
8
+ var crypto = require('crypto');
9
+ var v = require('valibot');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var path4__default = /*#__PURE__*/_interopDefault(path4);
32
+ var v__namespace = /*#__PURE__*/_interopNamespace(v);
33
+
34
+ // src/bash.ts
35
+
36
+ // src/constants.ts
37
+ var DEFAULT_INACTIVITY_TIMEOUT_MS = 6e4;
38
+ var DEFAULT_WALLCLOCK_BACKSTOP_MS = 3e5;
39
+ var MAX_COMMAND_LENGTH = 16384;
40
+ var MAX_OUTPUT_BYTES_INLINE = 30720;
41
+ var MAX_OUTPUT_BYTES_FILE = 10 * 1024 * 1024;
42
+ var BACKGROUND_MAX_JOBS = 16;
43
+ var KILL_GRACE_MS = 5e3;
44
+ var SENSITIVE_ENV_PREFIXES = [
45
+ "AWS_",
46
+ "BEDROCK_",
47
+ "GITHUB_TOKEN",
48
+ "GH_TOKEN",
49
+ "OPENAI_API_KEY",
50
+ "ANTHROPIC_API_KEY",
51
+ "GOOGLE_API_KEY",
52
+ "GEMINI_API_KEY",
53
+ "NPM_TOKEN",
54
+ "DOCKERHUB_TOKEN",
55
+ "SLACK_",
56
+ "STRIPE_"
57
+ ];
58
+ function createLocalBashExecutor(opts) {
59
+ const bashPath = opts?.bashPath ?? "/bin/bash";
60
+ const logDir = opts?.logDir ?? path4__default.default.join(os.tmpdir(), "agent-sh-bash-logs");
61
+ fs.mkdirSync(logDir, { recursive: true });
62
+ const jobs = /* @__PURE__ */ new Map();
63
+ async function runForeground2(input) {
64
+ const child = child_process.spawn(bashPath, ["-c", input.command], {
65
+ cwd: input.cwd,
66
+ env: { ...input.env },
67
+ stdio: ["ignore", "pipe", "pipe"]
68
+ });
69
+ child.stdout.on("data", (chunk) => input.onStdout(chunk));
70
+ child.stderr.on("data", (chunk) => input.onStderr(chunk));
71
+ let killedBySignal = false;
72
+ const onAbort = () => {
73
+ killedBySignal = true;
74
+ try {
75
+ child.kill("SIGTERM");
76
+ setTimeout(() => {
77
+ if (child.exitCode === null) child.kill("SIGKILL");
78
+ }, KILL_GRACE_MS).unref();
79
+ } catch {
80
+ }
81
+ };
82
+ if (input.signal.aborted) {
83
+ onAbort();
84
+ } else {
85
+ input.signal.addEventListener("abort", onAbort, { once: true });
86
+ }
87
+ return new Promise((resolve) => {
88
+ child.on("close", (code, signal) => {
89
+ input.signal.removeEventListener("abort", onAbort);
90
+ resolve({
91
+ exitCode: code,
92
+ killed: killedBySignal,
93
+ signal: signal ?? null
94
+ });
95
+ });
96
+ child.on("error", () => {
97
+ input.signal.removeEventListener("abort", onAbort);
98
+ resolve({ exitCode: null, killed: killedBySignal, signal: null });
99
+ });
100
+ });
101
+ }
102
+ async function spawnBackground(args) {
103
+ const jobId = crypto.randomUUID();
104
+ const outPath = path4__default.default.join(logDir, `${jobId}.out`);
105
+ const errPath = path4__default.default.join(logDir, `${jobId}.err`);
106
+ const outStream = fs.createWriteStream(outPath, { flags: "w" });
107
+ const errStream = fs.createWriteStream(errPath, { flags: "w" });
108
+ const child = child_process.spawn(bashPath, ["-c", args.command], {
109
+ cwd: args.cwd,
110
+ env: { ...args.env },
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ detached: false
113
+ });
114
+ child.stdout.pipe(outStream);
115
+ child.stderr.pipe(errStream);
116
+ const job = {
117
+ id: jobId,
118
+ outPath,
119
+ errPath,
120
+ running: true,
121
+ exitCode: null,
122
+ killed: false,
123
+ proc: child
124
+ };
125
+ jobs.set(jobId, job);
126
+ child.on("close", (code) => {
127
+ job.running = false;
128
+ job.exitCode = code;
129
+ job.proc = null;
130
+ });
131
+ child.on("error", () => {
132
+ job.running = false;
133
+ job.proc = null;
134
+ });
135
+ return { jobId };
136
+ }
137
+ async function readBackground(jobId, opts2) {
138
+ const job = jobs.get(jobId);
139
+ if (!job) {
140
+ throw new Error(`Unknown job_id: ${jobId}`);
141
+ }
142
+ const since = opts2.since_byte ?? 0;
143
+ const limit = opts2.head_limit ?? 30720;
144
+ const stdout = readSlice(job.outPath, since, limit);
145
+ const stderr = readSlice(job.errPath, since, limit);
146
+ return {
147
+ stdout: stdout.text,
148
+ stderr: stderr.text,
149
+ running: job.running,
150
+ exitCode: job.exitCode,
151
+ totalBytesStdout: stdout.totalBytes,
152
+ totalBytesStderr: stderr.totalBytes
153
+ };
154
+ }
155
+ async function killBackground(jobId, signal = "SIGTERM") {
156
+ const job = jobs.get(jobId);
157
+ if (!job || !job.proc) return;
158
+ job.killed = true;
159
+ try {
160
+ job.proc.kill(signal);
161
+ if (signal === "SIGTERM") {
162
+ setTimeout(() => {
163
+ if (job.running && job.proc) {
164
+ try {
165
+ job.proc.kill("SIGKILL");
166
+ } catch {
167
+ }
168
+ }
169
+ }, KILL_GRACE_MS).unref();
170
+ }
171
+ } catch {
172
+ }
173
+ }
174
+ async function closeSession() {
175
+ for (const job of jobs.values()) {
176
+ if (job.running && job.proc) {
177
+ try {
178
+ job.proc.kill("SIGTERM");
179
+ } catch {
180
+ }
181
+ }
182
+ }
183
+ }
184
+ return {
185
+ run: runForeground2,
186
+ spawnBackground,
187
+ readBackground,
188
+ killBackground,
189
+ closeSession
190
+ };
191
+ }
192
+ function readSlice(filePath, since, limit) {
193
+ if (!fs.existsSync(filePath)) return { text: "", totalBytes: 0 };
194
+ const totalBytes = fs.statSync(filePath).size;
195
+ if (since >= totalBytes) return { text: "", totalBytes };
196
+ const end = Math.min(since + limit, totalBytes);
197
+ const buf = fs.readFileSync(filePath).subarray(since, end);
198
+ return { text: buf.toString("utf8"), totalBytes };
199
+ }
200
+ async function resolveCwd(ops, session, requested) {
201
+ const base = requested ?? session.logicalCwd?.value ?? session.cwd;
202
+ const absolute = path4__default.default.isAbsolute(base) ? base : path4__default.default.resolve(session.cwd, base);
203
+ try {
204
+ return await ops.realpath(absolute);
205
+ } catch {
206
+ return absolute;
207
+ }
208
+ }
209
+ async function fenceBash(session, resolvedCwd) {
210
+ const { permissions } = session;
211
+ const isSensitive = harnessCore.matchesAnyPattern(
212
+ resolvedCwd,
213
+ permissions.sensitivePatterns
214
+ );
215
+ const insideWorkspace = harnessCore.isInsideAnyRoot(resolvedCwd, permissions.roots);
216
+ if (isSensitive && permissions.hook === void 0) {
217
+ return harnessCore.toolError(
218
+ "SENSITIVE",
219
+ `Refusing to run bash in sensitive path: ${resolvedCwd}`,
220
+ { meta: { path: resolvedCwd } }
221
+ );
222
+ }
223
+ if (!insideWorkspace && permissions.bypassWorkspaceGuard !== true && permissions.hook === void 0) {
224
+ return harnessCore.toolError(
225
+ "OUTSIDE_WORKSPACE",
226
+ `cwd is outside all configured workspace roots: ${resolvedCwd}`,
227
+ { meta: { path: resolvedCwd, roots: permissions.roots } }
228
+ );
229
+ }
230
+ return void 0;
231
+ }
232
+ async function askPermission(session, args) {
233
+ const { permissions } = session;
234
+ const commandHead = args.command.trimStart().split(/\s+/)[0] ?? "";
235
+ const alwaysPatterns = commandHead ? [`Bash(${commandHead}:*)`] : ["Bash(*)"];
236
+ if (permissions.hook === void 0) {
237
+ if (permissions.unsafeAllowBashWithoutHook === true) {
238
+ return { decision: "allow" };
239
+ }
240
+ return {
241
+ decision: "deny",
242
+ reason: "bash tool has no permission hook configured; refusing to run untrusted commands. Wire a hook or set session.permissions.unsafeAllowBashWithoutHook for test fixtures."
243
+ };
244
+ }
245
+ const decision = await permissions.hook({
246
+ tool: "bash",
247
+ path: args.cwd,
248
+ action: "read",
249
+ always_patterns: alwaysPatterns,
250
+ metadata: {
251
+ command: args.command,
252
+ cwd: args.cwd,
253
+ background: args.background,
254
+ timeout_ms: args.timeoutMs,
255
+ env_keys: args.envKeys,
256
+ network_required: null
257
+ }
258
+ });
259
+ if (decision === "deny") {
260
+ return {
261
+ decision: "deny",
262
+ reason: `Command blocked by permission policy. Pattern hint: ${alwaysPatterns.join(", ")}`
263
+ };
264
+ }
265
+ if (decision === "allow" || decision === "allow_once") {
266
+ return { decision };
267
+ }
268
+ return {
269
+ decision: "deny",
270
+ reason: "Permission hook returned 'ask' but bash runs in autonomous mode. Configure the hook to return allow or deny."
271
+ };
272
+ }
273
+ function resolveOps(session) {
274
+ return session.ops ?? harnessCore.defaultNodeOperations();
275
+ }
276
+ var HeadTailBuffer = class {
277
+ constructor(maxInline, maxFile, kind, spillDir) {
278
+ this.maxInline = maxInline;
279
+ this.maxFile = maxFile;
280
+ this.kind = kind;
281
+ this.spillDir = spillDir;
282
+ }
283
+ maxInline;
284
+ maxFile;
285
+ kind;
286
+ spillDir;
287
+ chunks = [];
288
+ totalBytes = 0;
289
+ byteCap = false;
290
+ spilled = false;
291
+ spillPath = null;
292
+ spillBytes = [];
293
+ write(chunk) {
294
+ this.totalBytes += chunk.byteLength;
295
+ if (this.totalBytes <= this.maxInline) {
296
+ this.chunks.push(chunk);
297
+ return;
298
+ }
299
+ if (!this.spilled) {
300
+ this.spilled = true;
301
+ this.byteCap = true;
302
+ this.spillPath = path4__default.default.join(
303
+ this.spillDir,
304
+ `${crypto.randomUUID()}.${this.kind}`
305
+ );
306
+ for (const c of this.chunks) this.appendSpill(c);
307
+ }
308
+ this.appendSpill(chunk);
309
+ this.spillBytes.push(chunk.byteLength);
310
+ }
311
+ appendSpill(chunk) {
312
+ if (this.spillPath === null) return;
313
+ if (!this.spillInit) {
314
+ fs.mkdirSync(this.spillDir, { recursive: true });
315
+ fs.writeFileSync(this.spillPath, "");
316
+ this.spillInit = true;
317
+ }
318
+ if (this.fileBytesWritten + chunk.byteLength > this.maxFile) {
319
+ return;
320
+ }
321
+ fs.appendFileSync(this.spillPath, Buffer.from(chunk));
322
+ this.fileBytesWritten += chunk.byteLength;
323
+ }
324
+ spillInit = false;
325
+ fileBytesWritten = 0;
326
+ /**
327
+ * Return the inline render:
328
+ * - If not capped: the full buffered text.
329
+ * - If capped: head (first maxInline/2 bytes) + marker + tail
330
+ * (last maxInline/2 bytes) approximation. We approximate the tail
331
+ * by decoding only the tail window (maxInline/2 bytes from the spill
332
+ * file) because the stream is write-once and we dropped the middle.
333
+ *
334
+ * The actual implementation is simpler: we keep only the head inline
335
+ * (first maxInline bytes, never overwritten) and emit a marker that
336
+ * points at the log path. Head-only is a deliberate simplification
337
+ * versus spec's head+tail — it matches OpenCode's default, and we
338
+ * rely on Read(path) to see the tail. Spec §4 head+tail is a v2
339
+ * improvement once we prove the file-path recovery path.
340
+ */
341
+ render() {
342
+ if (!this.spilled) {
343
+ const combined = Buffer.concat(this.chunks.map((c) => Buffer.from(c)));
344
+ return {
345
+ text: combined.toString("utf8"),
346
+ byteCap: false,
347
+ logPath: null
348
+ };
349
+ }
350
+ const head = Buffer.concat(
351
+ this.chunks.map((c) => Buffer.from(c)),
352
+ this.maxInline
353
+ ).toString("utf8");
354
+ const marker = `
355
+ ... (stream exceeded ${this.maxInline} bytes; full log at ${this.spillPath}) ...`;
356
+ return {
357
+ text: head + marker,
358
+ byteCap: true,
359
+ logPath: this.spillPath
360
+ };
361
+ }
362
+ bytesTotal() {
363
+ return this.totalBytes;
364
+ }
365
+ wasCapped() {
366
+ return this.byteCap;
367
+ }
368
+ };
369
+ function defaultSpillDir() {
370
+ return path4__default.default.join(os.tmpdir(), "agent-sh-bash-spill");
371
+ }
372
+ function formatResultText(args) {
373
+ const header = `<command>${args.command}</command>`;
374
+ const exitLine = `<exit_code>${args.exitCode}</exit_code>`;
375
+ const stdoutBlock = `<stdout>
376
+ ${args.stdout}
377
+ </stdout>`;
378
+ const stderrBlock = `<stderr>
379
+ ${args.stderr}
380
+ </stderr>`;
381
+ const hint = args.byteCap ? `(Output capped. Full log: ${args.logPath}. Read it with pagination if you need the middle.)` : args.kind === "ok" ? `(Command completed in ${args.durationMs}ms. exit=0.)` : `(Command exited nonzero in ${args.durationMs}ms. Exit code: ${args.exitCode}.)`;
382
+ return [header, exitLine, stdoutBlock, stderrBlock, hint].join("\n");
383
+ }
384
+ function formatTimeoutText(args) {
385
+ const header = `<command>${args.command}</command>`;
386
+ const stdoutBlock = `<stdout>
387
+ ${args.stdout}
388
+ </stdout>`;
389
+ const stderrBlock = `<stderr>
390
+ ${args.stderr}
391
+ </stderr>`;
392
+ const logHint = args.logPath ? ` Full log: ${args.logPath}.` : "";
393
+ const hint = `(Command hit ${args.reason} after ${args.durationMs}ms. ${args.partialBytes} bytes captured. Kill signal: SIGTERM then SIGKILL.${logHint} If the command is long-running, retry with background: true.)`;
394
+ return [header, stdoutBlock, stderrBlock, hint].join("\n");
395
+ }
396
+ function formatBackgroundStartedText(args) {
397
+ return [
398
+ `<command>${args.command}</command>`,
399
+ `<job_id>${args.jobId}</job_id>`,
400
+ `(Background job started. Poll output with bash_output(job_id). Kill with bash_kill(job_id).)`
401
+ ].join("\n");
402
+ }
403
+ function formatBashOutputText(args) {
404
+ const next = args.sinceByte + args.returnedBytes;
405
+ return [
406
+ `<job_id>${args.jobId}</job_id>`,
407
+ `<running>${args.running}</running>`,
408
+ `<exit_code>${args.exitCode === null ? "null" : args.exitCode}</exit_code>`,
409
+ `<stdout>
410
+ ${args.stdout}
411
+ </stdout>`,
412
+ `<stderr>
413
+ ${args.stderr}
414
+ </stderr>`,
415
+ `(Showing bytes ${args.sinceByte}-${next} of ${args.totalBytes}. Next since_byte: ${next}. Job running: ${args.running}.)`
416
+ ].join("\n");
417
+ }
418
+ function formatBashKillText(args) {
419
+ return `<job_id>${args.jobId}</job_id>
420
+ (${args.signal} sent. Poll bash_output to confirm termination.)`;
421
+ }
422
+ var BashParamsSchema = v__namespace.strictObject({
423
+ command: v__namespace.pipe(
424
+ v__namespace.string(),
425
+ v__namespace.minLength(1, "command is required"),
426
+ v__namespace.maxLength(
427
+ MAX_COMMAND_LENGTH,
428
+ `command exceeds ${MAX_COMMAND_LENGTH} bytes`
429
+ )
430
+ ),
431
+ cwd: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "cwd must not be empty"))),
432
+ timeout_ms: v__namespace.optional(
433
+ v__namespace.pipe(
434
+ v__namespace.number(),
435
+ v__namespace.integer(),
436
+ v__namespace.minValue(100, "timeout_ms must be >= 100 ms")
437
+ )
438
+ ),
439
+ description: v__namespace.optional(v__namespace.string()),
440
+ background: v__namespace.optional(v__namespace.boolean()),
441
+ env: v__namespace.optional(v__namespace.record(v__namespace.string(), v__namespace.string()))
442
+ });
443
+ var BashOutputParamsSchema = v__namespace.strictObject({
444
+ job_id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "job_id is required")),
445
+ since_byte: v__namespace.optional(
446
+ v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(0, "since_byte must be >= 0"))
447
+ ),
448
+ head_limit: v__namespace.optional(
449
+ v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1, "head_limit must be >= 1"))
450
+ )
451
+ });
452
+ var BashKillParamsSchema = v__namespace.strictObject({
453
+ job_id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "job_id is required")),
454
+ signal: v__namespace.optional(v__namespace.picklist(["SIGTERM", "SIGKILL"]))
455
+ });
456
+ var KNOWN_PARAM_ALIASES = {
457
+ cmd: "unknown parameter 'cmd'. Use 'command' instead.",
458
+ shell_command: "unknown parameter 'shell_command'. Use 'command' instead.",
459
+ script: "unknown parameter 'script'. Use 'command' instead.",
460
+ run: "unknown parameter 'run'. Use 'command' instead.",
461
+ directory: "unknown parameter 'directory'. Use 'cwd' instead.",
462
+ dir: "unknown parameter 'dir'. Use 'cwd' instead.",
463
+ path: "unknown parameter 'path'. Use 'cwd' instead.",
464
+ working_directory: "unknown parameter 'working_directory'. Use 'cwd' instead.",
465
+ timeout: "unknown parameter 'timeout'. Use 'timeout_ms' instead (milliseconds, not seconds). For 30s pass timeout_ms: 30000.",
466
+ time_limit: "unknown parameter 'time_limit'. Use 'timeout_ms' instead (milliseconds).",
467
+ timeout_seconds: "unknown parameter 'timeout_seconds'. Use 'timeout_ms' instead (multiply by 1000).",
468
+ env_vars: "unknown parameter 'env_vars'. Use 'env' instead.",
469
+ environment: "unknown parameter 'environment'. Use 'env' instead.",
470
+ lang: `unknown parameter 'lang'. Bash runs shell commands; invoke other languages via the command itself (e.g. 'python -c "..."', 'node -e "..."').`,
471
+ language: `unknown parameter 'language'. Invoke other languages via the command (e.g. 'python -c "..."', 'node -e "..."').`,
472
+ interpreter: `unknown parameter 'interpreter'. Invoke the interpreter inside the command itself (e.g. 'python -c "..."').`,
473
+ runtime: `unknown parameter 'runtime'. Invoke the runtime inside the command itself (e.g. 'node -e "..."').`,
474
+ stdin: `unknown parameter 'stdin'. Interactive stdin is not supported in v1. Pipe data into the command instead (e.g. 'echo "y" | npm init').`,
475
+ input: "unknown parameter 'input'. Interactive input is not supported in v1. Make the command non-interactive with flags like --yes.",
476
+ sandbox: "unknown parameter 'sandbox'. Sandboxing is configured on the session, not per-call.",
477
+ sandbox_mode: "unknown parameter 'sandbox_mode'. Sandboxing is configured on the session, not per-call.",
478
+ permissions: "unknown parameter 'permissions'. The permission hook is configured on the session.",
479
+ network: "unknown parameter 'network'. Network access is configured on the session / executor adapter.",
480
+ network_access: "unknown parameter 'network_access'. Network access is configured on the session / executor adapter.",
481
+ shell: "unknown parameter 'shell'. Shell binary is configured on the session.",
482
+ shell_binary: "unknown parameter 'shell_binary'. Shell binary is configured on the session."
483
+ };
484
+ function checkAliases(input) {
485
+ if (input === null || typeof input !== "object") return [];
486
+ const hints = [];
487
+ for (const key of Object.keys(input)) {
488
+ const hint = KNOWN_PARAM_ALIASES[key];
489
+ if (hint) hints.push(hint);
490
+ }
491
+ return hints;
492
+ }
493
+ function makeAliasIssues(messages) {
494
+ return messages.map(
495
+ (m) => ({
496
+ kind: "validation",
497
+ type: "custom",
498
+ input: void 0,
499
+ expected: null,
500
+ received: "unknown",
501
+ message: m
502
+ })
503
+ );
504
+ }
505
+ function safeParseBashParams(input) {
506
+ const aliases = checkAliases(input);
507
+ if (aliases.length > 0) {
508
+ return { ok: false, issues: makeAliasIssues(aliases) };
509
+ }
510
+ const result = v__namespace.safeParse(BashParamsSchema, input);
511
+ if (result.success) return { ok: true, value: result.output };
512
+ return { ok: false, issues: result.issues };
513
+ }
514
+ function safeParseBashOutputParams(input) {
515
+ const result = v__namespace.safeParse(BashOutputParamsSchema, input);
516
+ if (result.success) return { ok: true, value: result.output };
517
+ return { ok: false, issues: result.issues };
518
+ }
519
+ function safeParseBashKillParams(input) {
520
+ const result = v__namespace.safeParse(BashKillParamsSchema, input);
521
+ if (result.success) return { ok: true, value: result.output };
522
+ return { ok: false, issues: result.issues };
523
+ }
524
+ var BASH_TOOL_NAME = "bash";
525
+ var BASH_TOOL_DESCRIPTION = `Run a single shell command in a bash subprocess. Output is captured and returned with the exit code.
526
+
527
+ Usage:
528
+ - 'cd' carries over to subsequent calls if it stays inside the workspace; otherwise the cwd is reset. Environment variables do NOT persist across calls \u2014 set them inline (FOO=bar some-cmd) or via 'env'.
529
+ - For non-shell code, use language one-liners: 'python -c "print(2+2)"', 'node -e "console.log(2+2)"', 'deno eval "console.log(2+2)"'. For multi-line scripts, write a temp file with the write tool and invoke the interpreter on it.
530
+ - Long-running processes (servers, watchers) MUST use background: true. The tool returns a job_id; poll output with bash_output(job_id). Do not leave a foreground command running past the 5-minute wall-clock backstop.
531
+ - No interactive commands. Anything that needs stdin (pagers, Y/n prompts, REPLs, 'git commit' without -m) will hang until the inactivity timeout. Use flags to make commands non-interactive (--yes, -y, --no-pager) or pipe 'echo "y" |' in front.
532
+ - Inactivity timeout resets on any output; default 60000 ms. Override with timeout_ms. Wall-clock backstop is 5 minutes for foreground calls.
533
+ - Prefer this tool over other ways of running shell commands. For filename search prefer 'glob'; for content search prefer 'grep'.`;
534
+ var bashToolDefinition = {
535
+ name: BASH_TOOL_NAME,
536
+ description: BASH_TOOL_DESCRIPTION,
537
+ inputSchema: {
538
+ type: "object",
539
+ properties: {
540
+ command: {
541
+ type: "string",
542
+ description: "The shell command to run (single string, interpreted by bash -c)."
543
+ },
544
+ cwd: {
545
+ type: "string",
546
+ description: "Absolute working directory. Defaults to the session cwd plus any carried-over cd. Must be inside the workspace."
547
+ },
548
+ timeout_ms: {
549
+ type: "integer",
550
+ minimum: 100,
551
+ description: "Inactivity timeout in milliseconds. Any output resets the clock. Default 60000 (60 s). Wall-clock backstop is 5 minutes regardless."
552
+ },
553
+ description: {
554
+ type: "string",
555
+ description: "One-line human-readable 'why' (optional, for traces)."
556
+ },
557
+ background: {
558
+ type: "boolean",
559
+ description: "Run as a background job. Returns a job_id; poll output with bash_output. Use for servers, watchers, long-running builds."
560
+ },
561
+ env: {
562
+ type: "object",
563
+ additionalProperties: { type: "string" },
564
+ description: "Environment variables merged on top of the session env. Keys with sensitive prefixes (AWS_*, GITHUB_TOKEN, etc.) are rejected."
565
+ }
566
+ },
567
+ required: ["command"],
568
+ additionalProperties: false
569
+ }
570
+ };
571
+ var BASH_OUTPUT_TOOL_NAME = "bash_output";
572
+ var BASH_OUTPUT_TOOL_DESCRIPTION = `Poll a backgrounded bash job's output since a given byte offset.
573
+
574
+ Returns stdout and stderr slices plus whether the job is still running and its exit code if finished. Use 'since_byte' from the previous call to paginate through a long-running job's output without re-fetching already-seen bytes.`;
575
+ var bashOutputToolDefinition = {
576
+ name: BASH_OUTPUT_TOOL_NAME,
577
+ description: BASH_OUTPUT_TOOL_DESCRIPTION,
578
+ inputSchema: {
579
+ type: "object",
580
+ properties: {
581
+ job_id: {
582
+ type: "string",
583
+ description: "The job_id returned by a previous bash call with background: true."
584
+ },
585
+ since_byte: {
586
+ type: "integer",
587
+ minimum: 0,
588
+ description: "Start of the requested slice per stream, in bytes. Defaults to 0. Use next_since_byte from a previous output call to resume."
589
+ },
590
+ head_limit: {
591
+ type: "integer",
592
+ minimum: 1,
593
+ description: "Max bytes per stream (default 30720 / 30 KB)."
594
+ }
595
+ },
596
+ required: ["job_id"],
597
+ additionalProperties: false
598
+ }
599
+ };
600
+ var BASH_KILL_TOOL_NAME = "bash_kill";
601
+ var BASH_KILL_TOOL_DESCRIPTION = `Send a termination signal to a backgrounded bash job.
602
+
603
+ Defaults to SIGTERM (graceful). Use SIGKILL for an unresponsive job. The job's next bash_output call will report running: false.`;
604
+ var bashKillToolDefinition = {
605
+ name: BASH_KILL_TOOL_NAME,
606
+ description: BASH_KILL_TOOL_DESCRIPTION,
607
+ inputSchema: {
608
+ type: "object",
609
+ properties: {
610
+ job_id: {
611
+ type: "string",
612
+ description: "The job_id returned by a previous bash call with background: true."
613
+ },
614
+ signal: {
615
+ type: "string",
616
+ enum: ["SIGTERM", "SIGKILL"],
617
+ description: "Signal to send. Default SIGTERM."
618
+ }
619
+ },
620
+ required: ["job_id"],
621
+ additionalProperties: false
622
+ }
623
+ };
624
+
625
+ // src/bash.ts
626
+ var jobCountBySession = /* @__PURE__ */ new WeakMap();
627
+ function incJobCount(session) {
628
+ jobCountBySession.set(session, (jobCountBySession.get(session) ?? 0) + 1);
629
+ }
630
+ function jobCount(session) {
631
+ return jobCountBySession.get(session) ?? 0;
632
+ }
633
+ function err(error) {
634
+ return { kind: "error", error };
635
+ }
636
+ function resolveExecutor(session) {
637
+ if (session.executor) return session.executor;
638
+ if (session.permissions.unsafeAllowBashWithoutHook !== true) ;
639
+ return createLocalBashExecutor();
640
+ }
641
+ function detectTopLevelCd(command) {
642
+ const trimmed = command.trim();
643
+ if (trimmed.length === 0) return null;
644
+ const match = trimmed.match(/^cd\s+([^\s&|;`$()]+)$/);
645
+ if (!match) return null;
646
+ const arg = match[1];
647
+ if (arg === void 0) return null;
648
+ if (arg.startsWith('"') && arg.endsWith('"') || arg.startsWith("'") && arg.endsWith("'")) {
649
+ return arg.slice(1, -1);
650
+ }
651
+ return arg;
652
+ }
653
+ function checkEnv(env) {
654
+ for (const key of Object.keys(env)) {
655
+ for (const prefix of SENSITIVE_ENV_PREFIXES) {
656
+ if (key === prefix || prefix.endsWith("_") && key.startsWith(prefix)) {
657
+ return `env may not set sensitive-prefix variable '${key}' (prefix '${prefix}').`;
658
+ }
659
+ }
660
+ }
661
+ return null;
662
+ }
663
+ function byteLength(s) {
664
+ return Buffer.byteLength(s, "utf8");
665
+ }
666
+ async function bash(input, session) {
667
+ const parsed = safeParseBashParams(input);
668
+ if (!parsed.ok) {
669
+ const messages = parsed.issues.map((i) => i.message).join("; ");
670
+ return err(harnessCore.toolError("INVALID_PARAM", messages, { cause: parsed.issues }));
671
+ }
672
+ const params = parsed.value;
673
+ if (params.background === true && params.timeout_ms !== void 0) {
674
+ return err(
675
+ harnessCore.toolError(
676
+ "INVALID_PARAM",
677
+ "timeout_ms does not apply to background jobs; they have their own lifecycle (bash_kill). Drop timeout_ms or set background: false."
678
+ )
679
+ );
680
+ }
681
+ const envParam = params.env ?? {};
682
+ const envError = checkEnv(envParam);
683
+ if (envError) {
684
+ return err(harnessCore.toolError("INVALID_PARAM", envError));
685
+ }
686
+ const ops = resolveOps(session);
687
+ const resolvedCwd = await resolveCwd(ops, session, params.cwd);
688
+ const fenceError = await fenceBash(session, resolvedCwd);
689
+ if (fenceError) return err(fenceError);
690
+ const stat = await ops.stat(resolvedCwd).catch(() => void 0);
691
+ if (!stat) {
692
+ return err(
693
+ harnessCore.toolError("NOT_FOUND", `cwd does not exist: ${resolvedCwd}`, {
694
+ meta: { cwd: resolvedCwd }
695
+ })
696
+ );
697
+ }
698
+ if (stat.type !== "directory") {
699
+ return err(
700
+ harnessCore.toolError(
701
+ "IO_ERROR",
702
+ `cwd is not a directory: ${resolvedCwd}`,
703
+ { meta: { cwd: resolvedCwd } }
704
+ )
705
+ );
706
+ }
707
+ const effectiveTimeout = params.timeout_ms ?? session.defaultInactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS;
708
+ const decision = await askPermission(session, {
709
+ command: params.command,
710
+ cwd: resolvedCwd,
711
+ background: params.background ?? false,
712
+ timeoutMs: effectiveTimeout,
713
+ envKeys: Object.keys(envParam)
714
+ });
715
+ if (decision.decision === "deny") {
716
+ const echo = params.command.length > 200 ? params.command.slice(0, 200) + "..." : params.command;
717
+ return err(
718
+ harnessCore.toolError(
719
+ "PERMISSION_DENIED",
720
+ `${decision.reason}
721
+ Command: ${echo}`,
722
+ { meta: { command: params.command, cwd: resolvedCwd } }
723
+ )
724
+ );
725
+ }
726
+ const execEnv = {
727
+ ...session.env ?? process.env,
728
+ ...envParam
729
+ };
730
+ for (const [k, v2] of Object.entries(execEnv)) {
731
+ if (v2 === void 0) delete execEnv[k];
732
+ }
733
+ const executor = resolveExecutor(session);
734
+ if (params.background === true) {
735
+ return runBackground(
736
+ session,
737
+ executor,
738
+ params.command,
739
+ resolvedCwd,
740
+ execEnv
741
+ );
742
+ }
743
+ return runForeground(
744
+ session,
745
+ executor,
746
+ params.command,
747
+ resolvedCwd,
748
+ execEnv,
749
+ effectiveTimeout
750
+ );
751
+ }
752
+ async function runBackground(session, executor, command, cwd, env) {
753
+ if (!executor.spawnBackground) {
754
+ return err(
755
+ harnessCore.toolError(
756
+ "INVALID_PARAM",
757
+ "background: true is not supported by this executor adapter."
758
+ )
759
+ );
760
+ }
761
+ const maxJobs = session.maxBackgroundJobs ?? BACKGROUND_MAX_JOBS;
762
+ if (jobCount(session) >= maxJobs) {
763
+ return err(
764
+ harnessCore.toolError(
765
+ "IO_ERROR",
766
+ `Background job limit reached (${maxJobs}). Kill an existing job first with bash_kill.`
767
+ )
768
+ );
769
+ }
770
+ const { jobId } = await executor.spawnBackground({ command, cwd, env });
771
+ incJobCount(session);
772
+ return {
773
+ kind: "background_started",
774
+ output: formatBackgroundStartedText({ command, jobId }),
775
+ jobId
776
+ };
777
+ }
778
+ async function runForeground(session, executor, command, cwd, env, inactivityTimeoutMs) {
779
+ const wallclockMs = session.wallclockBackstopMs ?? DEFAULT_WALLCLOCK_BACKSTOP_MS;
780
+ const maxInline = session.maxOutputBytesInline ?? MAX_OUTPUT_BYTES_INLINE;
781
+ const maxFile = session.maxOutputBytesFile ?? MAX_OUTPUT_BYTES_FILE;
782
+ const spillDir = defaultSpillDir();
783
+ const stdoutBuf = new HeadTailBuffer(maxInline, maxFile, "out", spillDir);
784
+ const stderrBuf = new HeadTailBuffer(maxInline, maxFile, "err", spillDir);
785
+ const controller = new AbortController();
786
+ const abortOnOuter = () => controller.abort();
787
+ if (session.signal) {
788
+ if (session.signal.aborted) controller.abort();
789
+ else session.signal.addEventListener("abort", abortOnOuter, { once: true });
790
+ }
791
+ let timedOut = null;
792
+ let inactivityTimer = null;
793
+ const resetInactivity = () => {
794
+ if (inactivityTimer) clearTimeout(inactivityTimer);
795
+ inactivityTimer = setTimeout(() => {
796
+ timedOut = "inactivity timeout";
797
+ controller.abort();
798
+ }, inactivityTimeoutMs);
799
+ };
800
+ resetInactivity();
801
+ const wallclockTimer = setTimeout(() => {
802
+ timedOut = "wall-clock backstop";
803
+ controller.abort();
804
+ }, wallclockMs);
805
+ const start = Date.now();
806
+ let result;
807
+ try {
808
+ result = await executor.run({
809
+ command,
810
+ cwd,
811
+ env,
812
+ signal: controller.signal,
813
+ onStdout: (chunk) => {
814
+ stdoutBuf.write(chunk);
815
+ resetInactivity();
816
+ },
817
+ onStderr: (chunk) => {
818
+ stderrBuf.write(chunk);
819
+ resetInactivity();
820
+ }
821
+ });
822
+ } finally {
823
+ if (inactivityTimer) clearTimeout(inactivityTimer);
824
+ clearTimeout(wallclockTimer);
825
+ if (session.signal) {
826
+ session.signal.removeEventListener("abort", abortOnOuter);
827
+ }
828
+ }
829
+ const durationMs = Date.now() - start;
830
+ const stdoutRender = stdoutBuf.render();
831
+ const stderrRender = stderrBuf.render();
832
+ if (timedOut !== null) {
833
+ const logPath2 = stdoutRender.logPath ?? stderrRender.logPath;
834
+ return {
835
+ kind: "timeout",
836
+ output: formatTimeoutText({
837
+ command,
838
+ stdout: stdoutRender.text,
839
+ stderr: stderrRender.text,
840
+ reason: timedOut,
841
+ durationMs,
842
+ partialBytes: stdoutBuf.bytesTotal() + stderrBuf.bytesTotal(),
843
+ logPath: logPath2
844
+ }),
845
+ stdout: stdoutRender.text,
846
+ stderr: stderrRender.text,
847
+ reason: timedOut,
848
+ durationMs,
849
+ ...logPath2 ? { logPath: logPath2 } : {}
850
+ };
851
+ }
852
+ const exitCode = result.exitCode ?? -1;
853
+ const kind = exitCode === 0 ? "ok" : "nonzero_exit";
854
+ const logPath = stdoutRender.logPath ?? stderrRender.logPath;
855
+ const byteCap = stdoutRender.byteCap || stderrRender.byteCap;
856
+ return {
857
+ kind,
858
+ output: formatResultText({
859
+ command,
860
+ exitCode,
861
+ stdout: stdoutRender.text,
862
+ stderr: stderrRender.text,
863
+ durationMs,
864
+ byteCap,
865
+ logPath,
866
+ kind
867
+ }),
868
+ exitCode,
869
+ stdout: stdoutRender.text,
870
+ stderr: stderrRender.text,
871
+ durationMs,
872
+ ...logPath ? { logPath } : {},
873
+ byteCap
874
+ };
875
+ }
876
+ async function bashOutput(input, session) {
877
+ const parsed = safeParseBashOutputParams(input);
878
+ if (!parsed.ok) {
879
+ const messages = parsed.issues.map((i) => i.message).join("; ");
880
+ return err(harnessCore.toolError("INVALID_PARAM", messages));
881
+ }
882
+ const executor = session.executor ?? createLocalBashExecutor();
883
+ if (!executor.readBackground) {
884
+ return err(
885
+ harnessCore.toolError(
886
+ "INVALID_PARAM",
887
+ "bash_output is not supported by this executor adapter."
888
+ )
889
+ );
890
+ }
891
+ try {
892
+ const read = await executor.readBackground(parsed.value.job_id, {
893
+ ...parsed.value.since_byte !== void 0 ? { since_byte: parsed.value.since_byte } : {},
894
+ ...parsed.value.head_limit !== void 0 ? { head_limit: parsed.value.head_limit } : {}
895
+ });
896
+ const sinceByte = parsed.value.since_byte ?? 0;
897
+ const returnedBytes = byteLength(read.stdout) + byteLength(read.stderr);
898
+ const totalBytes = read.totalBytesStdout + read.totalBytesStderr;
899
+ return {
900
+ kind: "output",
901
+ output: formatBashOutputText({
902
+ jobId: parsed.value.job_id,
903
+ running: read.running,
904
+ exitCode: read.exitCode,
905
+ stdout: read.stdout,
906
+ stderr: read.stderr,
907
+ sinceByte,
908
+ returnedBytes,
909
+ totalBytes
910
+ }),
911
+ running: read.running,
912
+ exitCode: read.exitCode,
913
+ stdout: read.stdout,
914
+ stderr: read.stderr,
915
+ totalBytesStdout: read.totalBytesStdout,
916
+ totalBytesStderr: read.totalBytesStderr,
917
+ nextSinceByte: sinceByte + returnedBytes
918
+ };
919
+ } catch (e) {
920
+ return err(
921
+ harnessCore.toolError(
922
+ "NOT_FOUND",
923
+ e.message || `Unknown job_id: ${parsed.value.job_id}`
924
+ )
925
+ );
926
+ }
927
+ }
928
+ async function bashKill(input, session) {
929
+ const parsed = safeParseBashKillParams(input);
930
+ if (!parsed.ok) {
931
+ const messages = parsed.issues.map((i) => i.message).join("; ");
932
+ return err(harnessCore.toolError("INVALID_PARAM", messages));
933
+ }
934
+ const executor = session.executor ?? createLocalBashExecutor();
935
+ if (!executor.killBackground) {
936
+ return err(
937
+ harnessCore.toolError(
938
+ "INVALID_PARAM",
939
+ "bash_kill is not supported by this executor adapter."
940
+ )
941
+ );
942
+ }
943
+ const signal = parsed.value.signal ?? "SIGTERM";
944
+ await executor.killBackground(parsed.value.job_id, signal);
945
+ return {
946
+ kind: "killed",
947
+ output: formatBashKillText({ jobId: parsed.value.job_id, signal }),
948
+ jobId: parsed.value.job_id,
949
+ signal
950
+ };
951
+ }
952
+ function applyCwdCarry(session, command, exitCode) {
953
+ if (exitCode !== 0) {
954
+ return { changed: false, newCwd: null, escaped: false };
955
+ }
956
+ const target = detectTopLevelCd(command);
957
+ if (target === null) {
958
+ return { changed: false, newCwd: null, escaped: false };
959
+ }
960
+ const base = session.logicalCwd?.value ?? session.cwd;
961
+ const resolved = path4__default.default.isAbsolute(target) ? path4__default.default.resolve(target) : path4__default.default.resolve(base, target);
962
+ const inside = session.permissions.roots.some(
963
+ (r) => resolved === r || resolved.startsWith(r + path4__default.default.sep)
964
+ );
965
+ if (!inside && session.permissions.bypassWorkspaceGuard !== true) {
966
+ return { changed: false, newCwd: resolved, escaped: true };
967
+ }
968
+ if (session.logicalCwd) {
969
+ session.logicalCwd.value = resolved;
970
+ }
971
+ return { changed: true, newCwd: resolved, escaped: false };
972
+ }
973
+
974
+ exports.BACKGROUND_MAX_JOBS = BACKGROUND_MAX_JOBS;
975
+ exports.BASH_KILL_TOOL_DESCRIPTION = BASH_KILL_TOOL_DESCRIPTION;
976
+ exports.BASH_KILL_TOOL_NAME = BASH_KILL_TOOL_NAME;
977
+ exports.BASH_OUTPUT_TOOL_DESCRIPTION = BASH_OUTPUT_TOOL_DESCRIPTION;
978
+ exports.BASH_OUTPUT_TOOL_NAME = BASH_OUTPUT_TOOL_NAME;
979
+ exports.BASH_TOOL_DESCRIPTION = BASH_TOOL_DESCRIPTION;
980
+ exports.BASH_TOOL_NAME = BASH_TOOL_NAME;
981
+ exports.BashKillParamsSchema = BashKillParamsSchema;
982
+ exports.BashOutputParamsSchema = BashOutputParamsSchema;
983
+ exports.BashParamsSchema = BashParamsSchema;
984
+ exports.DEFAULT_INACTIVITY_TIMEOUT_MS = DEFAULT_INACTIVITY_TIMEOUT_MS;
985
+ exports.DEFAULT_WALLCLOCK_BACKSTOP_MS = DEFAULT_WALLCLOCK_BACKSTOP_MS;
986
+ exports.HeadTailBuffer = HeadTailBuffer;
987
+ exports.MAX_COMMAND_LENGTH = MAX_COMMAND_LENGTH;
988
+ exports.MAX_OUTPUT_BYTES_FILE = MAX_OUTPUT_BYTES_FILE;
989
+ exports.MAX_OUTPUT_BYTES_INLINE = MAX_OUTPUT_BYTES_INLINE;
990
+ exports.SENSITIVE_ENV_PREFIXES = SENSITIVE_ENV_PREFIXES;
991
+ exports.applyCwdCarry = applyCwdCarry;
992
+ exports.bash = bash;
993
+ exports.bashKill = bashKill;
994
+ exports.bashKillToolDefinition = bashKillToolDefinition;
995
+ exports.bashOutput = bashOutput;
996
+ exports.bashOutputToolDefinition = bashOutputToolDefinition;
997
+ exports.bashToolDefinition = bashToolDefinition;
998
+ exports.createLocalBashExecutor = createLocalBashExecutor;
999
+ exports.detectTopLevelCd = detectTopLevelCd;
1000
+ exports.formatBackgroundStartedText = formatBackgroundStartedText;
1001
+ exports.formatBashKillText = formatBashKillText;
1002
+ exports.formatBashOutputText = formatBashOutputText;
1003
+ exports.formatResultText = formatResultText;
1004
+ exports.formatTimeoutText = formatTimeoutText;
1005
+ exports.safeParseBashKillParams = safeParseBashKillParams;
1006
+ exports.safeParseBashOutputParams = safeParseBashOutputParams;
1007
+ exports.safeParseBashParams = safeParseBashParams;
1008
+ //# sourceMappingURL=index.cjs.map
1009
+ //# sourceMappingURL=index.cjs.map