OpenWebUI Code Execution via Open Terminal (replacing Jupyter)
Context
OpenWebUI's code execution and code interpreter features ran through the Jupyter service (minimal-notebook). Three problems motivated a change (issue #1117):
- Weak isolation. Jupyter sits on the
backendnetwork alongside application services and executes user code in a single, shared kernel. The network isolation policy permitsbackend → data, so a successful container breakout has a path toward NATS and the databases — an unacceptable blast radius for arbitrary user-submitted code. - No file delivery. The Jupyter engine has no mechanism to hand a generated file (doc/excel/pdf) to the end user; the model emits
sandbox:/…links that do not resolve, so files were only reachable from the Jupyter UI. - Upstream direction. OpenWebUI now classifies Jupyter as a legacy code-execution engine and recommends Open Terminal, its sandboxed, natively-integrated runtime with a file browser.
Decision Drivers
- Isolation / least privilege — the code-execution sandbox must not be able to reach NATS or the databases, and should isolate users from one another.
- File delivery — end users must be able to download generated artifacts.
- OpenWebUI-native, maintained path — prefer the supported integration over the deprecated engine.
- Reproducible, air-gap-friendly — no outbound internet at runtime; required libraries baked into the image.
Decision
Route OpenWebUI's code-execution path to a new open-terminal service:
(the base already ships pandas, openpyxl, python-docx, weasyprint, matplotlib, xlsxwriter)
- Isolation —
OPEN_TERMINAL_MULTI_USER=truefor per-user home directories; attached to a dedicatedcode-sandboxnetwork only, whose sole residents are the sandbox and its callers (open-webui; AI-Hub agents as a follow-up). Because Docker networks are bidirectional, keeping the sandbox offbackendis what severs lateral reach to the application/processing services as well asdata/NATS. The network isinternal: truein non-dev stages (no outbound internet); host port exposed only indev/local/build. - Wiring — OpenWebUI connects via the Integrations env
TERMINAL_SERVER_CONNECTIONS(bearer auth withOPEN_TERMINAL_API_KEY), replacing the JupyterCODE_EXECUTION_*/CODE_INTERPRETER_*engine variables. - Scope — plain LLM models only. OpenWebUI orchestrates Open Terminal through native
execute_codetool-calling; the existing/openaiproxy passestools/tool_callsthrough transparently for plain models, and all LiteLLM text-generation models declaresupports_function_calling: true. Native function calling must be enabled per model. - AI-Hub agent chats are deferred. Agent surfaces (the
aihub_pipeline.pyPipeand the agent branch of the/openaiproxy) own their own generation and do not expose OpenWebUI-orchestrated tool-calling, so Open Terminal does not engage for them. Supporting agents requires the OpenAI tool-calling handshake inside the agent framework and is tracked as a separate follow-up — consistent with #1117's "code execution invoked from our own agents (covered separately)". - Jupyter is retained for now (its full removal is a follow-up once any remaining consumers migrate), but OpenWebUI no longer uses it.
Consequences
- Positive — the code-execution sandbox sits alone in
code-sandboxand can reach only its callers, so a breakout reaches neither NATS/databases nor the otherbackendservices (LiteLLM, vLLM, MinerU, Speaches, Presidio, OTEL); it isolates users per home directory; generated files are downloadable via Open Terminal's file browser; the integration is the OpenWebUI-native, maintained path; the image is reproducible and requires no outbound internet to function (libraries are baked in at build time). - Trade-offs
- Only plain-LLM chats gain code execution this iteration (agent support deferred); native function calling must be enabled per model and multi-step reliability varies by model.
- Agents become a bridge node (future). When the agents service later joins
code-sandboxto use the sandbox, it will sit oncode-sandbox+backend+data. At the network layer the sandbox still cannot ride through an agent tobackend, but a compromised agent process would be a pivot point — socode-sandboxmembership is kept minimal (sandbox + callers only). - Dev stays non-internal.
code-sandboxisinternal: trueonly in non-dev stages; indevit is non-internal for localhost access, so the no-internet / no-lateral-reach guarantees apply tolocal/build/nightly/latest, notdev. Egress can additionally be firewalled per-deployment viaOPEN_TERMINAL_ALLOWED_DOMAINSif required. - Single shared container, per-user isolation only.
OPEN_TERMINAL_MULTI_USER=truegives each user a separate Linux account and home directory with standard filesystem permissions, but it is one shared container — all users share the same kernel, CPU, memory,/tmp, and process list. This is a convenience for small, trusted groups, not a hard multi-tenant boundary; a container-per-user model would be required for that. Accepted under the current threat model. /homepersistence and unbounded growth.${VOLUME_ROOT}/open-terminal:/homeaccumulates per-user artifacts on the host with no retention/quota policy yet, so disk usage grows over time — operators must monitor and prune manually until a janitor/TTL is added (a follow-up).- The image is ~1.19 GB; the Jupyter container keeps running, unused, until a later cleanup. See network isolation (
2025_12_22_docker_network_isolation.md).
- Deployment prerequisite — publish
open-terminal-office:0.11.34to ghcr before any non-dev stage pulls it (make -C infra/deployment build-and-push-open-terminal-image).nightly/latestpull this exact tag; if it is absent,open-webui'sdepends_on: open-terminal (service_healthy)gate fails and the stack will not start. Bump the tag deliberately and re-publish whenever the base tag or baked-in libraries change. - Licensing — Open Terminal is MIT (standard, OSI-approved; no branding clause and no end-user threshold). This is distinct from
open-webui, whose modified-BSD "Open WebUI License" carries the branding/≤50-user clause — that obligation comes from open-webui, not from adding this sandbox.
