rust-bash 0.3.0

A sandboxed bash interpreter for AI Agents with a virtual filesystem
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
# Chapter 8: Integration Targets

## Overview

rust-bash is designed to be embedded anywhere. This chapter covers the integration surfaces: Rust crate API, CLI binary, C FFI, WASM, and AI SDK tool definitions.

## Rust Crate API

The primary interface. All other integration targets are thin wrappers around this.

```rust
use rust_bash::{RustBashBuilder, ExecResult};
use std::collections::HashMap;

let mut shell = RustBashBuilder::new()
    .files(HashMap::from([
        ("/data.txt".into(), b"hello world".to_vec()),
        ("/config.json".into(), b"{}".to_vec()),
    ]))
    .env(HashMap::from([
        ("USER".into(), "agent".into()),
        ("HOME".into(), "/home/agent".into()),
    ]))
    .cwd("/")
    .build()
    .unwrap();

let result: ExecResult = shell.exec("cat /data.txt | grep hello").unwrap();
assert_eq!(result.stdout, "hello world\n");
assert_eq!(result.exit_code, 0);
```

### RustBashBuilder

```rust
RustBashBuilder::new()
    .files(HashMap<String, Vec<u8>>)     // Seed VFS with files (path → bytes)
    .env(HashMap<String, String>)        // Set environment variables
    .cwd("/path")                        // Set working directory (created automatically)
    .execution_limits(limits)            // Configure limits
    .network_policy(policy)              // Configure network access
    .fs(Arc<dyn VirtualFs>)              // Use a custom filesystem backend
    .command(Box::new(custom_cmd))       // Register a custom command
    .build()                             // Returns Result<RustBash, RustBashError>
```

### ExecResult

```rust
pub struct ExecResult {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: i32,
}
```

## CLI Binary

A standalone binary for command-line usage (Milestone M5.1).

```bash
# Execute a command
rust-bash -c 'echo hello | wc -c'

# Execute a script file with positional arguments
rust-bash script.sh arg1 arg2

# Read commands from stdin
echo 'echo hello' | rust-bash

# Seed files from disk
rust-bash --files /data:/app/data.txt --files /config:/app/config.json -c 'cat /app/data.txt'

# Set environment
rust-bash --env USER=agent --env HOME=/home/agent -c 'echo $USER'

# JSON output for machine consumption
rust-bash --json -c 'echo hello'
# {"stdout":"hello\n","stderr":"","exit_code":0}

# Interactive REPL (starts when no command/script/stdin is given)
rust-bash
```

### Interactive REPL

When launched without `-c`, a script file, or piped stdin, `rust-bash` starts an
interactive REPL with readline support:

- **Colored prompt**: `rust-bash:{cwd}$ ` — green after exit 0, red after non-zero
- **Tab completion**: completes built-in command names (first token only)
- **Multi-line input**: incomplete constructs wait for continuation input
- **History**: loaded from / saved to `~/.rust_bash_history`
- **Ctrl-C**: cancels the current input line
- **Ctrl-D**: exits the REPL with the last command's exit code
- **`exit [N]`**: exits with code N (default 0)
- **`--json`**: rejected in REPL mode (exits with code 2)

> An interactive REPL is also available as a runnable example showing library-level embedding:
> `cargo run --example shell`

The CLI binary compiles as a single binary with no additional runtime dependencies beyond libc.

## C FFI

For embedding in Python, Go, Ruby, or any language with C interop.

### Build

```bash
cargo build --features ffi --release
# Output: target/release/librust_bash.so (Linux), .dylib (macOS), .dll (Windows)
# Header: include/rust_bash.h
```

### API

Six functions are exported (see `include/rust_bash.h` for full documentation):

```c
#include "rust_bash.h"

// Lifecycle
struct RustBash *rust_bash_create(const char *config_json); // NULL config → defaults
void             rust_bash_free(struct RustBash *sb);       // NULL-safe no-op

// Execution
struct ExecResult *rust_bash_exec(struct RustBash *sb, const char *command);
void               rust_bash_result_free(struct ExecResult *result); // NULL-safe no-op

// Diagnostics
const char *rust_bash_last_error(void); // NULL if no error; do not free
const char *rust_bash_version(void);    // static string; do not free
```

The `ExecResult` struct:

```c
typedef struct ExecResult {
    const char *stdout_ptr;
    int32_t     stdout_len;
    const char *stderr_ptr;
    int32_t     stderr_len;
    int32_t     exit_code;
} ExecResult;
```

### Configuration via JSON

Config is passed as a JSON string to `rust_bash_create`. All fields are optional — an empty `{}` or `NULL` produces a default-configured sandbox.

```json
{
  "files": {
    "/data.txt": "content",
    "/config.json": "{}"
  },
  "env": {
    "USER": "agent",
    "HOME": "/home/agent"
  },
  "cwd": "/",
  "limits": {
    "max_command_count": 10000,
    "max_execution_time_secs": 30,
    "max_loop_iterations": 10000,
    "max_output_size": 10485760,
    "max_call_depth": 100,
    "max_string_length": 10485760,
    "max_glob_results": 100000,
    "max_substitution_depth": 50,
    "max_heredoc_size": 10485760,
    "max_brace_expansion": 10000
  },
  "network": {
    "enabled": true,
    "allowed_url_prefixes": ["https://api.example.com/"],
    "allowed_methods": ["GET", "POST"],
    "max_response_size": 10485760,
    "max_redirects": 5,
    "timeout_secs": 30
  }
}
```

### Memory Ownership

- `rust_bash_create` returns a heap-allocated sandbox; caller must call `rust_bash_free`.
- `rust_bash_exec` returns a heap-allocated result; caller must call `rust_bash_result_free`.
- String pointers in `ExecResult` are valid until `rust_bash_result_free` is called.
- `rust_bash_version` returns a static string — do not free.
- `rust_bash_last_error` returns a pointer into thread-local storage — valid only until the next FFI call on the same thread; do not free.

### Thread Safety

A `RustBash*` handle must not be shared across threads without external synchronization. Each handle is independently owned; different handles may be used concurrently from different threads. The last-error storage (`rust_bash_last_error`) is thread-local, so error messages are per-thread.

### Error Handling

Functions that can fail (`rust_bash_create`, `rust_bash_exec`) return `NULL` on error. After a `NULL` return, call `rust_bash_last_error()` on the same thread to retrieve a human-readable error message. The error string is valid until the next FFI call on that thread.

```c
struct RustBash *sb = rust_bash_create("{invalid json}");
if (!sb) {
    fprintf(stderr, "Error: %s\n", rust_bash_last_error());
}
```

### Python Example

```python
import ctypes

class ExecResult(ctypes.Structure):
    _fields_ = [
        ("stdout_ptr", ctypes.c_void_p),
        ("stdout_len", ctypes.c_int32),
        ("stderr_ptr", ctypes.c_void_p),
        ("stderr_len", ctypes.c_int32),
        ("exit_code", ctypes.c_int32),
    ]

lib = ctypes.CDLL("./target/release/librust_bash.so")

lib.rust_bash_create.argtypes = [ctypes.c_char_p]
lib.rust_bash_create.restype = ctypes.c_void_p
lib.rust_bash_exec.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
lib.rust_bash_exec.restype = ctypes.POINTER(ExecResult)
lib.rust_bash_result_free.argtypes = [ctypes.POINTER(ExecResult)]
lib.rust_bash_free.argtypes = [ctypes.c_void_p]
lib.rust_bash_last_error.restype = ctypes.c_char_p

sb = lib.rust_bash_create(b'{"files":{"/data.txt":"hello"}}')
if not sb:
    print("Error:", lib.rust_bash_last_error())
else:
    result = lib.rust_bash_exec(sb, b"cat /data.txt")
    if result:
        r = result.contents
        stdout = ctypes.string_at(r.stdout_ptr, r.stdout_len)
        print(stdout)  # b'hello\n'
        print("exit code:", r.exit_code)
        lib.rust_bash_result_free(result)
    lib.rust_bash_free(sb)
```

### Go Example

```go
package main

/*
#cgo LDFLAGS: -L./target/release -lrust_bash
#include "include/rust_bash.h"
#include <stdlib.h>
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
	sb := C.rust_bash_create(nil)
	if sb == nil {
		panic(C.GoString(C.rust_bash_last_error()))
	}
	defer C.rust_bash_free(sb)

	cmd := C.CString("echo hello world")
	defer C.free(unsafe.Pointer(cmd))

	r := C.rust_bash_exec(sb, cmd)
	if r == nil {
		panic(C.GoString(C.rust_bash_last_error()))
	}
	defer C.rust_bash_result_free(r)

	fmt.Printf("%s", C.GoStringN(r.stdout_ptr, C.int(r.stdout_len)))
	fmt.Printf("exit code: %d\n", r.exit_code)
}
```

## WASM Target

For browser and edge runtime embedding. The Rust interpreter compiles to `wasm32-unknown-unknown` via `wasm-bindgen`.

### Build

```bash
# Using the build script
./scripts/build-wasm.sh

# Or manually
cargo build --target wasm32-unknown-unknown --features wasm --no-default-features --release
wasm-bindgen target/wasm32-unknown-unknown/release/rust_bash.wasm --out-dir pkg --target bundler
```

### Platform Abstraction

- `std::time::{SystemTime, Instant}``crate::platform::*` (uses `web-time` crate on WASM)
- `std::thread::sleep` → returns error "sleep: not supported in browser environment"
- `chrono` uses `wasmbind` feature on WASM for timezone support
- `ureq`/`url` (networking) feature-gated behind `network` — disabled on WASM
- `OverlayFs`/`ReadWriteFs` feature-gated behind `native-fs` — WASM only gets `InMemoryFs`/`MountableFs`
- `parking_lot` compiles to WASM (falls back to spin-locks)

### Cargo Features for WASM

```toml
[features]
default = ["cli", "network", "native-fs"]
wasm = ["dep:wasm-bindgen", "dep:js-sys", "dep:serde", "dep:serde-wasm-bindgen"]
network = ["dep:ureq", "dep:url"]    # disabled on WASM
native-fs = []                         # disabled on WASM
```

### Compatibility Notes

- brush-parser compiles to `wasm32-unknown-unknown`
- The interpreter and VFS are pure Rust with no OS dependencies
- `web-time` crate provides `SystemTime`/`Instant` replacements for WASM
- Networking (`curl`) is feature-gated out; returns "command not found" on WASM

## npm Package (`rust-bash`)

The TypeScript npm package wraps both WASM and native addon backends behind a unified API.

### Installation

```bash
npm install rust-bash
```

### Architecture

The package ships three layers:

1. **TypeScript API** (`Bash` class, `defineCommand`, tool primitives) — the public interface
2. **Native addon** (napi-rs, planned) — near-native performance for Node.js
3. **WASM backend** — browser and edge runtime support

Backend detection is automatic on Node.js (native first, WASM fallback). Browsers use the `rust-bash/browser` entry point (WASM only).

### Quick Start (Node.js)

```typescript
import { Bash, tryLoadNative, createNativeBackend, initWasm, createWasmBackend } from 'rust-bash';

// Auto-detect backend
let createBackend;
if (await tryLoadNative()) {
  createBackend = createNativeBackend;
} else {
  await initWasm();
  createBackend = createWasmBackend;
}

const bash = await Bash.create(createBackend, {
  files: { '/data.txt': 'hello world' },
  env: { USER: 'agent' },
});

const result = await bash.exec('cat /data.txt | grep hello');
console.log(result.stdout); // "hello world\n"
```

### Quick Start (Browser)

```typescript
import { Bash, initWasm, createWasmBackend } from 'rust-bash/browser';

await initWasm();
const bash = await Bash.create(createWasmBackend, {
  files: { '/hello.txt': 'Hello from WASM!' },
  cwd: '/home/user',
});

const result = await bash.exec('cat /hello.txt');
console.log(result.stdout); // "Hello from WASM!\n"
```

### Bash Class API

```typescript
const bash = await Bash.create(createBackend, {
  files: {
    '/data.txt': 'hello world',              // eager
    '/lazy.txt': () => 'lazy content',        // lazy sync
    '/async.txt': async () => fetchData(),    // lazy async
  },
  env: { USER: 'agent', HOME: '/home/agent' },
  cwd: '/',
  executionLimits: {
    maxCommandCount: 10000,
    maxExecutionTimeSecs: 30,
  },
  customCommands: [myCommand],
});

// Execute commands
const result = await bash.exec('echo hello | tr a-z A-Z');
// { stdout: "HELLO\n", stderr: "", exitCode: 0 }

// Per-exec overrides
const result2 = await bash.exec('cat /data.txt', {
  env: { LANG: 'en_US.UTF-8' },
  cwd: '/data',
  stdin: 'input data',
});

// Direct VFS access
bash.fs.writeFileSync('/output.txt', 'content');
const data = bash.fs.readFileSync('/output.txt');
```

### Custom Commands

```typescript
import { defineCommand } from 'rust-bash';

const fetch = defineCommand('fetch', async (args, ctx) => {
  const url = args[0];
  const response = await globalThis.fetch(url);
  return { stdout: await response.text(), stderr: '', exitCode: 0 };
});

const bash = await Bash.create(createBackend, {
  customCommands: [fetch],
});
```

### Package Exports

| Export | Description |
|--------|-------------|
| `Bash` | Main class — `Bash.create(backend, options)` |
| `defineCommand` | Create custom commands |
| `bashToolDefinition` | JSON Schema tool definition for AI integration |
| `createBashToolHandler` | Factory for tool handlers |
| `formatToolForProvider` | Format tools for OpenAI, Anthropic, MCP |
| `handleToolCall` | Multi-tool dispatcher |
| `initWasm` / `createWasmBackend` | WASM backend |
| `tryLoadNative` / `createNativeBackend` | Native addon backend |

## AI SDK Tool Definition

For use with OpenAI, Anthropic, and other function-calling LLM APIs. Available via both the TypeScript npm package and the Rust CLI's MCP server mode.

### TypeScript: Framework-Agnostic Primitives

`rust-bash` exports JSON Schema tool definitions and a handler factory that work with **any** AI agent framework — no framework dependencies required.

```typescript
import { bashToolDefinition, createBashToolHandler, formatToolForProvider, createNativeBackend } from 'rust-bash';

// bashToolDefinition is a plain JSON Schema object:
// {
//   name: 'bash',
//   description: 'Execute bash commands in a sandboxed environment...',
//   inputSchema: {
//     type: 'object',
//     properties: { command: { type: 'string', description: '...' } },
//     required: ['command'],
//   },
// }

// createBashToolHandler returns a framework-agnostic handler:
const { handler, definition, bash } = createBashToolHandler(createNativeBackend, {
  files: { '/data.txt': 'hello world' },
  maxOutputLength: 10000,
});

const result = await handler({ command: 'grep hello /data.txt' });
// { stdout: 'hello world\n', stderr: '', exitCode: 0 }

// Format for specific providers (thin wrappers, no dependencies)
const openaiTool = formatToolForProvider(bashToolDefinition, 'openai');
// { type: "function", function: { name: "bash", description: "...", parameters: {...} } }

const anthropicTool = formatToolForProvider(bashToolDefinition, 'anthropic');
// { name: "bash", description: "...", input_schema: {...} }

const mcpTool = formatToolForProvider(bashToolDefinition, 'mcp');
// { name: "bash", description: "...", inputSchema: {...} }
```

Additional exports for agent loops:

- `handleToolCall(bash, toolName, args)` — dispatches `bash`, `readFile`/`read_file`, `writeFile`/`write_file`, `listDirectory`/`list_directory` tool calls (supports both camelCase and snake_case)
- `writeFileToolDefinition`, `readFileToolDefinition`, `listDirectoryToolDefinition` — JSON Schema definitions for file operation tools

### Recipe: OpenAI

```typescript
import OpenAI from 'openai';
import { createBashToolHandler, formatToolForProvider, bashToolDefinition, createNativeBackend } from 'rust-bash';

const { handler } = createBashToolHandler(createNativeBackend, { files: myFiles });

const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  tools: [formatToolForProvider(bashToolDefinition, 'openai')],
  messages: [{ role: 'user', content: 'List files in /data' }],
});

for (const toolCall of response.choices[0].message.tool_calls ?? []) {
  const result = await handler(JSON.parse(toolCall.function.arguments));
}
```

### Recipe: Anthropic

```typescript
import Anthropic from '@anthropic-ai/sdk';
import { createBashToolHandler, formatToolForProvider, bashToolDefinition, createNativeBackend } from 'rust-bash';

const { handler } = createBashToolHandler(createNativeBackend, { files: myFiles });

const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 1024,
  tools: [formatToolForProvider(bashToolDefinition, 'anthropic')],
  messages: [{ role: 'user', content: 'List files in /data' }],
});

for (const block of response.content) {
  if (block.type === 'tool_use') {
    const result = await handler(block.input);
  }
}
```

### Recipe: Vercel AI SDK

```typescript
import { tool } from 'ai';
import { z } from 'zod';
import { createBashToolHandler, createNativeBackend } from 'rust-bash';

const { handler } = createBashToolHandler(createNativeBackend, { files: myFiles });
const bashTool = tool({
  description: 'Execute bash commands in a sandbox',
  parameters: z.object({ command: z.string() }),
  execute: async ({ command }) => handler({ command }),
});
```

### Recipe: LangChain.js

```typescript
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { createBashToolHandler, createNativeBackend } from 'rust-bash';

const { handler, definition } = createBashToolHandler(createNativeBackend, { files: myFiles });
const bashTool = tool(
  async ({ command }) => JSON.stringify(await handler({ command })),
  { name: definition.name, description: definition.description, schema: z.object({ command: z.string() }) },
);
```

See [AI Agent Tool Recipe](../recipes/ai-agent-tool.md) for complete examples with full agent loops.

### MCP Server Mode

The CLI binary includes a built-in MCP (Model Context Protocol) server for direct integration with Claude Desktop, Cursor, VS Code, Windsurf, Cline, and the OpenAI Agents SDK:

```bash
rust-bash --mcp
```

Exposed tools: `bash`, `write_file`, `read_file`, `list_directory`.

Configuration for Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):

```json
{
  "mcpServers": {
    "rust-bash": {
      "command": "rust-bash",
      "args": ["--mcp"]
    }
  }
}
```

Configuration for VS Code (`.vscode/mcp.json`):

```json
{
  "servers": {
    "rust-bash": {
      "type": "stdio",
      "command": "rust-bash",
      "args": ["--mcp"]
    }
  }
}
```

The MCP server maintains a stateful shell session across all tool calls — variables, files, and working directory persist between invocations.

See [MCP Server Setup](../recipes/mcp-server.md) for detailed setup instructions for all supported clients.

### Tool Schema

```json
{
  "type": "function",
  "function": {
    "name": "bash",
    "description": "Execute bash commands in a sandboxed environment with an in-memory filesystem.",
    "parameters": {
      "type": "object",
      "properties": {
        "command": {
          "type": "string",
          "description": "The bash command to execute"
        }
      },
      "required": ["command"]
    }
  }
}
```

### Rust Integration Pattern

```rust
// Create sandbox once per agent session
let mut shell = RustBashBuilder::new()
    .files(project_files)
    .build()
    .unwrap();

// In the agent tool dispatch loop:
match tool_call.name.as_str() {
    "bash" => {
        let command = tool_call.arguments["command"].as_str().unwrap();
        let result = shell.exec(command)?;
        format!("stdout:\n{}\nstderr:\n{}\nexit_code: {}", 
                result.stdout, result.stderr, result.exit_code)
    }
    _ => { /* other tools */ }
}
```

## Browser Integration (WASM)

rust-bash runs in the browser via WebAssembly. The `rust-bash` npm package provides a browser entry point that loads the WASM binary.

### Architecture

The showcase website at `examples/website/` demonstrates the full browser integration:

1. **xterm.js** renders a terminal in the browser
2. **rust-bash WASM** (or a development mock) executes commands
3. An **AI agent** (via Cloudflare Worker → Gemini API) can request tool calls
4. Tool calls execute **locally** in the browser — no server roundtrip for bash

### Key Concepts

- **Shared state**: The user and agent share the same bash instance and VFS. Files created by the agent are visible to the user and vice versa.
- **Client-side execution**: All bash commands run locally in the WASM module. The only network call is to the LLM proxy for the `agent` command.
- **Cached responses**: The initial demo uses a hand-crafted `AgentEvent[]` array, avoiding API calls on first load.

### Usage

```typescript
import { Bash, initWasm, createWasmBackend } from 'rust-bash/browser';

// Initialize WASM module
await initWasm();

// Create a bash instance with preloaded files
const bash = await Bash.create(createWasmBackend, {
  files: { '/hello.txt': 'Hello from WASM!' },
  cwd: '/home/user',
});

const result = await bash.exec('cat /hello.txt');
console.log(result.stdout); // "Hello from WASM!"
```