Skip to content

Commit 5e2e7ea

Browse files
authored
feat(builtins): add embedded TypeScript/JS runtime via ZapCode (#940)
Add embedded TypeScript/JavaScript execution powered by zapcode-core, a pure-Rust TS interpreter with ~2µs cold start and no V8 dependency. - Feature-gated (`typescript`) and opt-in via builder (`.typescript()`) - Commands: ts, typescript, node, deno, bun (compat aliases configurable) - VFS bridging: readFile, writeFile, exists, readDir, mkdir, remove, stat - Resource limits: TypeScriptLimits (duration, memory, stack depth, allocations) - TypeScriptConfig: compat_aliases toggle, unsupported-mode hints - External function support for host-provided capabilities - 110+ tests: unit, security (50), integration (14), spec (45), threat model (8) - Spec: specs/016-zapcode-runtime.md - Threat model: TM-TS section with 23 threat entries - Docs: README, rustdoc guide, public-facing guide
1 parent 20aff9a commit 5e2e7ea

20 files changed

+4349
-2
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ jobs:
118118
cargo run --example live_mounts
119119
cargo run --example git_workflow --features git
120120
cargo run --example python_external_functions --features python
121+
cargo run --example typescript_external_functions --features typescript
121122
cargo run --example realfs_readonly --features realfs
122123
cargo run --example realfs_readwrite --features realfs
123124

AGENTS.md

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

4647
### Documentation
4748

README.md

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

2930
## Install
3031

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

134136
## Shell Features
135137

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

253+
## Experimental: TypeScript Support
254+
255+
Enable the `typescript` feature to embed the [ZapCode](https://github.com/TheUncharted/zapcode) TypeScript interpreter (pure Rust, no V8).
256+
TypeScript code runs in-memory with configurable resource limits and VFS bridging via external function suspend/resume.
257+
258+
```toml
259+
[dependencies]
260+
bashkit = { version = "0.1", features = ["typescript"] }
261+
```
262+
263+
```rust
264+
use bashkit::Bash;
265+
266+
let mut bash = Bash::builder().typescript().build();
267+
268+
// Inline code (ts, node, deno, bun aliases all work)
269+
bash.exec("ts -c \"console.log(2 ** 10)\"").await?;
270+
bash.exec("node -e \"console.log('hello')\"").await?;
271+
272+
// Script files from VFS
273+
bash.exec("ts /tmp/script.ts").await?;
274+
275+
// VFS bridging: readFile/writeFile async functions
276+
bash.exec(r#"ts -c "await writeFile('/tmp/data.txt', 'hello from ts')"#).await?;
277+
bash.exec("cat /tmp/data.txt").await?; // "hello from ts"
278+
```
279+
280+
Compat aliases (`node`, `deno`, `bun`) and unsupported-mode hints are configurable:
281+
282+
```rust
283+
use bashkit::{Bash, TypeScriptConfig};
284+
285+
// Only ts/typescript, no compat aliases
286+
let bash = Bash::builder()
287+
.typescript_with_config(TypeScriptConfig::default().compat_aliases(false))
288+
.build();
289+
```
290+
291+
Limitations: no `import`/`require`, no `eval()`, no network, no `process`/`Deno`/`Bun` globals.
292+
See [crates/bashkit/docs/typescript.md](crates/bashkit/docs/typescript.md) for the full guide.
293+
251294
## Virtual Filesystem
252295

253296
```rust

crates/bashkit/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ tracing = { workspace = true, optional = true }
6969
# Embedded Python interpreter (optional)
7070
monty = { git = "https://github.com/pydantic/monty", rev = "e59c8fa", optional = true }
7171

72+
# Embedded TypeScript interpreter (optional)
73+
zapcode-core = { version = "1.5", optional = true }
74+
7275
[features]
7376
default = []
7477
http_client = ["reqwest"]
@@ -89,6 +92,9 @@ scripted_tool = []
8992
# Enable python/python3 builtins via embedded Monty interpreter
9093
# Monty is a git dep (not yet on crates.io) — feature unavailable from registry
9194
python = ["dep:monty"]
95+
# Enable ts/node/deno/bun builtins via embedded ZapCode TypeScript interpreter
96+
# Usage: cargo build --features typescript
97+
typescript = ["dep:zapcode-core"]
9298
# Enable RealFs backend for accessing host filesystem directories
9399
# WARNING: This intentionally breaks the sandbox boundary.
94100
# Usage: cargo build --features realfs
@@ -127,6 +133,14 @@ required-features = ["python"]
127133
name = "python_external_functions"
128134
required-features = ["python"]
129135

136+
[[example]]
137+
name = "typescript_scripts"
138+
required-features = ["typescript"]
139+
140+
[[example]]
141+
name = "typescript_external_functions"
142+
required-features = ["typescript"]
143+
130144
[[example]]
131145
name = "realfs_readonly"
132146
required-features = ["realfs"]

crates/bashkit/docs/typescript.md

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Embedded TypeScript (ZapCode)
2+
3+
> **Experimental.** ZapCode is an early-stage TypeScript interpreter that may
4+
> have undiscovered crash or security bugs. Resource limits are enforced by
5+
> ZapCode's VM. The integration should be treated as experimental.
6+
7+
Bashkit embeds the [ZapCode](https://github.com/TheUncharted/zapcode) TypeScript
8+
interpreter, a pure-Rust implementation with ~2µs cold start, no V8 dependency,
9+
and built-in sandboxing. TypeScript runs entirely in-memory with configurable
10+
resource limits and no host access.
11+
12+
**See also:**
13+
- [Threat Model](./threat-model.md) - Security considerations (TM-TS-*)
14+
- [Custom Builtins](./custom_builtins.md) - Writing your own builtins
15+
- [Compatibility Reference](./compatibility.md) - Bash feature support
16+
- [`specs/016-zapcode-runtime.md`][spec] - Full specification
17+
18+
## Quick Start
19+
20+
Enable the `typescript` feature and register via builder:
21+
22+
```rust
23+
use bashkit::Bash;
24+
25+
# #[tokio::main]
26+
# async fn main() -> bashkit::Result<()> {
27+
let mut bash = Bash::builder().typescript().build();
28+
29+
let result = bash.exec("ts -c \"console.log('hello from ZapCode')\"").await?;
30+
assert_eq!(result.stdout, "hello from ZapCode\n");
31+
# Ok(())
32+
# }
33+
```
34+
35+
## Usage Patterns
36+
37+
### Inline Code
38+
39+
```bash
40+
ts -c "console.log(2 ** 10)"
41+
# Output: 1024
42+
43+
# Node.js, Deno, and Bun aliases also work
44+
node -e "console.log('hello')"
45+
deno -e "console.log('hello')"
46+
bun -e "console.log('hello')"
47+
```
48+
49+
### Expression Evaluation
50+
51+
When no `console.log()` is called, the last expression is displayed (REPL behavior):
52+
53+
```bash
54+
ts -c "1 + 2 * 3"
55+
# Output: 7
56+
```
57+
58+
### Script Files (from VFS)
59+
60+
```bash
61+
cat > /tmp/script.ts << 'EOF'
62+
const data = [1, 2, 3, 4, 5];
63+
const sum = data.reduce((a, b) => a + b, 0);
64+
console.log(`sum=${sum}, avg=${sum / data.length}`);
65+
EOF
66+
ts /tmp/script.ts
67+
```
68+
69+
### Pipelines and Command Substitution
70+
71+
```bash
72+
result=$(ts -c "console.log(42 * 3)")
73+
echo "Result: $result"
74+
75+
echo "console.log('piped')" | ts
76+
```
77+
78+
## Virtual Filesystem (VFS) Bridging
79+
80+
VFS operations are available as async global functions in the TypeScript
81+
environment. Files created by bash are readable from TypeScript and vice versa.
82+
83+
### Bash → TypeScript
84+
85+
```bash
86+
echo "important data" > /tmp/shared.txt
87+
ts -c "await readFile('/tmp/shared.txt')"
88+
# Output: important data
89+
```
90+
91+
### TypeScript → Bash
92+
93+
```bash
94+
ts -c "await writeFile('/tmp/result.txt', 'computed by ts\n')"
95+
cat /tmp/result.txt
96+
# Output: computed by ts
97+
```
98+
99+
### Supported VFS Operations
100+
101+
| Operation | Function | Return |
102+
|-----------|----------|--------|
103+
| Read file | `readFile(path)` | `string` |
104+
| Write file | `writeFile(path, content)` | `void` |
105+
| Check exists | `exists(path)` | `boolean` |
106+
| List directory | `readDir(path)` | `string[]` |
107+
| Create directory | `mkdir(path)` | `void` |
108+
| Delete | `remove(path)` | `void` |
109+
| File metadata | `stat(path)` | JSON string |
110+
111+
### Architecture
112+
113+
```text
114+
TS code → ZapCode VM → ExternalFn("readFile", [path]) → Bashkit VFS → resume
115+
```
116+
117+
ZapCode suspends at external function calls, Bashkit bridges them to the VFS,
118+
then resumes execution with the return value.
119+
120+
**Note:** `console.log()` output produced *after* a VFS call is not captured
121+
due to a `zapcode-core` API limitation. Use the return-value pattern instead —
122+
the last expression's value is printed automatically.
123+
124+
## Resource Limits
125+
126+
Default limits prevent runaway TypeScript code. Customize via `TypeScriptLimits`:
127+
128+
```rust,no_run
129+
use bashkit::{Bash, TypeScriptLimits};
130+
use std::time::Duration;
131+
132+
# fn main() {
133+
let bash = Bash::builder()
134+
.typescript_with_limits(
135+
TypeScriptLimits::default()
136+
.max_duration(Duration::from_secs(5))
137+
.max_memory(16 * 1024 * 1024) // 16 MB
138+
.max_allocations(100_000)
139+
.max_stack_depth(100)
140+
)
141+
.build();
142+
# }
143+
```
144+
145+
| Limit | Default | Purpose |
146+
|-------|---------|---------|
147+
| Duration | 30 seconds | Execution timeout |
148+
| Memory | 64 MB | Heap memory cap |
149+
| Stack depth | 512 | Call stack depth |
150+
| Allocations | 1,000,000 | Heap allocation cap |
151+
152+
## Configuration
153+
154+
Use `TypeScriptConfig` for full control over aliases and hint behavior:
155+
156+
```rust,no_run
157+
use bashkit::{Bash, TypeScriptConfig, TypeScriptLimits};
158+
use std::time::Duration;
159+
160+
# fn main() {
161+
// Default: ts, typescript, node, deno, bun + unsupported-mode hints
162+
let bash = Bash::builder().typescript().build();
163+
164+
// Only ts/typescript commands, no node/deno/bun aliases
165+
let bash = Bash::builder()
166+
.typescript_with_config(TypeScriptConfig::default().compat_aliases(false))
167+
.build();
168+
169+
// Disable unsupported-mode hints (plain errors only)
170+
let bash = Bash::builder()
171+
.typescript_with_config(TypeScriptConfig::default().unsupported_mode_hint(false))
172+
.build();
173+
174+
// Custom limits + selective config
175+
let bash = Bash::builder()
176+
.typescript_with_config(
177+
TypeScriptConfig::default()
178+
.limits(TypeScriptLimits::default().max_duration(Duration::from_secs(5)))
179+
.compat_aliases(false)
180+
)
181+
.build();
182+
# }
183+
```
184+
185+
### Unsupported Mode Hints
186+
187+
When enabled (default), using unsupported Node/Deno/Bun flags or subcommands
188+
produces helpful guidance:
189+
190+
```text
191+
$ node --inspect app.js
192+
node: unsupported option or subcommand: --inspect
193+
hint: This is an embedded TypeScript interpreter (ZapCode), not Node.js.
194+
hint: Only inline execution is supported:
195+
hint: node -e "console.log('hello')" # run inline code
196+
hint: node script.js # run file from VFS
197+
hint: echo "code" | node # pipe code via stdin
198+
```
199+
200+
## LLM Tool Integration
201+
202+
When using `BashTool` for AI agents, call `.typescript()` on the tool builder:
203+
204+
```rust,ignore
205+
use bashkit::{BashTool, Tool};
206+
207+
let tool = BashTool::builder()
208+
.typescript()
209+
.build();
210+
211+
// help() and system_prompt() automatically document TypeScript limitations
212+
let help = tool.help();
213+
```
214+
215+
The builtin's `llm_hint()` is automatically included in the tool's documentation,
216+
so LLMs know not to generate code using `import`, `eval()`, or HTTP.
217+
218+
## Limitations
219+
220+
**No `import`/`require`.** ZapCode has no module system. All code runs in a
221+
single scope.
222+
223+
**No `eval()`/`Function()`.** Dynamic code generation is blocked at the
224+
language level.
225+
226+
**No HTTP/network.** No `fetch`, `XMLHttpRequest`, or network APIs. ZapCode
227+
has no network primitives.
228+
229+
**No `process`/`Deno`/`Bun` globals.** Runtime-specific APIs are not available.
230+
Only standard TypeScript/JavaScript language features work.
231+
232+
**No npm packages.** Only built-in language features and registered external
233+
functions are available.
234+
235+
**stdout after VFS calls.** `console.log()` output after an `await readFile()`
236+
or similar VFS call is not captured. Use the return-value pattern: make the
237+
last expression the value you want printed.
238+
239+
## Security
240+
241+
All TypeScript execution runs in a virtual environment:
242+
243+
- **No host filesystem access** — all paths resolve through the VFS
244+
- **No network access** — no sockets, HTTP, or DNS
245+
- **No dynamic code execution**`eval()`, `Function()`, `import` blocked
246+
- **Resource limited** — time, memory, stack depth, and allocation caps
247+
- **Path traversal safe**`../..` is resolved by VFS path normalization
248+
- **Opt-in only** — requires both `typescript` feature AND `.typescript()` builder call
249+
250+
See threat IDs TM-TS-001 through TM-TS-023 in the [threat model](./threat-model.md).
251+
252+
[spec]: https://github.com/everruns/bashkit/blob/main/specs/016-zapcode-runtime.md

0 commit comments

Comments
 (0)