|
| 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