branch:
README.md
3234 bytesRaw
# Sandbox — Dynamic Code Execution with Worker Loader
An AI agent that writes JavaScript code and runs it in a **sandboxed dynamic Worker isolate**. The isolate has no internet access — its only connection to the outside world is a database binding that proxies through a sub-agent.
## How It Works
```
SandboxAgent (extends AIChatAgent)
│
├── executeCode tool ──▶ env.LOADER.get(id, {
│ mainModule: "harness.js",
│ modules: { "harness.js": ..., "user-code.js": agentCode },
│ env: { db: DatabaseLoopback }, ← only binding
│ globalOutbound: null, ← no fetch()
│ tails: [TailLoopback] ← capture console.log
│ })
│
├── DatabaseLoopback (WorkerEntrypoint)
│ └── proxies env.db calls back to the parent
│
├── TailLoopback (WorkerEntrypoint)
│ └── captures console output, delivers to parent
│
└── CustomerDatabase (Agent — own isolated SQLite)
└── query() / execute() / getAllCustomers()
```
Three layers of isolation:
1. **Dynamic isolate** — code runs in a Worker with `globalOutbound: null`. No `fetch()`, no `connect()`.
2. **Restricted env** — the only binding is `env.db`, a `DatabaseLoopback` that proxies to the parent.
3. **Sub-agent storage** — the database is a child DO whose SQLite the parent can't access directly.
## Key Pattern
```typescript
import { Agent } from "agents";
import { AIChatAgent } from "@cloudflare/ai-chat";
export class CustomerDatabase extends Agent<Env> {
onStart() {
this.sql`CREATE TABLE IF NOT EXISTS customers (...)`;
}
query(sql: string): Record<string, unknown>[] {
/* ... */
}
execute(sql: string): { success: boolean } {
/* ... */
}
}
export class SandboxAgent extends AIChatAgent<Env, SandboxState> {
private _db() {
return this.subAgent(CustomerDatabase, "database");
}
// DatabaseLoopback calls these — they forward to the sub-agent
async proxyDbQuery(sql: string) {
const db = await this._db();
return db.query(sql);
}
}
```
The **Loopback pattern**: dynamic Worker isolates can't hold sub-agent stubs directly — they can only have `ServiceStub` bindings. So `DatabaseLoopback` (a `WorkerEntrypoint`) proxies calls back to the parent, which delegates to the sub-agent.
Chain: `dynamic isolate → DatabaseLoopback → SandboxAgent → CustomerDatabase sub-agent`
## Quick Start
```bash
npm start
```
## Try It
1. "Count customers by tier" — agent writes code, runs in sandbox, shows output
2. "Find customers with emails containing 'example'" — agent queries via `env.db`
3. "Add a new customer named Zara" — agent calls `env.db.execute()` from sandbox
4. Check the **Executions** tab to see code + captured output
5. Check the **Customers** tab to see the database state
## Related
- [gadgets-subagents](../gadgets-subagents) — fan-out/fan-in with parallel sub-agents
- [gadgets-chat](../gadgets-chat) — multi-room chat via sub-agents
- [gadgets-gatekeeper](../gadgets-gatekeeper) — gated database access via sub-agent boundary
- [design/rfc-sub-agents.md](../../design/rfc-sub-agents.md) — RFC for the sub-agent API