@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.
@@ -0,0 +1,351 @@
1
+ import { ToolError, PermissionPolicy, ToolDefinition } from '@agent-sh/harness-core';
2
+ import * as v from 'valibot';
3
+
4
+ interface BashParams {
5
+ readonly command: string;
6
+ readonly cwd?: string;
7
+ readonly timeout_ms?: number;
8
+ readonly description?: string;
9
+ readonly background?: boolean;
10
+ readonly env?: Readonly<Record<string, string>>;
11
+ }
12
+ interface BashOutputParams {
13
+ readonly job_id: string;
14
+ readonly since_byte?: number;
15
+ readonly head_limit?: number;
16
+ }
17
+ interface BashKillParams {
18
+ readonly job_id: string;
19
+ readonly signal?: "SIGTERM" | "SIGKILL";
20
+ }
21
+ /**
22
+ * Executor interface — the pluggable boundary between core (which ships a
23
+ * local subprocess runner) and adapter packages (bash-docker, bash-firejail,
24
+ * bash-e2b). Core NEVER imports an adapter; adapters are peer deps of the
25
+ * harness that chooses one.
26
+ */
27
+ interface BashRunResult {
28
+ readonly exitCode: number | null;
29
+ readonly killed: boolean;
30
+ readonly signal: string | null;
31
+ }
32
+ interface BashRunInput {
33
+ readonly command: string;
34
+ readonly cwd: string;
35
+ readonly env: Readonly<Record<string, string>>;
36
+ readonly signal: AbortSignal;
37
+ readonly onStdout: (chunk: Uint8Array) => void;
38
+ readonly onStderr: (chunk: Uint8Array) => void;
39
+ }
40
+ interface BackgroundReadResult {
41
+ readonly stdout: string;
42
+ readonly stderr: string;
43
+ readonly running: boolean;
44
+ readonly exitCode: number | null;
45
+ readonly totalBytesStdout: number;
46
+ readonly totalBytesStderr: number;
47
+ }
48
+ interface BashExecutor {
49
+ run(input: BashRunInput): Promise<BashRunResult>;
50
+ spawnBackground?(input: {
51
+ command: string;
52
+ cwd: string;
53
+ env: Readonly<Record<string, string>>;
54
+ }): Promise<{
55
+ jobId: string;
56
+ }>;
57
+ readBackground?(jobId: string, opts: {
58
+ since_byte?: number;
59
+ head_limit?: number;
60
+ }): Promise<BackgroundReadResult>;
61
+ killBackground?(jobId: string, signal?: "SIGTERM" | "SIGKILL"): Promise<void>;
62
+ closeSession?(): Promise<void>;
63
+ }
64
+ /**
65
+ * Session-bound permission policy. Same shape as read/grep/glob sessions,
66
+ * with an opt-in escape hatch for unsandboxed test fixtures only.
67
+ */
68
+ interface BashPermissionPolicy extends PermissionPolicy {
69
+ readonly unsafeAllowBashWithoutHook?: boolean;
70
+ }
71
+ interface BashSessionConfig {
72
+ readonly cwd: string;
73
+ readonly permissions: BashPermissionPolicy;
74
+ readonly env?: Readonly<Record<string, string>>;
75
+ readonly executor?: BashExecutor;
76
+ readonly defaultInactivityTimeoutMs?: number;
77
+ readonly wallclockBackstopMs?: number;
78
+ readonly maxCommandLength?: number;
79
+ readonly maxOutputBytesInline?: number;
80
+ readonly maxOutputBytesFile?: number;
81
+ readonly maxBackgroundJobs?: number;
82
+ readonly signal?: AbortSignal;
83
+ /**
84
+ * Working directory the tool tracks across calls. When the model issues
85
+ * a top-level `cd <path>` that lands inside the workspace, we mutate this
86
+ * in place. Optional — if omitted, cwd-carry is disabled and every call
87
+ * runs at `session.cwd`.
88
+ */
89
+ logicalCwd?: {
90
+ value: string;
91
+ };
92
+ }
93
+ type BashOk = {
94
+ readonly kind: "ok";
95
+ readonly output: string;
96
+ readonly exitCode: number;
97
+ readonly stdout: string;
98
+ readonly stderr: string;
99
+ readonly durationMs: number;
100
+ readonly logPath?: string;
101
+ readonly byteCap: boolean;
102
+ };
103
+ type BashNonzeroExit = {
104
+ readonly kind: "nonzero_exit";
105
+ readonly output: string;
106
+ readonly exitCode: number;
107
+ readonly stdout: string;
108
+ readonly stderr: string;
109
+ readonly durationMs: number;
110
+ readonly logPath?: string;
111
+ readonly byteCap: boolean;
112
+ };
113
+ type BashTimeout = {
114
+ readonly kind: "timeout";
115
+ readonly output: string;
116
+ readonly stdout: string;
117
+ readonly stderr: string;
118
+ readonly reason: "inactivity timeout" | "wall-clock backstop";
119
+ readonly durationMs: number;
120
+ readonly logPath?: string;
121
+ };
122
+ type BashBackgroundStarted = {
123
+ readonly kind: "background_started";
124
+ readonly output: string;
125
+ readonly jobId: string;
126
+ };
127
+ type BashError = {
128
+ readonly kind: "error";
129
+ readonly error: ToolError;
130
+ };
131
+ type BashResult = BashOk | BashNonzeroExit | BashTimeout | BashBackgroundStarted | BashError;
132
+ type BashOutputResult = {
133
+ readonly kind: "output";
134
+ readonly output: string;
135
+ readonly running: boolean;
136
+ readonly exitCode: number | null;
137
+ readonly stdout: string;
138
+ readonly stderr: string;
139
+ readonly totalBytesStdout: number;
140
+ readonly totalBytesStderr: number;
141
+ readonly nextSinceByte: number;
142
+ } | BashError;
143
+ type BashKillResult = {
144
+ readonly kind: "killed";
145
+ readonly output: string;
146
+ readonly jobId: string;
147
+ readonly signal: "SIGTERM" | "SIGKILL";
148
+ } | BashError;
149
+
150
+ /**
151
+ * Top-level `cd` detector for session cwd-carry.
152
+ *
153
+ * Matches a single top-level `cd` invocation only — NOT inside pipelines
154
+ * (`cd x | true`), command lists (`cd x && y`), subshells (`(cd x)`),
155
+ * or with trailing arguments. This deliberately covers 95% of model
156
+ * intent without hand-parsing the full bash grammar.
157
+ *
158
+ * Returns the path argument if detected, else null.
159
+ */
160
+ declare function detectTopLevelCd(command: string): string | null;
161
+ declare function bash(input: unknown, session: BashSessionConfig): Promise<BashResult>;
162
+ declare function bashOutput(input: unknown, session: BashSessionConfig): Promise<BashOutputResult>;
163
+ declare function bashKill(input: unknown, session: BashSessionConfig): Promise<BashKillResult>;
164
+ /**
165
+ * Apply cwd-carry: if the command is a top-level `cd <path>` and the
166
+ * destination resolves inside the workspace, mutate session.logicalCwd.
167
+ * Called AFTER the command executes with exit 0 (caller's responsibility).
168
+ *
169
+ * Exposed separately so tests can exercise the logic directly AND so a
170
+ * harness wrapper can call it at the right point in the lifecycle. In
171
+ * core, the orchestrator does NOT auto-call this — we keep cwd-carry
172
+ * out of the hot path for correctness; the caller opts in by invoking
173
+ * applyCwdCarry after a successful bash() result.
174
+ *
175
+ * Rationale: cwd-carry mutates session state which has observable
176
+ * implications for concurrent calls. Making it explicit is safer.
177
+ */
178
+ declare function applyCwdCarry(session: BashSessionConfig, command: string, exitCode: number | null): {
179
+ changed: boolean;
180
+ newCwd: string | null;
181
+ escaped: boolean;
182
+ };
183
+
184
+ declare const BashParamsSchema: v.StrictObjectSchema<{
185
+ readonly command: v.SchemaWithPipe<[v.StringSchema<undefined>, v.MinLengthAction<string, 1, "command is required">, v.MaxLengthAction<string, 16384, "command exceeds 16384 bytes">]>;
186
+ readonly cwd: v.OptionalSchema<v.SchemaWithPipe<[v.StringSchema<undefined>, v.MinLengthAction<string, 1, "cwd must not be empty">]>, never>;
187
+ readonly timeout_ms: v.OptionalSchema<v.SchemaWithPipe<[v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 100, "timeout_ms must be >= 100 ms">]>, never>;
188
+ readonly description: v.OptionalSchema<v.StringSchema<undefined>, never>;
189
+ readonly background: v.OptionalSchema<v.BooleanSchema<undefined>, never>;
190
+ readonly env: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, never>;
191
+ }, undefined>;
192
+ declare const BashOutputParamsSchema: v.StrictObjectSchema<{
193
+ readonly job_id: v.SchemaWithPipe<[v.StringSchema<undefined>, v.MinLengthAction<string, 1, "job_id is required">]>;
194
+ readonly since_byte: v.OptionalSchema<v.SchemaWithPipe<[v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, "since_byte must be >= 0">]>, never>;
195
+ readonly head_limit: v.OptionalSchema<v.SchemaWithPipe<[v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, "head_limit must be >= 1">]>, never>;
196
+ }, undefined>;
197
+ declare const BashKillParamsSchema: v.StrictObjectSchema<{
198
+ readonly job_id: v.SchemaWithPipe<[v.StringSchema<undefined>, v.MinLengthAction<string, 1, "job_id is required">]>;
199
+ readonly signal: v.OptionalSchema<v.PicklistSchema<["SIGTERM", "SIGKILL"], undefined>, never>;
200
+ }, undefined>;
201
+ declare function safeParseBashParams(input: unknown): {
202
+ ok: true;
203
+ value: BashParams;
204
+ } | {
205
+ ok: false;
206
+ issues: v.BaseIssue<unknown>[];
207
+ };
208
+ declare function safeParseBashOutputParams(input: unknown): {
209
+ ok: true;
210
+ value: BashOutputParams;
211
+ } | {
212
+ ok: false;
213
+ issues: v.BaseIssue<unknown>[];
214
+ };
215
+ declare function safeParseBashKillParams(input: unknown): {
216
+ ok: true;
217
+ value: BashKillParams;
218
+ } | {
219
+ ok: false;
220
+ issues: v.BaseIssue<unknown>[];
221
+ };
222
+ declare const BASH_TOOL_NAME = "bash";
223
+ declare const BASH_TOOL_DESCRIPTION = "Run a single shell command in a bash subprocess. Output is captured and returned with the exit code.\n\nUsage:\n- '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'.\n- 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.\n- 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.\n- 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.\n- Inactivity timeout resets on any output; default 60000 ms. Override with timeout_ms. Wall-clock backstop is 5 minutes for foreground calls.\n- Prefer this tool over other ways of running shell commands. For filename search prefer 'glob'; for content search prefer 'grep'.";
224
+ declare const bashToolDefinition: ToolDefinition;
225
+ declare const BASH_OUTPUT_TOOL_NAME = "bash_output";
226
+ declare const BASH_OUTPUT_TOOL_DESCRIPTION = "Poll a backgrounded bash job's output since a given byte offset.\n\nReturns 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.";
227
+ declare const bashOutputToolDefinition: ToolDefinition;
228
+ declare const BASH_KILL_TOOL_NAME = "bash_kill";
229
+ declare const BASH_KILL_TOOL_DESCRIPTION = "Send a termination signal to a backgrounded bash job.\n\nDefaults to SIGTERM (graceful). Use SIGKILL for an unresponsive job. The job's next bash_output call will report running: false.";
230
+ declare const bashKillToolDefinition: ToolDefinition;
231
+
232
+ /**
233
+ * Default local-subprocess executor.
234
+ *
235
+ * Launches the bash binary with `-c <command>` via the argv form of
236
+ * node:child_process.spawn — NEVER the string-based shell-eval entry
237
+ * point. The command string is passed as a single argument to the bash
238
+ * binary, not interpolated into our own spawn args. All shell parsing
239
+ * happens inside the child bash process.
240
+ *
241
+ * This executor ships unsandboxed; sandboxing is the job of adapter
242
+ * packages that implement the same BashExecutor interface. See
243
+ * packages/bash/src/types.ts.
244
+ */
245
+ declare function createLocalBashExecutor(opts?: {
246
+ bashPath?: string;
247
+ logDir?: string;
248
+ }): BashExecutor;
249
+
250
+ /**
251
+ * Per-stream output buffer with head+tail capping and spill-to-file on
252
+ * overflow. Models rarely need the middle of a long output — they need
253
+ * either the setup line or the error tail. This buffer preserves both.
254
+ */
255
+ declare class HeadTailBuffer {
256
+ private readonly maxInline;
257
+ private readonly maxFile;
258
+ private readonly kind;
259
+ private readonly spillDir;
260
+ private readonly chunks;
261
+ private totalBytes;
262
+ private byteCap;
263
+ private spilled;
264
+ private spillPath;
265
+ private spillBytes;
266
+ constructor(maxInline: number, maxFile: number, kind: "out" | "err", spillDir: string);
267
+ write(chunk: Uint8Array): void;
268
+ private appendSpill;
269
+ private spillInit;
270
+ private fileBytesWritten;
271
+ /**
272
+ * Return the inline render:
273
+ * - If not capped: the full buffered text.
274
+ * - If capped: head (first maxInline/2 bytes) + marker + tail
275
+ * (last maxInline/2 bytes) approximation. We approximate the tail
276
+ * by decoding only the tail window (maxInline/2 bytes from the spill
277
+ * file) because the stream is write-once and we dropped the middle.
278
+ *
279
+ * The actual implementation is simpler: we keep only the head inline
280
+ * (first maxInline bytes, never overwritten) and emit a marker that
281
+ * points at the log path. Head-only is a deliberate simplification
282
+ * versus spec's head+tail — it matches OpenCode's default, and we
283
+ * rely on Read(path) to see the tail. Spec §4 head+tail is a v2
284
+ * improvement once we prove the file-path recovery path.
285
+ */
286
+ render(): {
287
+ text: string;
288
+ byteCap: boolean;
289
+ logPath: string | null;
290
+ };
291
+ bytesTotal(): number;
292
+ wasCapped(): boolean;
293
+ }
294
+ /**
295
+ * Format the text body of an "ok" / "nonzero_exit" result.
296
+ * Kept deliberately simple — structured fields ride on the result
297
+ * object; `output` is the canonical text the executor returns to the
298
+ * model at the tool_result boundary.
299
+ */
300
+ declare function formatResultText(args: {
301
+ command: string;
302
+ exitCode: number;
303
+ stdout: string;
304
+ stderr: string;
305
+ durationMs: number;
306
+ byteCap: boolean;
307
+ logPath: string | null;
308
+ kind: "ok" | "nonzero_exit";
309
+ }): string;
310
+ declare function formatTimeoutText(args: {
311
+ command: string;
312
+ stdout: string;
313
+ stderr: string;
314
+ reason: "inactivity timeout" | "wall-clock backstop";
315
+ durationMs: number;
316
+ partialBytes: number;
317
+ logPath: string | null;
318
+ }): string;
319
+ declare function formatBackgroundStartedText(args: {
320
+ command: string;
321
+ jobId: string;
322
+ }): string;
323
+ declare function formatBashOutputText(args: {
324
+ jobId: string;
325
+ running: boolean;
326
+ exitCode: number | null;
327
+ stdout: string;
328
+ stderr: string;
329
+ sinceByte: number;
330
+ returnedBytes: number;
331
+ totalBytes: number;
332
+ }): string;
333
+ declare function formatBashKillText(args: {
334
+ jobId: string;
335
+ signal: "SIGTERM" | "SIGKILL";
336
+ }): string;
337
+
338
+ declare const DEFAULT_INACTIVITY_TIMEOUT_MS = 60000;
339
+ declare const DEFAULT_WALLCLOCK_BACKSTOP_MS = 300000;
340
+ declare const MAX_COMMAND_LENGTH = 16384;
341
+ declare const MAX_OUTPUT_BYTES_INLINE = 30720;
342
+ declare const MAX_OUTPUT_BYTES_FILE: number;
343
+ declare const BACKGROUND_MAX_JOBS = 16;
344
+ /**
345
+ * Env var name prefixes that the tool refuses to let the model set via `env`.
346
+ * Defense in depth: even if the harness forwards its environment, the model
347
+ * should not be able to override credentials per-call.
348
+ */
349
+ declare const SENSITIVE_ENV_PREFIXES: readonly string[];
350
+
351
+ export { BACKGROUND_MAX_JOBS, BASH_KILL_TOOL_DESCRIPTION, BASH_KILL_TOOL_NAME, BASH_OUTPUT_TOOL_DESCRIPTION, BASH_OUTPUT_TOOL_NAME, BASH_TOOL_DESCRIPTION, BASH_TOOL_NAME, type BackgroundReadResult, type BashBackgroundStarted, type BashError, type BashExecutor, type BashKillParams, BashKillParamsSchema, type BashKillResult, type BashNonzeroExit, type BashOk, type BashOutputParams, BashOutputParamsSchema, type BashOutputResult, type BashParams, BashParamsSchema, type BashPermissionPolicy, type BashResult, type BashRunInput, type BashRunResult, type BashSessionConfig, type BashTimeout, DEFAULT_INACTIVITY_TIMEOUT_MS, DEFAULT_WALLCLOCK_BACKSTOP_MS, HeadTailBuffer, MAX_COMMAND_LENGTH, MAX_OUTPUT_BYTES_FILE, MAX_OUTPUT_BYTES_INLINE, SENSITIVE_ENV_PREFIXES, applyCwdCarry, bash, bashKill, bashKillToolDefinition, bashOutput, bashOutputToolDefinition, bashToolDefinition, createLocalBashExecutor, detectTopLevelCd, formatBackgroundStartedText, formatBashKillText, formatBashOutputText, formatResultText, formatTimeoutText, safeParseBashKillParams, safeParseBashOutputParams, safeParseBashParams };