|
1 | | -# agent-container |
| 1 | +<h3 align="center">agent-container</h3> |
| 2 | + |
| 3 | +<p align="center"> |
| 4 | + Give your agents tiny boxes, powered by <a href="https://github.com/cloudflare/workerd">workerd</a><br /> |
| 5 | + Agent Container is a workerd-based sandbox designed to give coding agents isolated execution environments, providing structured bindings (workspace, exec, env) scoped to a single repository rather than the host system. It's ideal for AI coding tools, agent frameworks, and platforms that need to run untrusted code with fine-grained capability control. |
| 6 | +</p> |
| 7 | + |
| 8 | +<p align="center"> |
| 9 | + <a href="#quick-start">Quick Start</a> · |
| 10 | + <a href="#why">Why</a> · |
| 11 | + <a href="#architecture">Architecture</a> · |
| 12 | + <a href="#api">API</a> · |
| 13 | + <a href="#development">Development</a> |
| 14 | +</p> |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Quick Start |
| 19 | + |
| 20 | +```ts |
| 21 | +import { createAgentContainer } from "agent-container"; |
| 22 | + |
| 23 | +const container = await createAgentContainer({ |
| 24 | + workspace: { |
| 25 | + root: process.cwd(), |
| 26 | + mode: "shadow", // disposable copy of your repo |
| 27 | + }, |
| 28 | + env: { |
| 29 | + include: ["PUBLIC_*", "APP_*"], |
| 30 | + }, |
| 31 | + exec: { |
| 32 | + allowedCommands: ["node", "git"], |
| 33 | + }, |
| 34 | +}); |
| 35 | + |
| 36 | +await container.start(); |
| 37 | + |
| 38 | +const session = await container.createWorkerdSession(); |
| 39 | +await session.start(); |
| 40 | + |
| 41 | +const { result } = await session.run({ |
| 42 | + code: ` |
| 43 | + const pkg = await WORKSPACE.readText("package.json"); |
| 44 | + const { stdout } = await EXEC.run({ command: "node", args: ["--version"] }); |
| 45 | + return { name: JSON.parse(pkg).name, node: stdout.trim() }; |
| 46 | + `, |
| 47 | +}); |
| 48 | + |
| 49 | +console.log(result); |
| 50 | +// { name: "my-project", node: "v22.0.0" } |
| 51 | + |
| 52 | +await session.stop(); |
| 53 | +await container.stop(); |
| 54 | +``` |
| 55 | + |
| 56 | +The code inside `workerd` cannot access `fs`, `process`, or `child_process` directly. It operates through explicit capability bindings that the host controls. |
| 57 | + |
| 58 | + |
| 59 | +## Why |
| 60 | + |
| 61 | +Coding agents need to work inside real projects: reading files, writing code, running scripts, using environment variables. But giving an agent unrestricted access to your machine is dangerous, and dropping it into a fake environment breaks too many real-world workflows. |
| 62 | + |
| 63 | +**agent-container** solves this by running agent code inside [workerd](https://github.com/cloudflare/workerd) while keeping all real authority in a Node.js host process. The agent gets a natural development experience. You keep control. |
| 64 | + |
| 65 | +| Problem | Solution | |
| 66 | +|---------|----------| |
| 67 | +| Agent sees `~`, `.ssh`, unrelated directories | Workspace-scoped filesystem with explicit mounts | |
| 68 | +| Agent reads raw `.env` files | Env resolution with secret classification | |
| 69 | +| Agent spawns arbitrary processes | Allowlisted command execution with timeouts | |
| 70 | +| Agent has ambient network access | Controlled fetch with origin restrictions | |
| 71 | +| Hard to audit agent actions | Structured observability events | |
| 72 | + |
| 73 | + |
| 74 | +## Architecture |
| 75 | + |
| 76 | +``` |
| 77 | +┌─────────────────────────────────────────────────────────────────────────────┐ |
| 78 | +│ HOST (Node.js) │ |
| 79 | +│ │ |
| 80 | +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ |
| 81 | +│ │ Workspace │ │ Env │ │ Exec │ │ |
| 82 | +│ │ Controller │ │ Resolver │ │ Controller │ │ |
| 83 | +│ │ │ │ │ │ │ │ |
| 84 | +│ │ • live/shadow │ │ • .env files │ │ • allowlist │ │ |
| 85 | +│ │ • mounts │ │ • process.env │ │ • timeouts │ │ |
| 86 | +│ │ • read/write │ │ • secrets │ │ • shell policy │ │ |
| 87 | +│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ |
| 88 | +│ │ │ │ │ |
| 89 | +│ └────────────────────┼────────────────────┘ │ |
| 90 | +│ │ │ |
| 91 | +│ ┌───────────▼───────────┐ │ |
| 92 | +│ │ Capability Bridge │ │ |
| 93 | +│ │ (localhost HTTP) │ │ |
| 94 | +│ └───────────┬───────────┘ │ |
| 95 | +└────────────────────────────────┼────────────────────────────────────────────┘ |
| 96 | + │ |
| 97 | +┌────────────────────────────────┼────────────────────────────────────────────┐ |
| 98 | +│ GUEST (workerd) │ │ |
| 99 | +│ ▼ │ |
| 100 | +│ ┌──────────────────────────────────────────────────────────────────────┐ │ |
| 101 | +│ │ Capability Bindings │ │ |
| 102 | +│ │ │ │ |
| 103 | +│ │ WORKSPACE EXEC ENV SECRETS OBSERVE │ │ |
| 104 | +│ │ read/write run/shell get/keys get/keys emit │ │ |
| 105 | +│ │ glob/grep (allowlist) (public) (secret) │ │ |
| 106 | +│ │ list/stat │ │ |
| 107 | +│ │ │ │ |
| 108 | +│ └──────────────────────────────────────────────────────────────────────┘ │ |
| 109 | +│ │ |
| 110 | +│ No direct fs, process, or child_process access │ |
| 111 | +└─────────────────────────────────────────────────────────────────────────────┘ |
| 112 | +``` |
| 113 | + |
| 114 | +**Two runtimes, one boundary:** |
| 115 | + |
| 116 | +- **Host (Node.js)** owns all real authority: filesystem, environment, subprocesses, network policy, observability |
| 117 | +- **Guest (workerd)** runs agent code with only the capabilities explicitly granted through bindings |
| 118 | + |
| 119 | +The capability bridge is a session-local HTTP server that maps guest-side binding calls to host-side controllers. Each request is validated, policy-checked, and logged before execution. |
| 120 | + |
| 121 | + |
| 122 | +## Capabilities |
| 123 | + |
| 124 | +### WORKSPACE |
| 125 | + |
| 126 | +Scoped filesystem access with optional mounts. |
| 127 | + |
| 128 | +```ts |
| 129 | +// Read and write files |
| 130 | +const content = await WORKSPACE.readText("src/index.ts"); |
| 131 | +await WORKSPACE.writeText("output/result.json", JSON.stringify(data)); |
| 132 | + |
| 133 | +// Search |
| 134 | +const files = await WORKSPACE.glob("**/*.ts"); |
| 135 | +const matches = await WORKSPACE.grep("TODO", { include: "**/*.ts" }); |
| 136 | + |
| 137 | +// Inspect |
| 138 | +const entries = await WORKSPACE.list("src"); |
| 139 | +const info = await WORKSPACE.stat("package.json"); |
| 140 | +``` |
| 141 | + |
| 142 | +**Modes:** |
| 143 | +- `live` — operate directly on the repo (default) |
| 144 | +- `shadow` — operate on a disposable copy |
| 145 | + |
| 146 | +**Mounts:** attach additional paths with `ro` or `rw` access. |
| 147 | + |
| 148 | +### EXEC |
| 149 | + |
| 150 | +Controlled subprocess execution. |
| 151 | + |
| 152 | +```ts |
| 153 | +const { stdout, exitCode } = await EXEC.run({ |
| 154 | + command: "node", |
| 155 | + args: ["--version"], |
| 156 | + timeoutMs: 5000, |
| 157 | +}); |
| 158 | + |
| 159 | +// Shell execution (requires allowShell: true) |
| 160 | +const result = await EXEC.shell({ |
| 161 | + script: "ls -la | head -5", |
| 162 | +}); |
| 163 | +``` |
| 164 | + |
| 165 | +**Policy options:** |
| 166 | +- `allowedCommands` — whitelist of executables |
| 167 | +- `allowShell` — enable/disable shell scripts |
| 168 | +- `defaultTimeoutMs` — execution timeout |
| 169 | + |
| 170 | +### ENV / SECRETS |
| 171 | + |
| 172 | +Separated environment variable access. |
| 173 | + |
| 174 | +```ts |
| 175 | +// Public config |
| 176 | +const apiUrl = await ENV.get("API_URL"); |
| 177 | +const publicKeys = await ENV.keys(); |
| 178 | + |
| 179 | +// Secrets (classified by pattern matching) |
| 180 | +const apiKey = await SECRETS.get("API_KEY"); |
| 181 | +``` |
| 182 | + |
| 183 | +**Classification:** variables matching patterns like `*_KEY`, `*_TOKEN`, `*_SECRET` are automatically classified as secrets and isolated from `ENV`. |
| 184 | + |
| 185 | +### OBSERVE |
| 186 | + |
| 187 | +Structured event emission for audit trails. |
| 188 | + |
| 189 | +```ts |
| 190 | +await OBSERVE.emit({ |
| 191 | + scope: "workspace", |
| 192 | + action: "custom-operation", |
| 193 | + outcome: "success", |
| 194 | + detail: "processed 42 files", |
| 195 | +}); |
| 196 | +``` |
| 197 | + |
| 198 | + |
| 199 | +## API |
| 200 | + |
| 201 | +### createAgentContainer(options) |
| 202 | + |
| 203 | +Creates a container with configured policies. |
| 204 | + |
| 205 | +```ts |
| 206 | +interface AgentContainerOptions { |
| 207 | + workspace: { |
| 208 | + root: string; |
| 209 | + mode?: "live" | "shadow"; |
| 210 | + mounts?: readonly { |
| 211 | + mountPath: string; |
| 212 | + sourcePath: string; |
| 213 | + mode: "ro" | "rw"; |
| 214 | + }[]; |
| 215 | + }; |
| 216 | + env?: { |
| 217 | + sources?: readonly EnvSource[]; |
| 218 | + include?: readonly string[]; |
| 219 | + exclude?: readonly string[]; |
| 220 | + secretPatterns?: readonly string[]; |
| 221 | + processEnv?: "none" | "allow-matching" | "all"; |
| 222 | + }; |
| 223 | + exec?: { |
| 224 | + allowedCommands?: readonly string[]; |
| 225 | + allowShell?: boolean; |
| 226 | + defaultTimeoutMs?: number; |
| 227 | + }; |
| 228 | + network?: { |
| 229 | + allowFetch?: boolean; |
| 230 | + allowedFetchOrigins?: readonly string[]; |
| 231 | + }; |
| 232 | + observability?: { |
| 233 | + emit(event: ObservabilityEvent): void | Promise<void>; |
| 234 | + }; |
| 235 | +} |
| 236 | +``` |
| 237 | + |
| 238 | +### container.createWorkerdSession(options?) |
| 239 | + |
| 240 | +Creates a workerd session for running code. |
| 241 | + |
| 242 | +```ts |
| 243 | +const session = await container.createWorkerdSession({ |
| 244 | + startupTimeoutMs: 30000, |
| 245 | + compatibilityDate: "2026-01-01", |
| 246 | +}); |
| 247 | + |
| 248 | +await session.start(); |
| 249 | +const { result, logs, durationMs } = await session.run({ code: "..." }); |
| 250 | +await session.stop(); |
| 251 | +``` |
| 252 | + |
| 253 | +### defineAgentContainerPlugin(options) |
| 254 | + |
| 255 | +Defines a plugin for integration with agent harnesses. |
| 256 | + |
| 257 | +```ts |
| 258 | +const plugin = defineAgentContainerPlugin({ |
| 259 | + name: "my-agent", |
| 260 | + container: { workspace: { root: "." } }, |
| 261 | + tools: { |
| 262 | + read: "WORKSPACE.readText", |
| 263 | + bash: "EXEC.run", |
| 264 | + }, |
| 265 | +}); |
| 266 | +``` |
| 267 | + |
| 268 | + |
| 269 | +## Project Structure |
| 270 | + |
| 271 | +``` |
| 272 | +packages/ |
| 273 | +├── agent-container/ # Core runtime |
| 274 | +├── types/ # Shared TypeScript types |
| 275 | +├── cli/ # CLI tools |
| 276 | +└── test-utils/ # Test helpers |
| 277 | +
|
| 278 | +apps/ |
| 279 | +├── e2e/ # Integration tests |
| 280 | +├── playground/ # Development playground (WIP) |
| 281 | +└── docs/ # Documentation (WIP) |
| 282 | +``` |
| 283 | + |
| 284 | + |
| 285 | +## Development |
| 286 | + |
| 287 | +```sh |
| 288 | +pnpm install |
| 289 | +pnpm build |
| 290 | +pnpm typecheck |
| 291 | +pnpm test |
| 292 | +pnpm test:e2e |
| 293 | +``` |
| 294 | + |
| 295 | +### CLI (WIP) |
| 296 | + |
| 297 | +```sh |
| 298 | +# Print container description for current directory |
| 299 | +agent-container describe |
| 300 | +``` |
| 301 | + |
| 302 | +--- |
| 303 | + |
| 304 | +## License |
| 305 | + |
| 306 | +[Apache-2.0](LICENSE) |
0 commit comments