Repository navigation
Conversation
…dboxed in a WebContainer The agent's write_file, read_file, list_files and run_command tools are host functions over a SandboxPort, backed in the browser by a WebContainer, so model-written code runs in the tab and cannot reach the host. The core is framework-free for reuse by other hosts; a small Vite page demos it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
There was a problem hiding this comment.
Review progress ██████████ 15/15 files
Comment — found 3 issue(s) at 7a5559a.
Actionable comments posted: 3
🤖 Prompt for AI agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@examples/webcontainer-agent/README.md` (line 39, Important):
The function returns a `SandboxTools` object (`{ tools: ToolDefinition[], dispose() }`), not an array with a `dispose` property. Embedders following this table will write `createSandboxTools(...).length` or `tools.dispose()` and get a runtime error. Reword the row to say it returns `{ tools, dispose }`.
2. In `@examples/webcontainer-agent/src/app/main.ts` (line 75, Important):
`dispose?.()` runs before every follow-up, so the background server from the previous build is killed before the agent sees the new request. `runSandboxAgent`'s docstring says servers left running "stay up when this returns, so the user keeps a live preview; the caller ends them with `dispose` when it is done with the sandbox." A follow-up is not the end of the sandbox. Concrete failure: user builds a page (server running, iframe showing it), then asks "make the title blue". Clicking Build kills the server, the iframe still points at the now-dead URL, and if the model only edits files and does not restart the server (it is not told it must), the user sees a dead preview with no change. Consider disposing only when the run replaces the project, or leaving the server alive across follow-ups and letting the agent reuse it.
3. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 377, Important):
`dispose()` kills every background process. `runSandboxAgent` documents that servers "stay up when this returns, so the user keeps a live preview," and that the caller ends them only when done with the sandbox. But `main.ts` calls the previous run's `dispose?.()` before each follow-up run (and `runSandboxAgent`'s catch disposes on any thrown error, including a provider failure or a Stop). So sending a follow-up message kills the dev server while `preview.src` still points at its URL, and the iframe goes dead. The agent's history still says "Server is running", so the model may not restart it. Consider disposing only when the sandbox is actually abandoned, or restarting the background servers after a follow-up edit.
…turns The page created tools per turn and disposed the previous turn's before each follow-up, and a failed or stopped run disposed its own, so a follow-up killed the server the preview iframe was showing. The caller now owns the tools for the life of the sandbox and passes them to every turn; a run given them never stops their servers. A stop_servers tool lets the agent restart a server whose own code changed without hitting EADDRINUSE. README corrects the return shape of createSandboxTools. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
The coverage gate pins the set of access:none workspaces, and the new example is a Vite app behind no published entry point, like examples/web. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
There was a problem hiding this comment.
Review progress █████████░ 16/17 files
Comment — found 4 issue(s) at 63bbce1.
Actionable comments posted: 4
Coverage is incomplete: reviewed 16/17 files; 0 skipped; 1 deferred and still unreviewed.
Comment @mergestorm-vortex review on this PR to review the remaining files.
🤖 Prompt for AI agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@examples/webcontainer-agent/README.md` (line 20, Important):
`The model can only ... run_command` omits `stop_servers`, which `createSandboxTools()` also registers. The documented tool list is therefore incomplete; readers will miss that the model can stop the running preview server (and may assume the four listed tools are exhaustive).
2. In `@examples/webcontainer-agent/src/app/main.ts` (line 85, Important):
`buildModelConfig(choice, modelInput.value.trim(), apiKey)` reads the model only after `await choice.register()`, while the provider selector remains enabled. If a user changes the provider during that async registration, `choice` still names the original provider but `syncModelDefault()` has changed the model to the new provider's default; the ensuing request is sent to the original provider with the incompatible model and can fail. Capture the selected model before awaiting registration, or otherwise keep provider and model selection fixed for the run.
3. In `@examples/webcontainer-agent/src/core/runSandboxAgent.ts` (line 76, Important):
`run.sandboxTools ?? createSandboxTools(...)`
When callers pass the reusable `sandboxTools` instance (the documented across-turn path), this runner never installs the callback that updates `previewUrl` and emits `type: "preview"`. A server can start and the caller’s own `onPreview` can fire, but `runSandboxAgent` still returns `previewUrl: undefined` and omits the preview event, contrary to the result/event contract.
4. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 329, Important):
`context.signal.addEventListener("abort", stop, { once: true });`
The listener is installed only after `await port.spawn(...)`. If the run is stopped while `spawn` is pending (or the signal was already aborted), this event is missed, so the process is not killed on cancellation and the tool can wait for its full timeout. For a background command that becomes ready, the server can also be left running when the shared, caller-owned tools survive the failed run.
…ncel during spawn runSandboxAgent subscribes to the tools' new onPreview for the length of the run, so previewUrl and preview events work with caller-owned tools too. A run_command now listens for cancellation before starting the program, so a Stop clicked mid-spawn kills it instead of leaving it running. The page reads the model before registering the provider and locks both controls for the run. README lists stop_servers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
There was a problem hiding this comment.
Review progress █████████░ 16/17 files
Comment — found 1 issue(s) at ac19dc0.
Actionable comment posted: 1
Coverage is incomplete: reviewed 16/17 files; 0 skipped; 1 deferred and still unreviewed.
Comment @mergestorm-vortex review on this PR to review the remaining files.
🤖 Prompt for AI agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@examples/webcontainer-agent/src/app/main.ts` (line 42, Important):
`refreshFiles()` invokes this recursive walk after every completed tool call, but unlike `list_files` it has no depth or entry cap. A single `write_file` with a deeply nested path (whose parents are created recursively), or a generated project with many files, can make the UI issue unbounded `readdir` calls and build a huge string, freezing the page. Apply a bound to this walk as well.
The page walked the project with no depth or entry cap after every tool call, and the model chooses the paths. The walk is now one exported function, listSandboxFiles, used by both the list_files tool and the page. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
There was a problem hiding this comment.
Review progress ██████████ 17/17 files
Comment — found 3 issue(s) at 4148252.
Actionable comments posted: 3
🤖 Prompt for AI agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@examples/webcontainer-agent/src/app/main.ts` (line 89, Important):
`void refreshFiles()` starts a recursive `readdir` walk without handling rejection. While that walk is in progress, a subsequent agent command can remove a directory it is about to enter; WebContainer's `readdir` then rejects, producing an unhandled promise rejection. The same walk is awaited in `finally` and can reject the run-button handler as well, so handle/report listing failures rather than letting a transient tree change escape.
2. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 94, Important):
`return { lines, truncated: lines.length >= MAX_LISTED_ENTRIES }`
This count-only flag has two concrete boundary errors: exactly 400 total entries are reported as truncated even when nothing was omitted, while a walk that stops descending at `depth === 6` can omit deeper files and still report `truncated: false` if it emitted fewer than 400 lines. The `list_files` tool and UI then tell the model/user the wrong completeness state; track whether either limit actually omitted entries.
3. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 319, Important):
`if (wantsBackground && background.size >= maxBackground)`
The slot check runs before `await port.spawn(...)`, but the process is not added to `background` until later. If these tools are used with parallel tool execution (supported by `AgentTask`'s `toolConcurrency`), two calls can both observe an empty set with a limit of 1, spawn, and then both register, leaving more servers running than the configured cap. Reserve the slot before awaiting spawn and release it on spawn failure or if startup does not become a background server.
…s, tolerant tree refresh listSandboxFiles reports truncation only when the entry cap or the depth limit left something out. A background run_command claims its server slot before it awaits spawn, so parallel tool calls cannot exceed the cap. The page's file tree refresh no longer throws when a program removes a directory mid-walk. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
There was a problem hiding this comment.
Review progress ██████████ 17/17 files
Comment — found 2 issue(s) at 86cca71.
Actionable comments posted: 2
🤖 Prompt for AI agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 252, Important):
`port.readFile(path)` materializes the entire file before the 40,000-character truncation below. A model can create a very large file through an allowed `node` or package-manager command and then read it, forcing an unbounded allocation in the browser and potentially freezing or exhausting its memory; truncating the returned string does not bound this operation.
2. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 345, Important):
`port.onServerReady` subscribes to a container-wide event, not an event associated with the `SandboxProcess` being started. If two background `run_command` calls overlap (for example, when an embedding AgentTask opts into `toolConcurrency`), the first server's event is recorded by both listeners, so both calls can return the same preview URL even if the second process has not started listening. The second tool call then reports a server as ready when it is not.
| const path = safePath(stringArg(input, "path")); | ||
| let content: string; | ||
| try { | ||
| content = await port.readFile(path); |
There was a problem hiding this comment.
Truncation does not bound the file read
port.readFile(path) materializes the entire file before the 40,000-character truncation below. A model can create a very large file through an allowed node or package-manager command and then read it, forcing an unbounded allocation in the browser and potentially freezing or exhausting its memory; truncating the returned string does not bound this operation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 252, Important):
`port.readFile(path)` materializes the entire file before the 40,000-character truncation below. A model can create a very large file through an allowed `node` or package-manager command and then read it, forcing an unbounded allocation in the browser and potentially freezing or exhausting its memory; truncating the returned string does not bound this operation.
There was a problem hiding this comment.
Leaving this as is. The path is real but the cost is bounded by the tab: a WebContainer's filesystem lives in the same tab's memory, so a file the model creates there already occupies it, and read_file duplicates that at most once before the 40,000-character cut. Bounding the read itself would need a stat or ranged read, which the WebContainer fs API does not offer, so the only fix is a size probe via a spawned process, which costs more code than the failure justifies in an example. If an embedder's port can stat, SandboxPort.readFile is the place to add a size check.
Generated by Claude Code
There was a problem hiding this comment.
Thanks for the reasoning. I can't dismiss this finding: it was raised on an earlier commit, so it stays open. At 4ae77a5 read_file still reads the whole file before the 40,000-character cut, so the point is still there. The tradeoff you describe is clear, and a maintainer can decide to accept it.
The container announces a listening port without saying which process owns it, so two overlapping background run_command calls could both return the first server's URL. Background starts now queue, so an announcement during a startup belongs to that startup. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
There was a problem hiding this comment.
Review progress ██████████ 17/17 files
Comment — found 2 issue(s) at 4ae77a5.
Actionable comments posted: 2
🤖 Prompt for AI agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 82, Important):
`[...(await port.readdir(dir))].sort(...)` materializes and sorts every child before the 400-entry cap is checked. A project with a very large single directory (which allowed `node` commands can create) therefore still allocates and sorts the entire listing on each `list_files` call and UI refresh, potentially freezing or exhausting the browser despite the advertised entry bound.
2. In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 446, Important):
`killBackground()` only sees processes already in `background`; a background `run_command` may still be waiting on `serverStartups` or `port.spawn()` and is not registered yet. With AgentTask `toolConcurrency > 1`, `stop_servers` can return `Stopped 0 server(s)` while a start is pending, after which that command starts and leaves a server running despite the explicit stop.
| * `readdir` calls. Shared by the `list_files` tool and any UI showing the tree. | ||
| */ | ||
| export async function listSandboxFiles(port: SandboxPort, root = "."): Promise<SandboxListing> { | ||
| const lines: string[] = []; |
There was a problem hiding this comment.
Bound the directory read, not just emitted entries
[...(await port.readdir(dir))].sort(...) materializes and sorts every child before the 400-entry cap is checked. A project with a very large single directory (which allowed node commands can create) therefore still allocates and sorts the entire listing on each list_files call and UI refresh, potentially freezing or exhausting the browser despite the advertised entry bound.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
In `@examples/webcontainer-agent/src/core/sandboxTools.ts` (line 82, Important):
`[...(await port.readdir(dir))].sort(...)` materializes and sorts every child before the 400-entry cap is checked. A project with a very large single directory (which allowed `node` commands can create) therefore still allocates and sorts the entire listing on each `list_files` call and UI refresh, potentially freezing or exhausting the browser despite the advertised entry bound.
There was a problem hiding this comment.
Leaving this as is, for the same reason as the read_file thread: the allocation happens inside port.readdir, which hands back the full array before this code sees it, and the WebContainer API has no paged or streaming readdir. The sort adds an O(n log n) pass over an array that already exists in the same tab's memory, so bounding it would not bound the allocation that matters. The 400-entry cap bounds what is returned to the model and rendered, which is what it is for.
Generated by Claude Code
There was a problem hiding this comment.
Thanks. The allocation inside port.readdir is outside this code, so I agree that part can't be bounded here. The spread copy and the sort still run over every entry before the 400-entry cap, though. The finding was raised on 4ae77a5, so it stays open for now.
…ill in flight A background run_command waiting in the startup queue or inside spawn was not yet registered, so stop_servers could report nothing stopped and the start would then complete. Every stop now bumps an epoch that a start checks before and after spawning, and the count includes pending starts. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad
|
Generated by Claude Code |
|
Generated by Claude Code |
Add a complete example of a coding agent that runs inside a browser tab using WebContainer, with safe sandboxed execution of model-generated code.
Summary
This adds
examples/webcontainer-agent, a fully functional web application where an AI agent writes and executes a small web project entirely within a WebContainer (Node.js compiled to run in the browser tab). The agent cannot reach the host filesystem, network, or processes—only the sandbox through four carefully bounded tools.Key changes
sandboxPort.ts: Abstract interface for sandbox operations (readFile,writeFile,mkdir,readdir,spawn,onServerReady). IncludesresolveSandboxPath()to validate and normalize paths, refusing escapes outside the project root.sandboxTools.ts: Four AI tools built onSandboxPort:write_file: Create/overwrite files with size limits and automatic parent directory creationread_file: Read text files with truncation for large fileslist_files: Recursively list project structure, skippingnode_modulesand.gitrun_command: Execute programs (from an allow-list) with timeout, output buffering (tail-keeping), and background server supportTailBufferto keep the last N characters of output (important for long installs where the error is at the end)webContainerPort.ts: Adapter from WebContainer API toSandboxPortinterface. Handles cross-origin isolation checks and server readiness detection.runSandboxAgent.ts: Orchestrates anAgentTaskwith sandbox tools, system prompt, and event streaming. Manages background server lifecycle and returns preview URLs.sandboxPrompt.ts: System prompt explaining the sandbox environment to the model (no shell, no native addons, no git/Python/Docker).ansi.ts: Utility to strip ANSI color codes from command output before sending to the model (saves tokens).Web UI (
main.ts,models.ts,index.html): Browser interface to select provider (Anthropic/OpenAI/Google), enter API key, describe what to build, and watch the agent work. Shows file tree and live preview iframe. Supports follow-up messages that edit the same project.Notable implementation details
No shell:
run_commandtakes program and arguments separately; no pipes, redirects, or&&chains possible. This prevents shell injection and forces explicit step-by-step thinking.Allow-list: Only
node,npm,npx,pnpm,yarn,ls,cat,mkdir,rm,mv,cp,touch,pwd,echoare available by default.Timeout handling: Foreground commands have a configurable timeout (default 90s); background servers wait for listening (default 45s). Uses
AbortControllerandPromise.race()for clean cancellation.Output buffering:
TailBufferkeeps only the tail of output to stay within token budgets while preserving error messages that appear at the end of long installs.Safe approval: Uses
approval: "never"because all tools operate only within the sandbox—there is nothing outside it for a person to approve.Key isolation: API keys live in page memory and go only to the provider SDK; sandbox code runs in a separate origin and cannot access them.
Cross-origin isolation: Vite config and HTML headers set
Cross-Origin-Embedder-Policy: require-corpandCross-Origin-Opener-Policy: same-originso WebContainer can useSharedArrayBuffer.Includes comprehensive tests (
sandboxTools.test.ts) with aFakeSandboxtest double.https://claude.ai/code/session_01AzKiEhbKvifSdRKSddoMad