Sandbox Execution
Sandbox Execution
Section titled “Sandbox Execution”Agents that run untrusted code, LLM-generated shell commands, or data
transforms route those calls through a Sandbox instead of the hosting JVM.
The backend is pluggable: a Docker container ships as the production default,
and stronger isolation tiers (micro-VM, remote) plug in through the
SandboxProvider SPI without touching agent code.
Maven Coordinates
Section titled “Maven Coordinates”<dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-sandbox</artifactId> <version>${project.version}</version></dependency>| Type | Role |
|---|---|
Sandbox | Per-instance resource. exec(List<String> command, Duration timeout), writeFile(Path, String), readFile(Path), optional expose(int port), snapshot(), hibernate(). AutoCloseable — never leaks to the hosting JVM. |
SandboxProvider | Factory: name(), isAvailable(), create(String image, SandboxLimits limits, Map<String,String> metadata), and tier() (defaults to PROCESS). Discovered via ServiceLoader — applications never new a provider directly. |
Sandboxes | Tier-aware resolver. Sandboxes.select(IsolationTier minTier) returns the strongest available SandboxProvider at or above the requested floor, as an Optional. |
IsolationTier | PROCESS, CONTAINER, MICRO_VM, REMOTE — ordered weakest to strongest. |
SandboxLimits | A record (double cpuFraction, long memoryBytes, Duration wallTime, NetworkPolicy networkPolicy). Ceilings enforced by each backend; exceeding one terminates the sandbox. |
NetworkPolicy | Egress policy, per-sandbox (not global). Mode is NONE, GIT_ONLY, ALLOWLIST, FULL; constants NetworkPolicy.NONE, NetworkPolicy.GIT_ONLY, NetworkPolicy.FULL; build an allowlist with NetworkPolicy.allowlist(host...). The default is NONE. |
@SandboxTool | Annotation binding an @AiTool method to a provider’s capabilities. |
Backends shipped in-tree
Section titled “Backends shipped in-tree”DockerSandboxProvider— the production default. Shells out to thedockerCLI in argv form (no shell interpolation), with per-call--rmand--network none(unlessNetworkPolicy.FULLis set) and a strict working-directory mount. Requires a running Docker daemon and fails hard when one is absent — it advertises availability from confirmed runtime state, never from classpath presence (Correctness Invariant #5, Runtime Truth).InProcessSandboxProvider— a dev-only reference implementation that runs commands viaProcessBuilderinside a temp directory. It is not a security boundary. It stays unavailable unless explicitly enabled with-Datmosphere.sandbox.insecure=true, and emits aWARNat startup. Tests and samples that cannot run Docker locally opt in deliberately.
Third-party backends
Section titled “Third-party backends”Firecracker, Kata, Vercel Sandbox, E2B, Modal, and Blaxel ship in separate
modules that implement SandboxProvider and register through
META-INF/services/org.atmosphere.ai.sandbox.SandboxProvider. The foundation
module stays dependency-free.
Quick Start
Section titled “Quick Start”The spring-boot-coding-agent sample resolves a provider through the
tier-aware Sandboxes.select(IsolationTier.PROCESS) call — a PROCESS floor
that prefers the strongest available isolation — and drives it with the SPI
directly. There is no fluent builder, allocate, or varargs exec.
@Agent(name = "coding-agent")public class CodingAgent {
// Tier-aware selection: PROCESS floor, preferring the strongest backend. private static SandboxProvider resolveProvider() { return Sandboxes.select(IsolationTier.PROCESS).orElse(null); }
public String readFile(String gitUrl, String path) { var provider = resolveProvider(); // SandboxLimits is a record: (cpuFraction, memoryBytes, wallTime, networkPolicy). // Cloning needs egress, so override the default NONE policy. var limits = new SandboxLimits( 1.0, 512L * 1024L * 1024L, Duration.ofSeconds(30), NetworkPolicy.FULL); try (Sandbox sandbox = provider.create("alpine:3.20", limits, Map.of("owner", "coding-agent"))) { // exec takes a List<String> command and a per-call timeout. sandbox.exec( List.of("git", "clone", "--depth", "1", gitUrl, "/workspace"), Duration.ofMinutes(2)); return sandbox.readFile(Path.of("/workspace/" + path)); } }}Closing the try-with-resources terminates the sandbox: the container stops,
volumes unmount, and the temp directory is removed. The provider’s lifecycle
is tied to the Sandbox handle, so callers never manage cleanup explicitly.
Security notes
Section titled “Security notes”- Every
execis argv-form — nosh -cwrapping. Thecoding-agentsample exercises this against a strict GitHub-URL regex so shell metacharacters cannot escape. DockerSandboxProviderrejects volume mounts outside the per-sandbox workdir; path traversal is blocked withPath.resolve().normalize().startsWith(workdir).NetworkPolicy.FULLis never the default. The@SandboxToolannotation (membersbackend, a requiredimage,cpuFraction,memoryBytes,wallTimeSeconds, andboolean network()defaulting tofalse) opts a tool into egress with, e.g.,@SandboxTool(image = "ubuntu:24.04", network = true)— the explicitnetwork = trueis the authorization receipt.
In-process eval — container-free JavaScript
Section titled “In-process eval — container-free JavaScript”The Sandbox SPI above runs code in a container. For lightweight
computation where a container is overkill — arithmetic, data shaping, JSON and
string work — Atmosphere also ships an in-process eval tool: a sandboxed
interpreter for model-authored code. It is the counterpart to code_exec: no
container, no network, instant, and available where Docker is not.
The interpreter is pluggable via the EvalEngine SPI (ServiceLoader,
highest-priority()-available wins — the same pattern as AgentRuntime and
SandboxProvider). JavaScript via Mozilla Rhino ships as the default engine;
an alternative — GraalJS, a Python interpreter, a WASM runtime — plugs in by
adding its jar with a META-INF/services/org.atmosphere.ai.code.EvalEngine
entry, no Atmosphere change required. Every engine must honour the same isolation
contract and EvalLimits (below).
Why an interpreter, not “pure Java”? Two reasons. The model authors computation as code strings at runtime, and it writes JavaScript/Python, not Java. More fundamentally, arbitrary Java cannot be sandboxed in-process: JShell / the Compiler API produce bytecode with full JVM authority, and the only in-process containment mechanism —
SecurityManager— is disabled from JDK 24 on. In-process and sandboxed therefore requires a language with a language-level sandbox (Rhino’sClassShutter+ no built-in I/O); for OS-level isolation of arbitrary code, use the containercode_execinstead.
Off by default (opt-in). Evaluating model-generated code is a deliberate
choice. Enable it with a system property (or the equivalent
ORG_ATMOSPHERE_AI_EVAL_* environment variable) and add the optional Rhino
dependency:
-Dorg.atmosphere.ai.eval.enabled=true<dependency> <groupId>org.mozilla</groupId> <artifactId>rhino</artifactId></dependency>| Property | Default | Meaning |
|---|---|---|
org.atmosphere.ai.eval.enabled | false | Master switch — the tool is not offered unless true and Rhino is on the classpath. |
org.atmosphere.ai.eval.instructionBudget | 10000000 | Interpreted-instruction ceiling per call — a runaway loop trips it and aborts. |
org.atmosphere.ai.eval.timeoutMillis | 5000 | Wall-clock ceiling per call. |
org.atmosphere.ai.eval.maxOutputChars | 8000 | Cap on the returned text. |
The tool advertises runtime truth: when enabled=true but Rhino is absent,
startup logs a warning and the tool reports inactive rather than offering
something that cannot run.
Security model
Section titled “Security model”- No host reach. The scope is built with Rhino’s
initSafeStandardObjects()— ECMAScript built-ins (Math,JSON,Array) but none of the LiveConnect Java bridge (java,Packages,getClass). A deny-allClassShutteris layered on top, so no Java class resolves even via a reflective escape. Rhino JavaScript has no built-in file or network I/O. - Bounded CPU. Interpreted mode plus an instruction observer enforce the
budget on a stock JVM (no GraalVM runtime required). The abort is a Java
Error, so a scripttry/catchcannot swallow it and keep looping. - No cross-call state. Every call runs in a fresh scope — one evaluation cannot see or corrupt another’s.
- Governance-gateable. The tool is tagged
ToolKind.EXECUTE, so aToolApprovalPolicyor governance policy gates it like any other code-execution surface.
Source and samples
Section titled “Source and samples”- Module README:
modules/sandbox/README.md - Sample:
spring-boot-coding-agent— clone/read/stream coding workflow over the Docker sandbox provider