@directive-run/sandbox 0.4.0 → 0.4.1
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 +13 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Execute Directive snippets server-side and return a structured transcript (logs,
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
Takes a TypeScript snippet (single source string OR a paired library + runner `files: [{path, source}]` array
|
|
7
|
+
Takes a TypeScript snippet (single source string OR a paired library + runner `files: [{path, source}]` array – same shape `playground_link` accepts), validates it against an AST allowlist, bundles the multi-file payload via esbuild, and executes the result in a bounded `worker_threads` sandbox. Returns:
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
interface SandboxResult {
|
|
@@ -16,22 +16,22 @@ interface SandboxResult {
|
|
|
16
16
|
}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The bundler injects an early-capture immediately after `createSystem(...)`, so the post-mortem snapshot survives mid-runner errors
|
|
19
|
+
The bundler injects an early-capture immediately after `createSystem(...)`, so the post-mortem snapshot survives mid-runner errors – a validation throw inside `await system.settle()` still hands you the init-state facts.
|
|
20
20
|
|
|
21
21
|
## Sandbox boundary
|
|
22
22
|
|
|
23
23
|
Three layers:
|
|
24
24
|
|
|
25
25
|
1. **AST allowlist validator** (`ts-morph`). Imports are restricted to a curated `@directive-run/*` set + relative `./*.js`:
|
|
26
|
-
- **Allowed:** `core`, `ai`, `query`, `react`, `vue`, `svelte`, `solid`, `lit`, `el`, `optimistic`, `timeline`, `mutator`, `knowledge`, `scaffold`, `claude-plugin`, `lint`, `sources` (17 packages
|
|
27
|
-
- **Denied:** `cli`, `mcp`, `sandbox`, `vite-plugin-api-proxy` (build / CLI / sandbox-meta tooling
|
|
26
|
+
- **Allowed:** `core`, `ai`, `query`, `react`, `vue`, `svelte`, `solid`, `lit`, `el`, `optimistic`, `timeline`, `mutator`, `knowledge`, `scaffold`, `claude-plugin`, `lint`, `sources` (17 packages – anything an end-user demo realistically composes from; the canonical, drift-proof list is `ALLOWED_DIRECTIVE_PACKAGES` in `src/validator.ts`).
|
|
27
|
+
- **Denied:** `cli`, `mcp`, `sandbox`, `vite-plugin-api-proxy` (build / CLI / sandbox-meta tooling – no legitimate use inside a sandboxed demo).
|
|
28
28
|
- Everything else (`node:fs`, `express`, `@sizls/*`, etc.) is rejected.
|
|
29
29
|
- **Identifier references** to FS / network / eval surfaces (`process`, `require`, `fetch`, `fs`, `child_process`, `eval`, `new Function`, `setTimeout`, `Buffer`, etc.) are rejected as free identifiers.
|
|
30
30
|
- **Property-access bypass chains** are rejected (v0.3.0): `globalThis.process`, `globalThis.fetch`, `globalThis["X"]` bracket access with string literal, `.constructor` access on any value, `Function(...)` call without `new`, `Reflect.get/has/getOwnPropertyDescriptor(globalThis, "X")` smuggle chains. The [June 2026 security audit](../../docs/security/sandbox-audit-2026-06.md) traces these PoCs and how v0.3.0 closes them.
|
|
31
31
|
2. **esbuild bundler**. Virtualizes the multi-file payload into a single ESM string with `@directive-run/*` externalized. Throws on imports that can't be resolved against the in-memory file map.
|
|
32
|
-
3. **`worker_threads.Worker`** with `resourceLimits` (32 MB heap, 16 MB code) and a clamped wall-clock budget (`[100ms, 10s]`, default 5s). The worker is hard-terminated on timeout
|
|
32
|
+
3. **`worker_threads.Worker`** with `resourceLimits` (32 MB heap, 16 MB code) and a clamped wall-clock budget (`[100ms, 10s]`, default 5s). The worker is hard-terminated on timeout – no cooperative cancellation needed.
|
|
33
33
|
|
|
34
|
-
Resource limits are heap-only
|
|
34
|
+
Resource limits are heap-only – workers share the parent process's FS / network access. The allowlist validator is what actually prevents sandbox escape; the worker layer adds OOM / runaway-CPU bounding on top.
|
|
35
35
|
|
|
36
36
|
## API
|
|
37
37
|
|
|
@@ -50,14 +50,14 @@ console.log(result.logs); // ["[start] count= 0", "[settled] count= 2"]
|
|
|
50
50
|
console.log(result.facts); // { count: 2 }
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
The single-source shortcut `runInSandbox({ source: "..." })` maps onto `src/main.ts` internally
|
|
53
|
+
The single-source shortcut `runInSandbox({ source: "..." })` maps onto `src/main.ts` internally – convenient for already-runnable snippets from `get_example` / `fix_code`.
|
|
54
54
|
|
|
55
55
|
### Cancellation
|
|
56
56
|
|
|
57
|
-
`runInSandbox` accepts an optional `AbortSignal`. Wire it to your HTTP request's signal (Next.js, Express, and Hono all expose one) so a client that disconnects mid-flight releases its worker slot immediately. Without this, an abandoned caller still occupies the per-process worker queue until the worker times out (up to 10s)
|
|
57
|
+
`runInSandbox` accepts an optional `AbortSignal`. Wire it to your HTTP request's signal (Next.js, Express, and Hono all expose one) so a client that disconnects mid-flight releases its worker slot immediately. Without this, an abandoned caller still occupies the per-process worker queue until the worker times out (up to 10s) – under sustained load that drives the pool to deadlock.
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
|
-
// Next.js App Router
|
|
60
|
+
// Next.js App Router – `request.signal` fires on client disconnect.
|
|
61
61
|
export async function POST(request: Request) {
|
|
62
62
|
const result = await runInSandbox({
|
|
63
63
|
source,
|
|
@@ -67,7 +67,7 @@ export async function POST(request: Request) {
|
|
|
67
67
|
}
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
The signal aborts the queue-wait phase AND the running worker
|
|
70
|
+
The signal aborts the queue-wait phase AND the running worker – the slot frees the moment the signal trips, not when the wall-clock budget expires.
|
|
71
71
|
|
|
72
72
|
### Operator knobs
|
|
73
73
|
|
|
@@ -75,7 +75,7 @@ The signal aborts the queue-wait phase AND the running worker — the slot frees
|
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
77
|
import { setMaxConcurrentWorkers } from "@directive-run/sandbox";
|
|
78
|
-
// At module init
|
|
78
|
+
// At module init – runs once per cold start.
|
|
79
79
|
setMaxConcurrentWorkers(8);
|
|
80
80
|
```
|
|
81
81
|
|
|
@@ -88,5 +88,5 @@ logger.error({ msg: "sandbox failed", err: sanitizeStack(err.stack) });
|
|
|
88
88
|
|
|
89
89
|
## See also
|
|
90
90
|
|
|
91
|
-
- [`@directive-run/mcp`](../mcp)
|
|
92
|
-
- [`@directive-run/scaffold`](../scaffold)
|
|
91
|
+
- [`@directive-run/mcp`](../mcp) – wraps `runInSandbox` as the `run_in_sandbox` MCP tool, returns the transcript to the AI client alongside a `playgroundUrl`.
|
|
92
|
+
- [`@directive-run/scaffold`](../scaffold) – `generateRunner` produces the canonical runner shape this sandbox expects to execute (`createSystem` → `start()` → dispatch → `settle()` → log).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directive-run/sandbox",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Execute Directive snippets server-side and return a structured transcript (logs + facts + errors). Consumed by @directive-run/mcp's run_in_sandbox tool and directive.run/playground's live DevTools panel. Uses worker_threads + esbuild bundling + an AST allowlist validator so user-supplied TypeScript runs with a bounded surface (allowlisted imports, allowlisted API calls, 5s wall clock, 32 MB heap).",
|
|
5
5
|
"license": "(MIT OR Apache-2.0)",
|
|
6
6
|
"author": "Jason Comes",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@directive-run/core": "1.
|
|
50
|
+
"@directive-run/core": "1.22.0"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
53
|
"esbuild": "^0.24.0",
|