@unchartedfr/zapcode 1.0.0-beta.3 → 1.1.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.
- package/README.md +209 -316
- package/npm/darwin-arm64/package.json +1 -1
- package/npm/darwin-arm64/zapcode.darwin-arm64.node +0 -0
- package/npm/darwin-x64/package.json +1 -1
- package/npm/darwin-x64/zapcode.darwin-x64.node +0 -0
- package/npm/linux-arm64-gnu/package.json +1 -1
- package/npm/linux-arm64-gnu/zapcode.linux-arm64-gnu.node +0 -0
- package/npm/linux-x64-gnu/package.json +1 -1
- package/npm/linux-x64-gnu/zapcode.linux-x64-gnu.node +0 -0
- package/npm/linux-x64-musl/package.json +1 -1
- package/npm/linux-x64-musl/zapcode.linux-x64-musl.node +0 -0
- package/npm/win32-arm64-msvc/package.json +1 -1
- package/npm/win32-arm64-msvc/zapcode.win32-arm64-msvc.node +0 -0
- package/npm/win32-x64-msvc/package.json +1 -1
- package/npm/win32-x64-msvc/zapcode.win32-x64-msvc.node +0 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,29 +1,61 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
3
|
-
<p align="center"><strong>Run AI code. Safely. Instantly.</strong></p>
|
|
4
|
-
<p align="center">A minimal, secure TypeScript interpreter written in Rust for use by AI</p>
|
|
2
|
+
<img src="assets/logo.png" alt="Zapcode" width="160" />
|
|
5
3
|
</p>
|
|
4
|
+
<h1 align="center">Zapcode</h1>
|
|
5
|
+
<p align="center"><strong>Run AI code. Safely. Instantly.</strong></p>
|
|
6
|
+
<p align="center">A minimal, secure TypeScript interpreter written in Rust for use by AI agents</p>
|
|
6
7
|
|
|
7
8
|
<p align="center">
|
|
8
|
-
<a href="https://github.com/TheUncharted/zapcode/actions"><img src="https://img.shields.io/github/actions/workflow/status/TheUncharted/zapcode/ci.yml?branch=
|
|
9
|
+
<a href="https://github.com/TheUncharted/zapcode/actions"><img src="https://img.shields.io/github/actions/workflow/status/TheUncharted/zapcode/ci.yml?branch=master&label=CI" alt="CI"></a>
|
|
10
|
+
<a href="https://crates.io/crates/zapcode-core"><img src="https://img.shields.io/crates/v/zapcode-core" alt="crates.io"></a>
|
|
9
11
|
<a href="https://www.npmjs.com/package/@unchartedfr/zapcode"><img src="https://img.shields.io/npm/v/@unchartedfr/zapcode" alt="npm"></a>
|
|
10
12
|
<a href="https://pypi.org/project/zapcode/"><img src="https://img.shields.io/pypi/v/zapcode" alt="PyPI"></a>
|
|
11
|
-
<a href="https://github.com/TheUncharted/zapcode/blob/
|
|
13
|
+
<a href="https://github.com/TheUncharted/zapcode/blob/master/LICENSE"><img src="https://img.shields.io/github/license/TheUncharted/zapcode" alt="License"></a>
|
|
12
14
|
</p>
|
|
13
15
|
|
|
14
16
|
---
|
|
15
17
|
|
|
16
18
|
> **Experimental** — Zapcode is under active development. APIs may change.
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## Why agents should write code
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
AI agents are more capable when they **write code** instead of chaining tool calls. Code gives agents loops, conditionals, variables, and composition — things that tool chains simulate poorly.
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
- [CodeMode](https://blog.cloudflare.com/codemode-ai-agent-coding) — Cloudflare on why agents should write code
|
|
25
|
+
- [Programmatic Tool Calling](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/tool-use-examples#programmatic-tool-calling) — Anthropic's approach
|
|
26
|
+
- [Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-mcp) — Anthropic engineering
|
|
27
|
+
- [Smol Agents](https://huggingface.co/docs/smolagents/en/index) — Hugging Face's code-first agents
|
|
28
|
+
|
|
29
|
+
**But running AI-generated code is dangerous and slow.**
|
|
30
|
+
|
|
31
|
+
Docker adds 200-500ms of cold-start latency and requires a container runtime. V8 isolates bring ~20MB of binary and millisecond startup. Neither supports snapshotting execution mid-function.
|
|
32
|
+
|
|
33
|
+
Zapcode takes a different approach: a purpose-built TypeScript interpreter that starts in **2 microseconds**, enforces a security sandbox at the language level, and can snapshot execution state to bytes for later resumption — all in a single, embeddable library with zero dependencies on Node.js or V8.
|
|
34
|
+
|
|
35
|
+
Inspired by [Monty](https://github.com/pydantic/monty), Pydantic's Python subset interpreter that takes the same approach for Python.
|
|
36
|
+
|
|
37
|
+
## Alternatives
|
|
38
|
+
|
|
39
|
+
| | Language completeness | Security | Startup | Snapshots | Setup |
|
|
40
|
+
|---|---|---|---|---|---|
|
|
41
|
+
| **Zapcode** | TypeScript subset | Language-level sandbox | **~2 µs** | Built-in, < 2 KB | `npm install` / `pip install` |
|
|
42
|
+
| Docker + Node.js | Full Node.js | Container isolation | ~200-500 ms | No | Container runtime |
|
|
43
|
+
| V8 Isolates | Full JS/TS | Isolate boundary | ~5-50 ms | No | V8 (~20 MB) |
|
|
44
|
+
| Deno Deploy | Full TS | Isolate + permissions | ~10-50 ms | No | Cloud service |
|
|
45
|
+
| QuickJS | Full ES2023 | Process isolation | ~1-5 ms | No | C library |
|
|
46
|
+
| WASI/Wasmer | Depends on guest | Wasm sandbox | ~1-10 ms | Possible | Wasm runtime |
|
|
47
|
+
|
|
48
|
+
### Why not Docker?
|
|
49
|
+
|
|
50
|
+
Docker provides strong isolation but adds hundreds of milliseconds of cold-start latency, requires a container runtime, and doesn't support snapshotting execution state mid-function. For AI agent loops that execute thousands of small code snippets, the overhead dominates.
|
|
51
|
+
|
|
52
|
+
### Why not V8?
|
|
53
|
+
|
|
54
|
+
V8 is the gold standard for JavaScript execution. But it brings ~20 MB of binary size, millisecond startup times, and a vast API surface that must be carefully restricted for sandboxing. If you need full ECMAScript compliance, use V8. If you need microsecond startup, byte-sized snapshots, and a security model where "blocked by default" is the foundation rather than an afterthought, use Zapcode.
|
|
23
55
|
|
|
24
56
|
## Benchmarks
|
|
25
57
|
|
|
26
|
-
All benchmarks run
|
|
58
|
+
All benchmarks run the full pipeline: parse → compile → execute. No caching, no warm-up.
|
|
27
59
|
|
|
28
60
|
| Benchmark | Zapcode | Docker + Node.js | V8 Isolate |
|
|
29
61
|
|---|---|---|---|
|
|
@@ -42,105 +74,184 @@ All benchmarks run on the full pipeline: parse → compile → execute. No cachi
|
|
|
42
74
|
|
|
43
75
|
No background thread, no GC, no runtime — CPU usage is exactly proportional to the instructions executed.
|
|
44
76
|
|
|
45
|
-
|
|
77
|
+
```bash
|
|
78
|
+
cargo bench # run benchmarks yourself
|
|
79
|
+
```
|
|
46
80
|
|
|
47
|
-
##
|
|
81
|
+
## Installation
|
|
48
82
|
|
|
49
|
-
|
|
83
|
+
**TypeScript / JavaScript**
|
|
84
|
+
```bash
|
|
85
|
+
npm install @unchartedfr/zapcode # npm / yarn / pnpm / bun
|
|
86
|
+
```
|
|
50
87
|
|
|
88
|
+
**Python**
|
|
51
89
|
```bash
|
|
52
|
-
|
|
90
|
+
pip install zapcode # pip / uv
|
|
53
91
|
```
|
|
54
92
|
|
|
55
|
-
|
|
93
|
+
**Rust**
|
|
94
|
+
```toml
|
|
95
|
+
# Cargo.toml
|
|
96
|
+
[dependencies]
|
|
97
|
+
zapcode-core = "1.0.0"
|
|
98
|
+
```
|
|
56
99
|
|
|
100
|
+
**WebAssembly**
|
|
57
101
|
```bash
|
|
58
|
-
|
|
59
|
-
curl -fsSL ... | bash -s -- --lang python # Python
|
|
60
|
-
curl -fsSL ... | bash -s -- --lang rust # Rust
|
|
61
|
-
curl -fsSL ... | bash -s -- --lang wasm # WebAssembly
|
|
102
|
+
wasm-pack build crates/zapcode-wasm --target web
|
|
62
103
|
```
|
|
63
104
|
|
|
64
|
-
|
|
105
|
+
## Basic Usage
|
|
65
106
|
|
|
66
|
-
|
|
67
|
-
<summary><strong>Rust</strong></summary>
|
|
107
|
+
### TypeScript / JavaScript
|
|
68
108
|
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
zapcode-core = { git = "https://github.com/TheUncharted/zapcode.git" }
|
|
72
|
-
```
|
|
73
|
-
</details>
|
|
109
|
+
```typescript
|
|
110
|
+
import { Zapcode, ZapcodeSnapshotHandle } from '@unchartedfr/zapcode';
|
|
74
111
|
|
|
75
|
-
|
|
76
|
-
|
|
112
|
+
// Simple expression
|
|
113
|
+
const b = new Zapcode('1 + 2 * 3');
|
|
114
|
+
console.log(b.run().output); // 7
|
|
77
115
|
|
|
78
|
-
|
|
116
|
+
// With inputs
|
|
117
|
+
const greeter = new Zapcode(
|
|
118
|
+
'`Hello, ${name}! You are ${age} years old.`',
|
|
119
|
+
{ inputs: ['name', 'age'] },
|
|
120
|
+
);
|
|
121
|
+
console.log(greeter.run({ name: 'Zapcode', age: 30 }).output);
|
|
79
122
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
// Data processing
|
|
124
|
+
const processor = new Zapcode(`
|
|
125
|
+
const items = [
|
|
126
|
+
{ name: "Widget", price: 25.99, qty: 3 },
|
|
127
|
+
{ name: "Gadget", price: 49.99, qty: 1 },
|
|
128
|
+
];
|
|
129
|
+
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
|
|
130
|
+
({ total, names: items.map(i => i.name) })
|
|
131
|
+
`);
|
|
132
|
+
console.log(processor.run().output);
|
|
133
|
+
// { total: 127.96, names: ["Widget", "Gadget"] }
|
|
134
|
+
|
|
135
|
+
// External function (snapshot/resume)
|
|
136
|
+
const app = new Zapcode(`const data = await fetch(url); data`, {
|
|
137
|
+
inputs: ['url'],
|
|
138
|
+
externalFunctions: ['fetch'],
|
|
139
|
+
});
|
|
140
|
+
const state = app.start({ url: 'https://api.example.com' });
|
|
141
|
+
if (!state.completed) {
|
|
142
|
+
console.log(state.functionName); // "fetch"
|
|
143
|
+
const snapshot = ZapcodeSnapshotHandle.load(state.snapshot);
|
|
144
|
+
const final_ = snapshot.resume({ status: 'ok' });
|
|
145
|
+
console.log(final_.output); // { status: "ok" }
|
|
146
|
+
}
|
|
85
147
|
```
|
|
86
148
|
|
|
87
|
-
|
|
149
|
+
See [`examples/typescript/basic.ts`](examples/typescript/basic.ts) for more.
|
|
88
150
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
151
|
+
### Python
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from zapcode import Zapcode, ZapcodeSnapshot
|
|
155
|
+
|
|
156
|
+
# Simple expression
|
|
157
|
+
b = Zapcode("1 + 2 * 3")
|
|
158
|
+
print(b.run()["output"]) # 7
|
|
159
|
+
|
|
160
|
+
# With inputs
|
|
161
|
+
b = Zapcode(
|
|
162
|
+
'`Hello, ${name}!`',
|
|
163
|
+
inputs=["name"],
|
|
164
|
+
)
|
|
165
|
+
print(b.run({"name": "Zapcode"})["output"]) # "Hello, Zapcode!"
|
|
166
|
+
|
|
167
|
+
# External function (snapshot/resume)
|
|
168
|
+
b = Zapcode(
|
|
169
|
+
"const w = await getWeather(city); `${city}: ${w.temp}°C`",
|
|
170
|
+
inputs=["city"],
|
|
171
|
+
external_functions=["getWeather"],
|
|
172
|
+
)
|
|
173
|
+
state = b.start({"city": "London"})
|
|
174
|
+
if state.get("suspended"):
|
|
175
|
+
result = state["snapshot"].resume({"condition": "Cloudy", "temp": 12})
|
|
176
|
+
print(result["output"]) # "London: 12°C"
|
|
93
177
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
178
|
+
# Snapshot persistence
|
|
179
|
+
state = b.start({"city": "Tokyo"})
|
|
180
|
+
if state.get("suspended"):
|
|
181
|
+
bytes_ = state["snapshot"].dump() # serialize to bytes
|
|
182
|
+
restored = ZapcodeSnapshot.load(bytes_) # load from bytes
|
|
183
|
+
result = restored.resume({"condition": "Clear", "temp": 26})
|
|
97
184
|
```
|
|
98
|
-
|
|
185
|
+
|
|
186
|
+
See [`examples/python/basic.py`](examples/python/basic.py) for more.
|
|
99
187
|
|
|
100
188
|
<details>
|
|
101
|
-
<summary><strong>
|
|
189
|
+
<summary><strong>Rust</strong></summary>
|
|
102
190
|
|
|
103
|
-
|
|
191
|
+
```rust
|
|
192
|
+
use zapcode_core::{ZapcodeRun, Value, ResourceLimits, VmState};
|
|
104
193
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
194
|
+
// Simple expression
|
|
195
|
+
let runner = ZapcodeRun::new(
|
|
196
|
+
"1 + 2 * 3".to_string(), vec![], vec![],
|
|
197
|
+
ResourceLimits::default(),
|
|
198
|
+
)?;
|
|
199
|
+
assert_eq!(runner.run_simple()?, Value::Int(7));
|
|
109
200
|
|
|
110
|
-
|
|
201
|
+
// With inputs and external functions (snapshot/resume)
|
|
202
|
+
let runner = ZapcodeRun::new(
|
|
203
|
+
r#"const weather = await getWeather(city);
|
|
204
|
+
`${city}: ${weather.condition}, ${weather.temp}°C`"#.to_string(),
|
|
205
|
+
vec!["city".to_string()],
|
|
206
|
+
vec!["getWeather".to_string()],
|
|
207
|
+
ResourceLimits::default(),
|
|
208
|
+
)?;
|
|
111
209
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
git clone https://github.com/TheUncharted/zapcode.git
|
|
116
|
-
cd zapcode/crates/zapcode-py
|
|
117
|
-
maturin develop --release --uv
|
|
210
|
+
let state = runner.start(vec![
|
|
211
|
+
("city".to_string(), Value::String("London".into())),
|
|
212
|
+
])?;
|
|
118
213
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
214
|
+
if let VmState::Suspended { snapshot, .. } = state {
|
|
215
|
+
let weather = Value::Object(indexmap::indexmap! {
|
|
216
|
+
"condition".into() => Value::String("Cloudy".into()),
|
|
217
|
+
"temp".into() => Value::Int(12),
|
|
218
|
+
});
|
|
219
|
+
let final_state = snapshot.resume(weather)?;
|
|
220
|
+
// VmState::Complete("London: Cloudy, 12°C")
|
|
221
|
+
}
|
|
124
222
|
```
|
|
223
|
+
|
|
224
|
+
See [`examples/rust/basic.rs`](examples/rust/basic.rs) for more.
|
|
125
225
|
</details>
|
|
126
226
|
|
|
127
227
|
<details>
|
|
128
|
-
<summary><strong>WebAssembly</strong></summary>
|
|
228
|
+
<summary><strong>WebAssembly (browser)</strong></summary>
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<script type="module">
|
|
232
|
+
import init, { Zapcode } from './zapcode-wasm/zapcode_wasm.js';
|
|
129
233
|
|
|
130
|
-
|
|
234
|
+
await init();
|
|
131
235
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
236
|
+
const b = new Zapcode(`
|
|
237
|
+
const items = [10, 20, 30];
|
|
238
|
+
items.map(x => x * 2).reduce((a, b) => a + b, 0)
|
|
239
|
+
`);
|
|
240
|
+
const result = b.run();
|
|
241
|
+
console.log(result.output); // 120
|
|
242
|
+
</script>
|
|
136
243
|
```
|
|
137
244
|
|
|
138
|
-
|
|
245
|
+
See [`examples/wasm/index.html`](examples/wasm/index.html) for a full playground.
|
|
139
246
|
</details>
|
|
140
247
|
|
|
141
|
-
## Usage
|
|
248
|
+
## AI Agent Usage
|
|
142
249
|
|
|
143
|
-
###
|
|
250
|
+
### Vercel AI SDK (@unchartedfr/zapcode-ai)
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm install @unchartedfr/zapcode-ai ai @ai-sdk/anthropic # or @ai-sdk/amazon-bedrock, @ai-sdk/openai
|
|
254
|
+
```
|
|
144
255
|
|
|
145
256
|
The recommended way — one call gives you `{ system, tools }` that plug directly into `generateText` / `streamText`:
|
|
146
257
|
|
|
@@ -187,10 +298,10 @@ Under the hood: the LLM writes TypeScript code that calls your tools → Zapcode
|
|
|
187
298
|
|
|
188
299
|
See [`examples/typescript/ai-agent-zapcode-ai.ts`](examples/typescript/ai-agent-zapcode-ai.ts) for the full working example.
|
|
189
300
|
|
|
190
|
-
### With Anthropic SDK directly
|
|
191
|
-
|
|
192
301
|
<details>
|
|
193
|
-
<summary><strong>
|
|
302
|
+
<summary><strong>Anthropic SDK</strong></summary>
|
|
303
|
+
|
|
304
|
+
**TypeScript:**
|
|
194
305
|
|
|
195
306
|
```typescript
|
|
196
307
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -225,11 +336,7 @@ while (!state.completed) {
|
|
|
225
336
|
console.log(state.output);
|
|
226
337
|
```
|
|
227
338
|
|
|
228
|
-
|
|
229
|
-
</details>
|
|
230
|
-
|
|
231
|
-
<details>
|
|
232
|
-
<summary><strong>Python</strong></summary>
|
|
339
|
+
**Python:**
|
|
233
340
|
|
|
234
341
|
```python
|
|
235
342
|
import anthropic
|
|
@@ -254,10 +361,11 @@ while state.get("suspended"):
|
|
|
254
361
|
print(state["output"])
|
|
255
362
|
```
|
|
256
363
|
|
|
257
|
-
See [`examples/python/ai_agent_anthropic.py`](examples/python/ai_agent_anthropic.py).
|
|
364
|
+
See [`examples/typescript/ai-agent-anthropic.ts`](examples/typescript/ai-agent-anthropic.ts) and [`examples/python/ai_agent_anthropic.py`](examples/python/ai_agent_anthropic.py).
|
|
258
365
|
</details>
|
|
259
366
|
|
|
260
|
-
|
|
367
|
+
<details>
|
|
368
|
+
<summary><strong>Multi-SDK support</strong></summary>
|
|
261
369
|
|
|
262
370
|
`zapcode()` returns adapters for all major AI SDKs from a single call:
|
|
263
371
|
|
|
@@ -278,32 +386,27 @@ await openai.chat.completions.create({
|
|
|
278
386
|
// Anthropic SDK
|
|
279
387
|
await anthropic.messages.create({ system, tools: anthropicTools, messages });
|
|
280
388
|
|
|
281
|
-
// Any SDK — just extract the
|
|
389
|
+
// Any SDK — just extract the code from the tool call and pass it to handleToolCall
|
|
282
390
|
const result = await handleToolCall(codeFromToolCall);
|
|
283
391
|
```
|
|
284
392
|
|
|
285
|
-
Python:
|
|
286
|
-
|
|
287
393
|
```python
|
|
288
394
|
b = zapcode(tools={...})
|
|
289
395
|
b.anthropic_tools # → Anthropic SDK format
|
|
290
396
|
b.openai_tools # → OpenAI SDK format
|
|
291
397
|
b.handle_tool_call(code) # → Universal handler
|
|
292
398
|
```
|
|
293
|
-
|
|
294
|
-
### Custom adapters
|
|
295
|
-
|
|
296
|
-
Building a new AI SDK or framework? You can write a custom adapter without forking Zapcode:
|
|
399
|
+
</details>
|
|
297
400
|
|
|
298
401
|
<details>
|
|
299
|
-
<summary><strong>
|
|
402
|
+
<summary><strong>Custom adapters</strong></summary>
|
|
403
|
+
|
|
404
|
+
Build a custom adapter for any AI SDK without forking Zapcode:
|
|
300
405
|
|
|
301
406
|
```typescript
|
|
302
407
|
import { zapcode, createAdapter } from "@unchartedfr/zapcode-ai";
|
|
303
408
|
|
|
304
|
-
// Create a typed adapter for your SDK
|
|
305
409
|
const myAdapter = createAdapter("my-sdk", (ctx) => {
|
|
306
|
-
// ctx gives you: system, toolName, toolDescription, toolSchema, handleToolCall
|
|
307
410
|
return {
|
|
308
411
|
systemMessage: ctx.system,
|
|
309
412
|
actions: [{
|
|
@@ -322,12 +425,7 @@ const { custom } = zapcode({
|
|
|
322
425
|
});
|
|
323
426
|
|
|
324
427
|
const myConfig = custom["my-sdk"];
|
|
325
|
-
// { systemMessage: "...", actions: [{ id: "execute_code", ... }] }
|
|
326
428
|
```
|
|
327
|
-
</details>
|
|
328
|
-
|
|
329
|
-
<details>
|
|
330
|
-
<summary><strong>Python</strong></summary>
|
|
331
429
|
|
|
332
430
|
```python
|
|
333
431
|
from zapcode_ai import zapcode, Adapter, AdapterContext
|
|
@@ -343,188 +441,25 @@ class LangChainAdapter(Adapter):
|
|
|
343
441
|
description=ctx.tool_description,
|
|
344
442
|
)
|
|
345
443
|
|
|
346
|
-
b = zapcode(
|
|
347
|
-
tools={...},
|
|
348
|
-
adapters=[LangChainAdapter()],
|
|
349
|
-
)
|
|
350
|
-
|
|
444
|
+
b = zapcode(tools={...}, adapters=[LangChainAdapter()])
|
|
351
445
|
langchain_tool = b.custom["langchain"]
|
|
352
446
|
```
|
|
353
|
-
</details>
|
|
354
447
|
|
|
355
448
|
The adapter receives an `AdapterContext` with everything needed: system prompt, tool name, tool JSON schema, and a `handleToolCall` function. Return whatever shape your SDK expects.
|
|
356
|
-
|
|
357
|
-
### Basic usage by language
|
|
358
|
-
|
|
359
|
-
<details>
|
|
360
|
-
<summary><strong>TypeScript / JavaScript</strong></summary>
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
import { Zapcode, ZapcodeSnapshotHandle } from '@unchartedfr/zapcode';
|
|
364
|
-
|
|
365
|
-
// Simple expression
|
|
366
|
-
const b = new Zapcode('1 + 2 * 3');
|
|
367
|
-
console.log(b.run().output); // 7
|
|
368
|
-
|
|
369
|
-
// With inputs
|
|
370
|
-
const greeter = new Zapcode(
|
|
371
|
-
'`Hello, ${name}! You are ${age} years old.`',
|
|
372
|
-
{ inputs: ['name', 'age'] },
|
|
373
|
-
);
|
|
374
|
-
console.log(greeter.run({ name: 'Zapcode', age: 30 }).output);
|
|
375
|
-
|
|
376
|
-
// Data processing
|
|
377
|
-
const processor = new Zapcode(`
|
|
378
|
-
const items = [
|
|
379
|
-
{ name: "Widget", price: 25.99, qty: 3 },
|
|
380
|
-
{ name: "Gadget", price: 49.99, qty: 1 },
|
|
381
|
-
];
|
|
382
|
-
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
|
|
383
|
-
({ total, names: items.map(i => i.name) })
|
|
384
|
-
`);
|
|
385
|
-
console.log(processor.run().output);
|
|
386
|
-
// { total: 127.96, names: ["Widget", "Gadget"] }
|
|
387
|
-
|
|
388
|
-
// External function (snapshot/resume)
|
|
389
|
-
const app = new Zapcode(`const data = await fetch(url); data`, {
|
|
390
|
-
inputs: ['url'],
|
|
391
|
-
externalFunctions: ['fetch'],
|
|
392
|
-
});
|
|
393
|
-
const state = app.start({ url: 'https://api.example.com' });
|
|
394
|
-
if (!state.completed) {
|
|
395
|
-
console.log(state.functionName); // "fetch"
|
|
396
|
-
const snapshot = ZapcodeSnapshotHandle.load(state.snapshot);
|
|
397
|
-
const final_ = snapshot.resume({ status: 'ok' });
|
|
398
|
-
console.log(final_.output); // { status: "ok" }
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Classes
|
|
402
|
-
const counter = new Zapcode(`
|
|
403
|
-
class Counter {
|
|
404
|
-
count: number;
|
|
405
|
-
constructor(start: number) { this.count = start; }
|
|
406
|
-
increment() { return ++this.count; }
|
|
407
|
-
}
|
|
408
|
-
const c = new Counter(10);
|
|
409
|
-
[c.increment(), c.increment(), c.increment()]
|
|
410
|
-
`);
|
|
411
|
-
console.log(counter.run().output); // [11, 12, 13]
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
See [`examples/typescript/basic.ts`](examples/typescript/basic.ts) for the full example.
|
|
415
|
-
</details>
|
|
416
|
-
|
|
417
|
-
<details>
|
|
418
|
-
<summary><strong>Rust</strong></summary>
|
|
419
|
-
|
|
420
|
-
```rust
|
|
421
|
-
use zapcode_core::{ZapcodeRun, Value, ResourceLimits, VmState};
|
|
422
|
-
|
|
423
|
-
// Simple expression
|
|
424
|
-
let runner = ZapcodeRun::new(
|
|
425
|
-
"1 + 2 * 3".to_string(), vec![], vec![],
|
|
426
|
-
ResourceLimits::default(),
|
|
427
|
-
)?;
|
|
428
|
-
assert_eq!(runner.run_simple()?, Value::Int(7));
|
|
429
|
-
|
|
430
|
-
// With inputs and external functions (snapshot/resume)
|
|
431
|
-
let runner = ZapcodeRun::new(
|
|
432
|
-
r#"const weather = await getWeather(city);
|
|
433
|
-
`${city}: ${weather.condition}, ${weather.temp}°C`"#.to_string(),
|
|
434
|
-
vec!["city".to_string()],
|
|
435
|
-
vec!["getWeather".to_string()],
|
|
436
|
-
ResourceLimits::default(),
|
|
437
|
-
)?;
|
|
438
|
-
|
|
439
|
-
let state = runner.start(vec![
|
|
440
|
-
("city".to_string(), Value::String("London".into())),
|
|
441
|
-
])?;
|
|
442
|
-
|
|
443
|
-
if let VmState::Suspended { snapshot, .. } = state {
|
|
444
|
-
let weather = Value::Object(indexmap::indexmap! {
|
|
445
|
-
"condition".into() => Value::String("Cloudy".into()),
|
|
446
|
-
"temp".into() => Value::Int(12),
|
|
447
|
-
});
|
|
448
|
-
let final_state = snapshot.resume(weather)?;
|
|
449
|
-
// VmState::Complete("London: Cloudy, 12°C")
|
|
450
|
-
}
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
See [`examples/rust/basic.rs`](examples/rust/basic.rs) for the full example.
|
|
454
|
-
</details>
|
|
455
|
-
|
|
456
|
-
<details>
|
|
457
|
-
<summary><strong>Python</strong></summary>
|
|
458
|
-
|
|
459
|
-
```python
|
|
460
|
-
from zapcode import Zapcode, ZapcodeSnapshot
|
|
461
|
-
|
|
462
|
-
# Simple expression
|
|
463
|
-
b = Zapcode("1 + 2 * 3")
|
|
464
|
-
print(b.run()["output"]) # 7
|
|
465
|
-
|
|
466
|
-
# With inputs
|
|
467
|
-
b = Zapcode(
|
|
468
|
-
'`Hello, ${name}!`',
|
|
469
|
-
inputs=["name"],
|
|
470
|
-
)
|
|
471
|
-
print(b.run({"name": "Zapcode"})["output"]) # "Hello, Zapcode!"
|
|
472
|
-
|
|
473
|
-
# External function (snapshot/resume)
|
|
474
|
-
b = Zapcode(
|
|
475
|
-
"const w = await getWeather(city); `${city}: ${w.temp}°C`",
|
|
476
|
-
inputs=["city"],
|
|
477
|
-
external_functions=["getWeather"],
|
|
478
|
-
)
|
|
479
|
-
state = b.start({"city": "London"})
|
|
480
|
-
if state.get("suspended"):
|
|
481
|
-
result = state["snapshot"].resume({"condition": "Cloudy", "temp": 12})
|
|
482
|
-
print(result["output"]) # "London: 12°C"
|
|
483
|
-
|
|
484
|
-
# Snapshot persistence
|
|
485
|
-
state = b.start({"city": "Tokyo"})
|
|
486
|
-
if state.get("suspended"):
|
|
487
|
-
bytes_ = state["snapshot"].dump() # serialize to bytes
|
|
488
|
-
restored = ZapcodeSnapshot.load(bytes_) # load from bytes
|
|
489
|
-
result = restored.resume({"condition": "Clear", "temp": 26})
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
See [`examples/python/basic.py`](examples/python/basic.py) for the full example.
|
|
493
449
|
</details>
|
|
494
450
|
|
|
495
|
-
|
|
496
|
-
<summary><strong>WebAssembly (browser)</strong></summary>
|
|
451
|
+
## What Zapcode Can and Cannot Do
|
|
497
452
|
|
|
498
|
-
|
|
499
|
-
<script type="module">
|
|
500
|
-
import init, { Zapcode } from './zapcode-wasm/zapcode_wasm.js';
|
|
453
|
+
**Can do:**
|
|
501
454
|
|
|
502
|
-
await
|
|
455
|
+
- Execute a useful subset of TypeScript — variables, functions, classes, generators, async/await, closures, destructuring, spread/rest, optional chaining, nullish coalescing, template literals, try/catch
|
|
456
|
+
- Strip TypeScript types at parse time via [oxc](https://oxc.rs) — no `tsc` needed
|
|
457
|
+
- Snapshot execution to bytes and resume later, even in a different process or machine
|
|
458
|
+
- Call from Rust, Node.js, Python, or WebAssembly
|
|
459
|
+
- Track and limit resources — memory, allocations, stack depth, and wall-clock time
|
|
460
|
+
- 30+ string methods, 25+ array methods, plus Math, JSON, Object, and Promise builtins
|
|
503
461
|
|
|
504
|
-
|
|
505
|
-
const items = [10, 20, 30];
|
|
506
|
-
items.map(x => x * 2).reduce((a, b) => a + b, 0)
|
|
507
|
-
`);
|
|
508
|
-
const result = b.run();
|
|
509
|
-
console.log(result.output); // 120
|
|
510
|
-
</script>
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
See [`examples/wasm/index.html`](examples/wasm/index.html) for a full playground.
|
|
514
|
-
</details>
|
|
515
|
-
|
|
516
|
-
## What Zapcode can and cannot do
|
|
517
|
-
|
|
518
|
-
### Can do
|
|
519
|
-
|
|
520
|
-
- **Execute a useful subset of TypeScript** — variables, functions, classes, generators, async/await, closures, destructuring, spread/rest, optional chaining, nullish coalescing, template literals, try/catch
|
|
521
|
-
- **Strip TypeScript types** at parse time via [oxc](https://oxc.rs) — no `tsc` needed
|
|
522
|
-
- **Snapshot execution to bytes** and resume later, even in a different process or machine
|
|
523
|
-
- **Call from Rust, Node.js, Python, or WebAssembly**
|
|
524
|
-
- **Track and limit resources** — memory, allocations, stack depth, and wall-clock time
|
|
525
|
-
- **30+ string methods, 25+ array methods**, plus Math, JSON, Object, and Promise builtins
|
|
526
|
-
|
|
527
|
-
### Cannot do
|
|
462
|
+
**Cannot do:**
|
|
528
463
|
|
|
529
464
|
- Run arbitrary npm packages or the full Node.js standard library
|
|
530
465
|
- Execute regular expressions (parsing supported, execution is a no-op)
|
|
@@ -533,8 +468,7 @@ See [`examples/wasm/index.html`](examples/wasm/index.html) for a full playground
|
|
|
533
468
|
|
|
534
469
|
These are intentional constraints, not bugs. Zapcode targets one use case: **running code written by AI agents** inside a secure, embeddable sandbox.
|
|
535
470
|
|
|
536
|
-
|
|
537
|
-
<summary><strong>Full supported syntax table</strong></summary>
|
|
471
|
+
## Supported Syntax
|
|
538
472
|
|
|
539
473
|
| Feature | Status |
|
|
540
474
|
|---|---|
|
|
@@ -560,26 +494,6 @@ These are intentional constraints, not bugs. Zapcode targets one use case: **run
|
|
|
560
494
|
| `var` declarations | Not supported (use `let`/`const`) |
|
|
561
495
|
| Decorators | Not supported |
|
|
562
496
|
| `Symbol`, `WeakMap`, `WeakSet` | Not supported |
|
|
563
|
-
</details>
|
|
564
|
-
|
|
565
|
-
## Alternatives
|
|
566
|
-
|
|
567
|
-
| | Language completeness | Security | Startup | Snapshots | Setup |
|
|
568
|
-
|---|---|---|---|---|---|
|
|
569
|
-
| **Zapcode** | TypeScript subset | Language-level sandbox | **~2 µs** | Built-in, < 2 KB | `cargo add` / `npm install` |
|
|
570
|
-
| Docker + Node.js | Full Node.js | Container isolation | ~200-500 ms | No | Container runtime |
|
|
571
|
-
| V8 Isolates | Full JS/TS | Isolate boundary | ~5-50 ms | No | V8 (~20 MB) |
|
|
572
|
-
| Deno Deploy | Full TS | Isolate + permissions | ~10-50 ms | No | Cloud service |
|
|
573
|
-
| QuickJS | Full ES2023 | Process isolation | ~1-5 ms | No | C library |
|
|
574
|
-
| WASI/Wasmer | Depends on guest | Wasm sandbox | ~1-10 ms | Possible | Wasm runtime |
|
|
575
|
-
|
|
576
|
-
### Why not Docker?
|
|
577
|
-
|
|
578
|
-
Docker provides strong isolation but adds hundreds of milliseconds of cold-start latency, requires a container runtime, and doesn't support snapshotting execution state mid-function. For AI agent loops that execute thousands of small code snippets, the overhead dominates.
|
|
579
|
-
|
|
580
|
-
### Why not V8?
|
|
581
|
-
|
|
582
|
-
V8 is the gold standard for JavaScript execution. But it brings ~20 MB of binary size, millisecond startup times, and a vast API surface that must be carefully restricted for sandboxing. If you need full ECMAScript compliance, use V8. If you need microsecond startup, byte-sized snapshots, and a security model where "blocked by default" is the foundation rather than an afterthought, use Zapcode.
|
|
583
497
|
|
|
584
498
|
## Security
|
|
585
499
|
|
|
@@ -610,17 +524,13 @@ The **only** escape hatch is external functions that you explicitly register. Wh
|
|
|
610
524
|
| Call stack depth | 512 frames | `max_stack_depth` |
|
|
611
525
|
| Heap allocations | 100,000 | `max_allocations` |
|
|
612
526
|
|
|
613
|
-
Limits are checked during execution, so infinite loops, deep recursion, and allocation bombs are all caught.
|
|
614
|
-
|
|
615
527
|
### Zero `unsafe` code
|
|
616
528
|
|
|
617
|
-
The `zapcode-core` crate contains **zero `unsafe` blocks**. Memory safety is guaranteed by the Rust compiler.
|
|
529
|
+
The `zapcode-core` crate contains **zero `unsafe` blocks**. Memory safety is guaranteed by the Rust compiler.
|
|
618
530
|
|
|
619
531
|
<details>
|
|
620
532
|
<summary><strong>Adversarial test suite — 65 tests across 19 attack categories</strong></summary>
|
|
621
533
|
|
|
622
|
-
The sandbox is validated by **65 adversarial security tests** (`tests/security.rs`) that simulate real attack scenarios:
|
|
623
|
-
|
|
624
534
|
| Attack category | Tests | Result |
|
|
625
535
|
|---|---|---|
|
|
626
536
|
| Prototype pollution (`Object.prototype`, `__proto__`) | 4 | Blocked |
|
|
@@ -643,7 +553,9 @@ The sandbox is validated by **65 adversarial security tests** (`tests/security.r
|
|
|
643
553
|
| `setTimeout`, `setInterval`, `Proxy`, `Reflect` | 6 | Blocked |
|
|
644
554
|
| `with` statement, `arguments.callee` | 3 | Blocked |
|
|
645
555
|
|
|
646
|
-
|
|
556
|
+
```bash
|
|
557
|
+
cargo test -p zapcode-core --test security # run the security tests
|
|
558
|
+
```
|
|
647
559
|
|
|
648
560
|
**Known limitations:**
|
|
649
561
|
- `Object.freeze()` is not yet implemented — frozen objects can still be mutated (correctness gap, not a sandbox escape)
|
|
@@ -680,28 +592,9 @@ TypeScript source
|
|
|
680
592
|
```bash
|
|
681
593
|
git clone https://github.com/TheUncharted/zapcode.git
|
|
682
594
|
cd zapcode
|
|
683
|
-
|
|
684
|
-
# Run all tests (214 tests)
|
|
685
|
-
cargo test
|
|
686
|
-
|
|
687
|
-
# Run benchmarks
|
|
688
|
-
cargo bench
|
|
689
|
-
|
|
690
|
-
# Check all crates (including bindings)
|
|
691
|
-
cargo check --workspace
|
|
595
|
+
./scripts/dev-setup.sh # installs toolchain, builds, runs tests
|
|
692
596
|
```
|
|
693
597
|
|
|
694
|
-
## Why AI agents should write code
|
|
695
|
-
|
|
696
|
-
For motivation on why you might want LLMs to write and execute code instead of chaining tool calls:
|
|
697
|
-
|
|
698
|
-
- [CodeMode](https://blog.cloudflare.com/codemode-ai-agent-coding) from Cloudflare
|
|
699
|
-
- [Programmatic Tool Calling](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/tool-use-examples#programmatic-tool-calling) from Anthropic
|
|
700
|
-
- [Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-mcp) from Anthropic
|
|
701
|
-
- [Smol Agents](https://huggingface.co/docs/smolagents/en/index) from Hugging Face
|
|
702
|
-
|
|
703
|
-
Zapcode is inspired by [Monty](https://github.com/pydantic/monty), Pydantic's Python subset interpreter that takes the same approach for Python.
|
|
704
|
-
|
|
705
598
|
## License
|
|
706
599
|
|
|
707
600
|
MIT
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchartedfr/zapcode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A minimal, secure TypeScript interpreter for AI agents — Node.js bindings",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.js",
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"@napi-rs/cli": "^3.5.1"
|
|
63
63
|
},
|
|
64
64
|
"optionalDependencies": {
|
|
65
|
-
"@unchartedfr/zapcode-linux-x64-gnu": "1.
|
|
66
|
-
"@unchartedfr/zapcode-linux-x64-musl": "1.
|
|
67
|
-
"@unchartedfr/zapcode-linux-arm64-gnu": "1.
|
|
68
|
-
"@unchartedfr/zapcode-darwin-x64": "1.
|
|
69
|
-
"@unchartedfr/zapcode-darwin-arm64": "1.
|
|
70
|
-
"@unchartedfr/zapcode-win32-x64-msvc": "1.
|
|
71
|
-
"@unchartedfr/zapcode-win32-arm64-msvc": "1.
|
|
65
|
+
"@unchartedfr/zapcode-linux-x64-gnu": "1.1.0",
|
|
66
|
+
"@unchartedfr/zapcode-linux-x64-musl": "1.1.0",
|
|
67
|
+
"@unchartedfr/zapcode-linux-arm64-gnu": "1.1.0",
|
|
68
|
+
"@unchartedfr/zapcode-darwin-x64": "1.1.0",
|
|
69
|
+
"@unchartedfr/zapcode-darwin-arm64": "1.1.0",
|
|
70
|
+
"@unchartedfr/zapcode-win32-x64-msvc": "1.1.0",
|
|
71
|
+
"@unchartedfr/zapcode-win32-arm64-msvc": "1.1.0"
|
|
72
72
|
}
|
|
73
73
|
}
|