Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ jobs:
cargo run --example live_mounts
cargo run --example git_workflow --features git
cargo run --example python_external_functions --features python
cargo run --example typescript_external_functions --features typescript
cargo run --example realfs_readonly --features realfs
cargo run --example realfs_readwrite --features realfs

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn
| 012-maintenance | Pre-release maintenance requirements |
| 013-python-package | Python package, PyPI wheels, platform matrix |
| 014-scripted-tool-orchestration | Compose ToolDef+callback pairs into OrchestratorTool via bash scripts |
| 016-zapcode-runtime | Embedded TypeScript via ZapCode, VFS bridging, resource limits |

### Documentation

Expand Down
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Virtual bash interpreter for multi-tenant environments. Written in Rust.
- **Language bindings** - Python (PyO3) and JavaScript/TypeScript (NAPI-RS) for Node.js, Bun, and Deno
- **Experimental: Git support** - Virtual git operations on the virtual filesystem (`git` feature)
- **Experimental: Python support** - Embedded Python interpreter via [Monty](https://github.com/pydantic/monty) (`python` feature)
- **Experimental: TypeScript support** - Embedded TypeScript interpreter via [ZapCode](https://github.com/TheUncharted/zapcode) (`typescript` feature)

## Install

Expand All @@ -44,6 +45,7 @@ Optional features:
```bash
cargo add bashkit --features git # Virtual git operations
cargo add bashkit --features python # Embedded Python interpreter
cargo add bashkit --features typescript # Embedded TypeScript interpreter
cargo add bashkit --features realfs # Real filesystem backend
cargo add bashkit --features scripted_tool # Tool orchestration framework
```
Expand Down Expand Up @@ -129,7 +131,7 @@ assert_eq!(output.result["stdout"], "hello\nworld\n");
| Data formats | `csv`, `json`, `yaml`, `tomlq`, `template`, `envsubst` |
| Network | `curl`, `wget` (requires allowlist), `http` |
| DevOps | `assert`, `dotenv`, `glob`, `log`, `retry`, `semver`, `verify`, `parallel`, `patch` |
| Experimental | `python`, `python3` (requires `python` feature), `git` (requires `git` feature) |
| Experimental | `python`, `python3` (requires `python` feature), `ts`, `typescript`, `node`, `deno`, `bun` (requires `typescript` feature), `git` (requires `git` feature) |

## Shell Features

Expand Down Expand Up @@ -248,6 +250,47 @@ Stdlib modules: `math`, `re`, `pathlib`, `os` (getenv/environ), `sys`, `typing`.
Limitations: no `open()` (use `pathlib.Path`), no network, no classes, no third-party imports.
See [crates/bashkit/docs/python.md](crates/bashkit/docs/python.md) for the full guide.

## Experimental: TypeScript Support

Enable the `typescript` feature to embed the [ZapCode](https://github.com/TheUncharted/zapcode) TypeScript interpreter (pure Rust, no V8).
TypeScript code runs in-memory with configurable resource limits and VFS bridging via external function suspend/resume.

```toml
[dependencies]
bashkit = { version = "0.1", features = ["typescript"] }
```

```rust
use bashkit::Bash;

let mut bash = Bash::builder().typescript().build();

// Inline code (ts, node, deno, bun aliases all work)
bash.exec("ts -c \"console.log(2 ** 10)\"").await?;
bash.exec("node -e \"console.log('hello')\"").await?;

// Script files from VFS
bash.exec("ts /tmp/script.ts").await?;

// VFS bridging: readFile/writeFile async functions
bash.exec(r#"ts -c "await writeFile('/tmp/data.txt', 'hello from ts')"#).await?;
bash.exec("cat /tmp/data.txt").await?; // "hello from ts"
```

Compat aliases (`node`, `deno`, `bun`) and unsupported-mode hints are configurable:

```rust
use bashkit::{Bash, TypeScriptConfig};

// Only ts/typescript, no compat aliases
let bash = Bash::builder()
.typescript_with_config(TypeScriptConfig::default().compat_aliases(false))
.build();
```

Limitations: no `import`/`require`, no `eval()`, no network, no `process`/`Deno`/`Bun` globals.
See [crates/bashkit/docs/typescript.md](crates/bashkit/docs/typescript.md) for the full guide.

## Virtual Filesystem

```rust
Expand Down
14 changes: 14 additions & 0 deletions crates/bashkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ tracing = { workspace = true, optional = true }
# Embedded Python interpreter (optional)
monty = { git = "https://github.com/pydantic/monty", rev = "e59c8fa", optional = true }

# Embedded TypeScript interpreter (optional)
zapcode-core = { version = "1.5", optional = true }

[features]
default = []
http_client = ["reqwest"]
Expand All @@ -89,6 +92,9 @@ scripted_tool = []
# Enable python/python3 builtins via embedded Monty interpreter
# Monty is a git dep (not yet on crates.io) — feature unavailable from registry
python = ["dep:monty"]
# Enable ts/node/deno/bun builtins via embedded ZapCode TypeScript interpreter
# Usage: cargo build --features typescript
typescript = ["dep:zapcode-core"]
# Enable RealFs backend for accessing host filesystem directories
# WARNING: This intentionally breaks the sandbox boundary.
# Usage: cargo build --features realfs
Expand Down Expand Up @@ -127,6 +133,14 @@ required-features = ["python"]
name = "python_external_functions"
required-features = ["python"]

[[example]]
name = "typescript_scripts"
required-features = ["typescript"]

[[example]]
name = "typescript_external_functions"
required-features = ["typescript"]

[[example]]
name = "realfs_readonly"
required-features = ["realfs"]
Expand Down
252 changes: 252 additions & 0 deletions crates/bashkit/docs/typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# Embedded TypeScript (ZapCode)

> **Experimental.** ZapCode is an early-stage TypeScript interpreter that may
> have undiscovered crash or security bugs. Resource limits are enforced by
> ZapCode's VM. The integration should be treated as experimental.

Bashkit embeds the [ZapCode](https://github.com/TheUncharted/zapcode) TypeScript
interpreter, a pure-Rust implementation with ~2µs cold start, no V8 dependency,
and built-in sandboxing. TypeScript runs entirely in-memory with configurable
resource limits and no host access.

**See also:**
- [Threat Model](./threat-model.md) - Security considerations (TM-TS-*)
- [Custom Builtins](./custom_builtins.md) - Writing your own builtins
- [Compatibility Reference](./compatibility.md) - Bash feature support
- [`specs/016-zapcode-runtime.md`][spec] - Full specification

## Quick Start

Enable the `typescript` feature and register via builder:

```rust
use bashkit::Bash;

# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
let mut bash = Bash::builder().typescript().build();

let result = bash.exec("ts -c \"console.log('hello from ZapCode')\"").await?;
assert_eq!(result.stdout, "hello from ZapCode\n");
# Ok(())
# }
```

## Usage Patterns

### Inline Code

```bash
ts -c "console.log(2 ** 10)"
# Output: 1024

# Node.js, Deno, and Bun aliases also work
node -e "console.log('hello')"
deno -e "console.log('hello')"
bun -e "console.log('hello')"
```

### Expression Evaluation

When no `console.log()` is called, the last expression is displayed (REPL behavior):

```bash
ts -c "1 + 2 * 3"
# Output: 7
```

### Script Files (from VFS)

```bash
cat > /tmp/script.ts << 'EOF'
const data = [1, 2, 3, 4, 5];
const sum = data.reduce((a, b) => a + b, 0);
console.log(`sum=${sum}, avg=${sum / data.length}`);
EOF
ts /tmp/script.ts
```

### Pipelines and Command Substitution

```bash
result=$(ts -c "console.log(42 * 3)")
echo "Result: $result"

echo "console.log('piped')" | ts
```

## Virtual Filesystem (VFS) Bridging

VFS operations are available as async global functions in the TypeScript
environment. Files created by bash are readable from TypeScript and vice versa.

### Bash → TypeScript

```bash
echo "important data" > /tmp/shared.txt
ts -c "await readFile('/tmp/shared.txt')"
# Output: important data
```

### TypeScript → Bash

```bash
ts -c "await writeFile('/tmp/result.txt', 'computed by ts\n')"
cat /tmp/result.txt
# Output: computed by ts
```

### Supported VFS Operations

| Operation | Function | Return |
|-----------|----------|--------|
| Read file | `readFile(path)` | `string` |
| Write file | `writeFile(path, content)` | `void` |
| Check exists | `exists(path)` | `boolean` |
| List directory | `readDir(path)` | `string[]` |
| Create directory | `mkdir(path)` | `void` |
| Delete | `remove(path)` | `void` |
| File metadata | `stat(path)` | JSON string |

### Architecture

```text
TS code → ZapCode VM → ExternalFn("readFile", [path]) → Bashkit VFS → resume
```

ZapCode suspends at external function calls, Bashkit bridges them to the VFS,
then resumes execution with the return value.

**Note:** `console.log()` output produced *after* a VFS call is not captured
due to a `zapcode-core` API limitation. Use the return-value pattern instead —
the last expression's value is printed automatically.

## Resource Limits

Default limits prevent runaway TypeScript code. Customize via `TypeScriptLimits`:

```rust,no_run
use bashkit::{Bash, TypeScriptLimits};
use std::time::Duration;

# fn main() {
let bash = Bash::builder()
.typescript_with_limits(
TypeScriptLimits::default()
.max_duration(Duration::from_secs(5))
.max_memory(16 * 1024 * 1024) // 16 MB
.max_allocations(100_000)
.max_stack_depth(100)
)
.build();
# }
```

| Limit | Default | Purpose |
|-------|---------|---------|
| Duration | 30 seconds | Execution timeout |
| Memory | 64 MB | Heap memory cap |
| Stack depth | 512 | Call stack depth |
| Allocations | 1,000,000 | Heap allocation cap |

## Configuration

Use `TypeScriptConfig` for full control over aliases and hint behavior:

```rust,no_run
use bashkit::{Bash, TypeScriptConfig, TypeScriptLimits};
use std::time::Duration;

# fn main() {
// Default: ts, typescript, node, deno, bun + unsupported-mode hints
let bash = Bash::builder().typescript().build();

// Only ts/typescript commands, no node/deno/bun aliases
let bash = Bash::builder()
.typescript_with_config(TypeScriptConfig::default().compat_aliases(false))
.build();

// Disable unsupported-mode hints (plain errors only)
let bash = Bash::builder()
.typescript_with_config(TypeScriptConfig::default().unsupported_mode_hint(false))
.build();

// Custom limits + selective config
let bash = Bash::builder()
.typescript_with_config(
TypeScriptConfig::default()
.limits(TypeScriptLimits::default().max_duration(Duration::from_secs(5)))
.compat_aliases(false)
)
.build();
# }
```

### Unsupported Mode Hints

When enabled (default), using unsupported Node/Deno/Bun flags or subcommands
produces helpful guidance:

```text
$ node --inspect app.js
node: unsupported option or subcommand: --inspect
hint: This is an embedded TypeScript interpreter (ZapCode), not Node.js.
hint: Only inline execution is supported:
hint: node -e "console.log('hello')" # run inline code
hint: node script.js # run file from VFS
hint: echo "code" | node # pipe code via stdin
```

## LLM Tool Integration

When using `BashTool` for AI agents, call `.typescript()` on the tool builder:

```rust,ignore
use bashkit::{BashTool, Tool};

let tool = BashTool::builder()
.typescript()
.build();

// help() and system_prompt() automatically document TypeScript limitations
let help = tool.help();
```

The builtin's `llm_hint()` is automatically included in the tool's documentation,
so LLMs know not to generate code using `import`, `eval()`, or HTTP.

## Limitations

**No `import`/`require`.** ZapCode has no module system. All code runs in a
single scope.

**No `eval()`/`Function()`.** Dynamic code generation is blocked at the
language level.

**No HTTP/network.** No `fetch`, `XMLHttpRequest`, or network APIs. ZapCode
has no network primitives.

**No `process`/`Deno`/`Bun` globals.** Runtime-specific APIs are not available.
Only standard TypeScript/JavaScript language features work.

**No npm packages.** Only built-in language features and registered external
functions are available.

**stdout after VFS calls.** `console.log()` output after an `await readFile()`
or similar VFS call is not captured. Use the return-value pattern: make the
last expression the value you want printed.

## Security

All TypeScript execution runs in a virtual environment:

- **No host filesystem access** — all paths resolve through the VFS
- **No network access** — no sockets, HTTP, or DNS
- **No dynamic code execution** — `eval()`, `Function()`, `import` blocked
- **Resource limited** — time, memory, stack depth, and allocation caps
- **Path traversal safe** — `../..` is resolved by VFS path normalization
- **Opt-in only** — requires both `typescript` feature AND `.typescript()` builder call

See threat IDs TM-TS-001 through TM-TS-023 in the [threat model](./threat-model.md).

[spec]: https://github.com/everruns/bashkit/blob/main/specs/016-zapcode-runtime.md
Loading
Loading