Isolated exec environments
AiChat uses E2B sandboxes as its isolated execution layer for two different paths:
python_execand related data tools in chat sessions- Forge jobs launched from the chat control surface
The shared boundary lives in apps/aichat/server/utils/sandbox.ts, which is the only server module that imports the E2B SDK. Everything else asks for a sandbox through the SandboxDriver interface.
What the isolated environment is
The sandbox is a remote filesystem + command runtime created from an E2B template. The codebase currently uses two templates:
base— Debian + Python 3 + Node.jscode-interpreter-v1—baseplus Jupyter and a Python data stack
Template choice is part of the sandbox acquisition call, not a separate subsystem. The driver also documents a desktop template, but the chat and Forge paths in this repository use base and code-interpreter-v1.
The driver caches a handle by scope id and reuses it only while that remote sandbox is still running. In chat, this can preserve one interpreter across multiple cells during the sandbox's lifetime. A Forge job uses one sandbox during a single run and releases it afterward; a retry or follow-up run gets a fresh sandbox. E2B pause/resume is not implemented.
Code runs in chat
Python execution in chat flows through apps/aichat/server/utils/pythonTool.ts.
- The agent tool
python_execis built bybuildPythonExecTool(sessionId). - The tool calls
acquireChatSandbox(sessionId)fromapps/aichat/server/utils/sandbox.ts. acquireChatSandbox()always requests an interpreter sandbox with:interpreter: truetemplate: 'code-interpreter-v1'
- The tool then calls
sandbox.runCode(handle, params.code, ...).
That runCode path gives python_exec a Jupyter kernel. While the cached chat sandbox remains alive, imports, variables, and loaded data frames can be reused across calls in the same conversation.
Output shape for code runs
runCode() returns:
stdoutstderrexitCodecharts
The chart list is produced by mapExecution() in apps/aichat/server/utils/sandbox.ts. It prefers Plotly outputs first, then falls back to image outputs such as matplotlib PNGs.
pythonTool.ts keeps the LLM-visible summary small and stores the full execution details separately in details. The chat runtime then uses toolDetailsFailed() to mark non-zero Python exits as tool errors.
Data attachment flow
When a user attaches files in chat, chatRuntime.ts enables the datasets and python_exec tool ids together. That allows the dataset tools in apps/aichat/server/utils/datasetTools.ts to fetch the original uploaded file into the same chat sandbox with dataset_fetch_file, after which python_exec can analyze it directly.
Agent runs in Forge
Forge jobs use the same sandbox driver, but they acquire a job-scoped sandbox in apps/aichat/server/utils/forgeJobs.ts.
The job orchestration path is:
forgeTools.tsexposes the job-facing chat tools such asforge_create_job,forge_plan_job,forge_run_job, andforge_get_job.forgeJobs.tscreates the job record and later acquires theforge-job:${job.id}scope withuseSandbox().acquire(...), usingjob.sandboxTemplate || 'base'.- The chosen Forge adapter in
apps/aichat/server/utils/forgeAdapters.tsruns against that handle.
There are two adapter modes visible in the codebase:
pi— the default adapter, which drives the sandbox withbash,read_file, andwrite_filetools through thepi-agent-coreloopclaude-code— a headless CLI-based adapter that also runs inside the same sandbox driver
Unlike chat Python runs, Forge job execution is repository-oriented. forgeJobs.ts mints the scoped GitHub token, clones the repository, and creates the branch before handing the checked-out repoPath to the adapter. The adapter edits and tests inside that path. Afterward, forgeJobs.ts commits, pushes, and collects the diff. Keeping those Git operations in the orchestrator is part of the execution boundary.
Sandbox templates for jobs
forgeTools.ts exposes the available templates to the chat UI, and job creation stores the selected template in the forge_jobs.sandbox_template column (mapped to forgeJobs.sandboxTemplate by Drizzle). The current code paths use:
basefor most reposcode-interpreter-v1for data-heavy Python work
Isolation boundaries
The codebase enforces isolation in a few concrete ways:
- All sandbox access goes through
SandboxDriverinsandbox.ts. - Only
sandbox.tsimportse2bor@e2b/code-interpreterdirectly. - Chat code execution can reuse an interpreter sandbox within one session id while that sandbox remains alive.
- Each Forge run uses a separate sandbox handle keyed by
forge-job:${job.id}and releases it when the run finishes. - Sandboxes are created with a 10-minute E2B timeout. Individual code and command calls fall back to 60 seconds at the driver level;
python_execdefaults to 30 seconds, and Forge supplies per-step overrides. runCommand()returns stdout, stderr, and exit code; it does not throw the raw SDK error back into the app path.
The sandbox driver also notes that resources are fixed by template. In other words, a larger environment means a different template, not a resized runtime.
Where results appear
For chat Python runs, the results surface in two places:
- the streamed tool result seen by the LLM
- the
detailspayload returned to the chat runtime and UI, including charts
For Forge jobs, the adapter records a structured trace and summary, and forgeJobs.ts persists job state, cost, diff, and error information in the job record.
In both cases, errors are normalized before they reach the caller. Python execution folds kernel errors into stderr and exitCode: 1; command execution returns a similar { stdout, stderr, exitCode } shape.
Operational notes and limits
A few implementation details are worth keeping in mind:
acquire()caches the first sandbox created for an id and reuses it while it is still running.- Forge releases and kills its sandbox in a
finallyblock after every run; it does not pause or resume that environment. - The interpreter sandbox flips Plotly to the
plotly_mimetyperenderer once per kernel so structured chart output can be captured. mapExecution()caps chart count and size to keep outputs bounded.- The chat sandbox and Forge sandbox are both E2B-backed, but they are not the same handle and do not share filesystem state.
- The repository’s docs already describe this at a high level in
apps/web/content/docs/aichat/concepts.md; this page is the implementation-oriented reference.