[{"data":1,"prerenderedAt":552},["ShallowReactive",2],{"docs:\u002Fdocs\u002Faichat\u002Fisolated-exec-envs":3,"docs-nav":525},{"id":4,"title":5,"body":6,"description":517,"extension":518,"meta":519,"nav":503,"navigation":520,"path":521,"seo":522,"stem":523,"__hash__":524},"docs\u002Fdocs\u002Faichat\u002Fisolated-exec-envs.md","Isolated exec environments",{"type":7,"value":8,"toc":501},"minimark",[9,13,17,31,42,47,50,67,80,83,87,93,138,148,153,159,181,191,205,209,233,237,243,246,292,295,323,336,340,353,365,369,372,417,420,424,427,438,444,457,461,464],[10,11,5],"h1",{"id":12},"isolated-exec-environments",[14,15,16],"p",{},"AiChat uses E2B sandboxes as its isolated execution layer for two different paths:",[18,19,20,28],"ul",{},[21,22,23,27],"li",{},[24,25,26],"code",{},"python_exec"," and related data tools in chat sessions",[21,29,30],{},"Forge jobs launched from the chat control surface",[14,32,33,34,37,38,41],{},"The shared boundary lives in ",[24,35,36],{},"apps\u002Faichat\u002Fserver\u002Futils\u002Fsandbox.ts",", which is the only server module that imports the E2B SDK. Everything else asks for a sandbox through the ",[24,39,40],{},"SandboxDriver"," interface.",[43,44,46],"h2",{"id":45},"what-the-isolated-environment-is","What the isolated environment is",[14,48,49],{},"The sandbox is a remote filesystem + command runtime created from an E2B template. The codebase currently uses two templates:",[18,51,52,58],{},[21,53,54,57],{},[24,55,56],{},"base"," — Debian + Python 3 + Node.js",[21,59,60,63,64,66],{},[24,61,62],{},"code-interpreter-v1"," — ",[24,65,56],{}," plus Jupyter and a Python data stack",[14,68,69,70,73,74,76,77,79],{},"Template choice is part of the sandbox acquisition call, not a separate subsystem. The driver also documents a ",[24,71,72],{},"desktop"," template, but the chat and Forge paths in this repository use ",[24,75,56],{}," and ",[24,78,62],{},".",[14,81,82],{},"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\u002Fresume is not implemented.",[43,84,86],{"id":85},"code-runs-in-chat","Code runs in chat",[14,88,89,90,79],{},"Python execution in chat flows through ",[24,91,92],{},"apps\u002Faichat\u002Fserver\u002Futils\u002FpythonTool.ts",[94,95,96,105,114,132],"ol",{},[21,97,98,99,101,102,79],{},"The agent tool ",[24,100,26],{}," is built by ",[24,103,104],{},"buildPythonExecTool(sessionId)",[21,106,107,108,111,112,79],{},"The tool calls ",[24,109,110],{},"acquireChatSandbox(sessionId)"," from ",[24,113,36],{},[21,115,116,119,120],{},[24,117,118],{},"acquireChatSandbox()"," always requests an interpreter sandbox with:\n",[18,121,122,127],{},[21,123,124],{},[24,125,126],{},"interpreter: true",[21,128,129],{},[24,130,131],{},"template: 'code-interpreter-v1'",[21,133,134,135,79],{},"The tool then calls ",[24,136,137],{},"sandbox.runCode(handle, params.code, ...)",[14,139,140,141,144,145,147],{},"That ",[24,142,143],{},"runCode"," path gives ",[24,146,26],{}," 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.",[149,150,152],"h3",{"id":151},"output-shape-for-code-runs","Output shape for code runs",[14,154,155,158],{},[24,156,157],{},"runCode()"," returns:",[18,160,161,166,171,176],{},[21,162,163],{},[24,164,165],{},"stdout",[21,167,168],{},[24,169,170],{},"stderr",[21,172,173],{},[24,174,175],{},"exitCode",[21,177,178],{},[24,179,180],{},"charts",[14,182,183,184,187,188,190],{},"The chart list is produced by ",[24,185,186],{},"mapExecution()"," in ",[24,189,36],{},". It prefers Plotly outputs first, then falls back to image outputs such as matplotlib PNGs.",[14,192,193,196,197,200,201,204],{},[24,194,195],{},"pythonTool.ts"," keeps the LLM-visible summary small and stores the full execution details separately in ",[24,198,199],{},"details",". The chat runtime then uses ",[24,202,203],{},"toolDetailsFailed()"," to mark non-zero Python exits as tool errors.",[149,206,208],{"id":207},"data-attachment-flow","Data attachment flow",[14,210,211,212,215,216,76,219,221,222,225,226,229,230,232],{},"When a user attaches files in chat, ",[24,213,214],{},"chatRuntime.ts"," enables the ",[24,217,218],{},"datasets",[24,220,26],{}," tool ids together. That allows the dataset tools in ",[24,223,224],{},"apps\u002Faichat\u002Fserver\u002Futils\u002FdatasetTools.ts"," to fetch the original uploaded file into the same chat sandbox with ",[24,227,228],{},"dataset_fetch_file",", after which ",[24,231,26],{}," can analyze it directly.",[43,234,236],{"id":235},"agent-runs-in-forge","Agent runs in Forge",[14,238,239,240,79],{},"Forge jobs use the same sandbox driver, but they acquire a job-scoped sandbox in ",[24,241,242],{},"apps\u002Faichat\u002Fserver\u002Futils\u002FforgeJobs.ts",[14,244,245],{},"The job orchestration path is:",[94,247,248,268,285],{},[21,249,250,253,254,257,258,257,261,264,265,79],{},[24,251,252],{},"forgeTools.ts"," exposes the job-facing chat tools such as ",[24,255,256],{},"forge_create_job",", ",[24,259,260],{},"forge_plan_job",[24,262,263],{},"forge_run_job",", and ",[24,266,267],{},"forge_get_job",[21,269,270,273,274,277,278,281,282,79],{},[24,271,272],{},"forgeJobs.ts"," creates the job record and later acquires the ",[24,275,276],{},"forge-job:${job.id}"," scope with ",[24,279,280],{},"useSandbox().acquire(...)",", using ",[24,283,284],{},"job.sandboxTemplate || 'base'",[21,286,287,288,291],{},"The chosen Forge adapter in ",[24,289,290],{},"apps\u002Faichat\u002Fserver\u002Futils\u002FforgeAdapters.ts"," runs against that handle.",[14,293,294],{},"There are two adapter modes visible in the codebase:",[18,296,297,317],{},[21,298,299,302,303,257,306,264,309,312,313,316],{},[24,300,301],{},"pi"," — the default adapter, which drives the sandbox with ",[24,304,305],{},"bash",[24,307,308],{},"read_file",[24,310,311],{},"write_file"," tools through the ",[24,314,315],{},"pi-agent-core"," loop",[21,318,319,322],{},[24,320,321],{},"claude-code"," — a headless CLI-based adapter that also runs inside the same sandbox driver",[14,324,325,326,328,329,332,333,335],{},"Unlike chat Python runs, Forge job execution is repository-oriented. ",[24,327,272],{}," mints the scoped GitHub token, clones the repository, and creates the branch before handing the checked-out ",[24,330,331],{},"repoPath"," to the adapter. The adapter edits and tests inside that path. Afterward, ",[24,334,272],{}," commits, pushes, and collects the diff. Keeping those Git operations in the orchestrator is part of the execution boundary.",[149,337,339],{"id":338},"sandbox-templates-for-jobs","Sandbox templates for jobs",[14,341,342,344,345,348,349,352],{},[24,343,252],{}," exposes the available templates to the chat UI, and job creation stores the selected template in the ",[24,346,347],{},"forge_jobs.sandbox_template"," column (mapped to ",[24,350,351],{},"forgeJobs.sandboxTemplate"," by Drizzle). The current code paths use:",[18,354,355,360],{},[21,356,357,359],{},[24,358,56],{}," for most repos",[21,361,362,364],{},[24,363,62],{}," for data-heavy Python work",[43,366,368],{"id":367},"isolation-boundaries","Isolation boundaries",[14,370,371],{},"The codebase enforces isolation in a few concrete ways:",[18,373,374,382,396,399,405,411],{},[21,375,376,377,187,379,79],{},"All sandbox access goes through ",[24,378,40],{},[24,380,381],{},"sandbox.ts",[21,383,384,385,387,388,391,392,395],{},"Only ",[24,386,381],{}," imports ",[24,389,390],{},"e2b"," or ",[24,393,394],{},"@e2b\u002Fcode-interpreter"," directly.",[21,397,398],{},"Chat code execution can reuse an interpreter sandbox within one session id while that sandbox remains alive.",[21,400,401,402,404],{},"Each Forge run uses a separate sandbox handle keyed by ",[24,403,276],{}," and releases it when the run finishes.",[21,406,407,408,410],{},"Sandboxes are created with a 10-minute E2B timeout. Individual code and command calls fall back to 60 seconds at the driver level; ",[24,409,26],{}," defaults to 30 seconds, and Forge supplies per-step overrides.",[21,412,413,416],{},[24,414,415],{},"runCommand()"," returns stdout, stderr, and exit code; it does not throw the raw SDK error back into the app path.",[14,418,419],{},"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.",[43,421,423],{"id":422},"where-results-appear","Where results appear",[14,425,426],{},"For chat Python runs, the results surface in two places:",[18,428,429,432],{},[21,430,431],{},"the streamed tool result seen by the LLM",[21,433,434,435,437],{},"the ",[24,436,199],{}," payload returned to the chat runtime and UI, including charts",[14,439,440,441,443],{},"For Forge jobs, the adapter records a structured trace and summary, and ",[24,442,272],{}," persists job state, cost, diff, and error information in the job record.",[14,445,446,447,76,449,452,453,456],{},"In both cases, errors are normalized before they reach the caller. Python execution folds kernel errors into ",[24,448,170],{},[24,450,451],{},"exitCode: 1","; command execution returns a similar ",[24,454,455],{},"{ stdout, stderr, exitCode }"," shape.",[43,458,460],{"id":459},"operational-notes-and-limits","Operational notes and limits",[14,462,463],{},"A few implementation details are worth keeping in mind:",[18,465,466,472,479,486,491,494],{},[21,467,468,471],{},[24,469,470],{},"acquire()"," caches the first sandbox created for an id and reuses it while it is still running.",[21,473,474,475,478],{},"Forge releases and kills its sandbox in a ",[24,476,477],{},"finally"," block after every run; it does not pause or resume that environment.",[21,480,481,482,485],{},"The interpreter sandbox flips Plotly to the ",[24,483,484],{},"plotly_mimetype"," renderer once per kernel so structured chart output can be captured.",[21,487,488,490],{},[24,489,186],{}," caps chart count and size to keep outputs bounded.",[21,492,493],{},"The chat sandbox and Forge sandbox are both E2B-backed, but they are not the same handle and do not share filesystem state.",[21,495,496,497,500],{},"The repository’s docs already describe this at a high level in ",[24,498,499],{},"apps\u002Fweb\u002Fcontent\u002Fdocs\u002Faichat\u002Fconcepts.md","; this page is the implementation-oriented reference.",{"title":502,"searchDepth":503,"depth":503,"links":504},"",3,[505,507,511,514,515,516],{"id":45,"depth":506,"text":46},2,{"id":85,"depth":506,"text":86,"children":508},[509,510],{"id":151,"depth":503,"text":152},{"id":207,"depth":503,"text":208},{"id":235,"depth":506,"text":236,"children":512},[513],{"id":338,"depth":503,"text":339},{"id":367,"depth":506,"text":368},{"id":422,"depth":506,"text":423},{"id":459,"depth":506,"text":460},"How AiChat runs Python cells and agent jobs in E2B sandboxes scoped to each chat session or agent run.","md",{},true,"\u002Fdocs\u002Faichat\u002Fisolated-exec-envs",{"title":5,"description":517},"docs\u002Faichat\u002Fisolated-exec-envs","9vq4yqtU_j3zQlu42i3vTIRXU1tcbFS2vApwKcPK1wQ",[526],{"title":527,"path":528,"stem":529,"children":530,"page":551},"Docs","\u002Fdocs","docs",[531],{"title":532,"path":533,"stem":534,"children":535,"page":551},"Aichat","\u002Fdocs\u002Faichat","docs\u002Faichat",[536,541,545,546],{"title":537,"path":538,"stem":539,"nav":540},"Getting started","\u002Fdocs\u002Faichat\u002Fgetting-started","docs\u002Faichat\u002Fgetting-started",1,{"title":542,"path":543,"stem":544,"nav":506},"Concepts","\u002Fdocs\u002Faichat\u002Fconcepts","docs\u002Faichat\u002Fconcepts",{"title":5,"path":521,"stem":523,"nav":503},{"title":547,"path":548,"stem":549,"nav":550},"Markdown test","\u002Fdocs\u002Faichat\u002Fmarkdown-test","docs\u002Faichat\u002Fmarkdown-test",9,false,1786110827901]