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