After studying this material, you should be able to:
Before touching any code, understand why this exists.
When AI agents generate and execute code, that code needs to run somewhere. The naive approach is using JavaScript's built-in eval().
Why eval() is dangerous:
Your Application
├── Database credentials ← eval() can access ALL of this
├── API secret keys ← eval() can access ALL of this
├── Internal services ← eval() can access ALL of this
└── eval(agentGeneratedCode) ← runs with FULL application access
Three specific risks:
| Risk | Description |
|---|---|
| Secret exposure | Agent code can read process.env.DATABASE_URL |
| No pause points | Cannot stop execution for human approval mid-run |
| No audit trail | No durable record of what ran and what it returned |
Think of it like a contractor working in your building:
eval access)┌─────────────────────────────────────────┐
│ YOUR APPLICATION │
│ │
│ database credentials ✓ │
│ API keys ✓ │
│ internal services ✓ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ SANDBOX (QuickJS) │ │
│ │ │ │
│ │ agent-generated code runs here │ │
│ │ │ │
│ │ can ONLY call host functions ────┼──┼──► your app handles it
│ │ cannot access Node.js │ │
│ │ cannot access network directly │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
pnpm add run
import { run } from 'run';
const result = await run({
source: `
const orders = await store.listOrders("customer_123");
const total = orders.reduce((sum, order) => sum + order.amount, 0);
return { count: orders.length, total };
`,
hostFunctions: {
store: {
listOrders: async (customerId: string) => {
return database.orders.findMany({ customerId }); // real DB call
},
},
},
});
if (result.status === 'completed') {
console.log(result.value);
}
run({
source: "...", ← the code that runs INSIDE the sandbox
hostFunctions: {...} ← the doors you give the sandbox to knock on
})
What the sandbox sees:
// Inside sandbox — this is ALL it knows about
store.listOrders("customer_123") // ✓ available
database.credentials // ✗ does not exist here
process.env // ✗ does not exist here
What your application keeps:
// Outside sandbox — stays in your application
database.orders.findMany(...) // actual implementation
process.env.DATABASE_URL // credentials never exposed
if (result.status === 'completed') {
console.log(result.value); // use the result
} else if (result.status === 'interrupted') {
// handle pause point (covered in Step 5)
}
Host functions should map to specific actions in your product, not generic capabilities.
❌ Too broad — hard to control:
hostFunctions: {
http: {
request: async (url, method, body) => {
return fetch(url, { method, body }); // agent can call ANYTHING
}
}
}
✅ Narrow and purposeful:
hostFunctions: {
orders: {
refund: async (orderId: string) => {
// clear place to add authorization checks
await verifyUserOwnsOrder(orderId);
return processRefund(orderId);
}
}
}
Generic: http.request(url)
→ Who is calling this? For what reason? Is it authorized?
→ Very hard to reason about
Specific: orders.refund(orderId)
→ Exactly one thing happens
→ Easy to add auth checks
→ Easy to audit
One major benefit — the agent can coordinate parallel work:
const result = await run({
source: `
// These two calls happen AT THE SAME TIME
const [account, invoices] = await Promise.all([
crm.getAccount("account_123"),
billing.listInvoices("account_123"),
]);
// Filtering logic stays inside sandbox
const overdue = invoices.filter(i => i.status === "overdue");
// Only the useful result comes back
return { account: account.name, overdue };
`,
hostFunctions: {
crm: { getAccount },
billing: { listInvoices },
},
});
What this achieves:
| Without Run SDK | With Run SDK |
|---|---|
| Two sequential LLM calls | One LLM response |
| Full billing response in context | Only overdue invoices returned |
| Agent coordinates in prompts | Agent coordinates in code |
Some operations need a human to approve before continuing. The Run SDK supports pausing execution mid-run and resuming it later without repeating completed work.
Program starts
│
▼
host function called (e.g., publish document)
│
▼
Is this the first time? ──YES──► interrupt execution
│ │
│NO ▼
│ Save token + ask human
│ │
▼ ▼
Check approval result Human decides
│ │
├── approved ──────────────────►│
│ Resume with token
└── denied ──► return { published: false }
import { getHostFunctionContext } from 'run';
const hostFunctions = {
documents: {
publish: async (draftId: string) => {
const context = getHostFunctionContext();
// FIRST CALL: no resume data exists yet
if (context.resume === undefined) {
context.interrupt({
kind: 'approval',
message: `Publish ${draftId}?`,
});
// execution pauses here
}
// RESUMED CALL: check what the human decided
if (context.resume.resolution !== true) {
return { published: false }; // denied
}
return publishDraft(draftId); // approved
},
},
};
const result = await run({ source, hostFunctions });
if (result.status === 'interrupted') {
// result contains a signed token
await db.save({
token: result.token, // save this
message: result.message, // "Publish draft_456?"
requestedBy: currentUser,
});
// worker does NOT need to stay alive
// resume whenever the human responds
}
// Later, when human approves...
const resumed = await run({
source,
hostFunctions,
resume: {
token: savedToken,
resolution: true, // approved
}
});
Run 1: getAccount() ✓ → listInvoices() ✓ → publish() INTERRUPTED
│
results saved
Run 2 (resume): getAccount() [uses saved result]
listInvoices() [uses saved result]
publish() [receives approval, continues]
Key insight: Completed work is recorded and replayed, not re-executed. Your database is not called twice.
Sandboxing prevents access violations, but you still need to handle:
import { createRunner } from 'run';
// Create once, reuse across many runs
const runner = createRunner({
limits: {
timeoutMs: 10_000, // 10 seconds max
memoryLimitBytes: 32 * 1024 * 1024, // 32 MB max
},
});
// Use like run()
const result = await runner.run({ source, hostFunctions });
┌─────────────────────────────────────┐
│ QuickJS heap (sandbox memory) │ ← memoryLimitBytes covers this
│ │
│ Values crossing the boundary │ ← also covered
│ (host function return values) │
└─────────────────────────────────────┘
The Run SDK applies these automatically — you do not configure them:
| Protection | What It Does |
|---|---|
| Fresh context per run | No state leaks between executions |
| Dynamic eval disabled | No eval() inside the sandbox |
| Hardened prototypes | Prevents prototype pollution attacks |
Sandbox code ──► UNTRUSTED — Run SDK enforces limits
Host functions ──► TRUSTED — YOU must add authorization checks
Host functions are your application code. The Run SDK does not protect them from themselves. If orders.refund() needs auth checks, you write those checks.
npm install inside the sandbox)PROBLEM: eval() gives agent code full application access
SOLUTION: Run SDK
1. ISOLATE → QuickJS sandbox in worker thread
2. EXPOSE → only narrow host functions
3. PAUSE → interrupt for human approval
4. RESUME → replay with saved results, no repeated work
5. LIMIT → timeout and memory bounds
RESULT: Agent code can coordinate your tools
without touching your secrets or infrastructure
| Concept | Key API |
|---|---|
| Basic execution | run({ source, hostFunctions }) |
| Shared limits | createRunner({ limits }) |
| Interrupt for approval | context.interrupt({ kind, message }) |
| Check resume state | context.resume |
| Resume a run | run({ source, hostFunctions, resume: { token, resolution } }) |