markpollack/agent-sandbox

Sandbox abstraction for secure code execution in AI agent applications

★ 3Forks 1JavaGitHub ↗Compare

README

Agent Sandbox

One Java API for synchronous command execution and workspace file operations across multiple backends. The shared command/file contract is tested by the AbstractSandboxTCK; backend-specific or unsupported capabilities, such as interactive execution, are not interchangeable. An agent, evaluation harness, or build runner can choose a local process, a Docker container, or a remote Firecracker microVM at construction time.

Documentation: lab.pollack.ai/projects/agent-sandbox — backends, the core API, file operations and customizers. Release notes: What's New.

Concepts

  • Sandbox — the backend-neutral surface: exec, files(), workDir(), and close(). Everything a backend cannot offer everywhere stays off this interface.
  • ExecSpec / ExecResult — an immutable command specification (argument vector, environment overrides, timeout) and its outcome (exit code, stdout, stderr, duration). An ExecSpec is an argument vector on every backend: arguments are never split or expanded by a shell.
  • SandboxFiles — fluent file operations against the sandbox workspace, chained back into the sandbox with and().
  • ExecSpecCustomizer — construction-time interception of each spec before it runs.
  • AbstractSandboxTCK — the compatibility kit every backend passes, published in the agent-sandbox-core test-jar so an out-of-tree backend can run it too.

Modules

Module Backend Isolation Notable dependencies
agent-sandbox-core LocalSandbox none — runs on the host zt-exec, slf4j
agent-sandbox-docker DockerSandbox container Testcontainers
agent-sandbox-e2b E2BSandbox remote microVM jackson, awaitility

LocalSandbox provides process and workspace convenience but no security isolation; it is for trusted code and development. DockerSandbox is the convenient local Docker backend. It is not presented as a hardened multi-tenant execution service, and a container alone is not a complete hostile-workload security boundary.

Container images are yours, not ours

This project publishes Maven artifacts. It does not build, publish, own, or maintain any container image.

agent-sandbox-docker runs an image you select. There is no default: every constructor and DockerSandbox.builder().image(...) requires an explicit reference, and build() fails before it contacts Docker if you did not supply one.

try (Sandbox sandbox = DockerSandbox.builder()
        .image("your-registry/your-runtime@sha256:...")   // required
        .build()) {
    ...
}

What that means for you:

  • You own the image — its provenance, contents, patching cadence, and vulnerability policy. Nothing about it is asserted or vouched for here.
  • Prefer an immutable digest (repo@sha256:...) over a mutable tag for reproducible operation, and scan and attest whatever you pick. A :latest tag can change under you without any release of this library.
  • The image needs a POSIX userland: bash, GNU coreutils, and GNU findutils. File listing uses find -printf, which BusyBox does not implement, so minimal BusyBox images will not work.

Docker access is a privileged trust boundary

Reaching a Docker daemon is not a sandbox in itself. A caller who can start containers can generally obtain root-equivalent control of the host, and container isolation alone is not a security guarantee against hostile code. Treat this backend as workload separation, and add the kernel-, user-, and network-level controls your threat model actually requires before running code you do not trust.

Maven

<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>agent-sandbox-core</artifactId>
    <version>0.9.3</version>
</dependency>

Add agent-sandbox-docker or agent-sandbox-e2b for those backends; each brings agent-sandbox-core transitively.

Example

try (Sandbox sandbox = LocalSandbox.builder()
        .tempDirectory("build-")
        .build()) {

    ExecResult result = sandbox.files()
        .create("pom.xml", pomContent)
        .create("src/main/java/App.java", code)
        .and()
        .exec(ExecSpec.of("mvn", "-q", "compile"));

    if (result.success()) {
        System.out.println(result.stdout());
    }
}

Build

./mvnw clean verify

That runs the unit tests and the LocalSandbox TCK. The other two backends need infrastructure and gate themselves off when it is absent:

./mvnw -B -pl agent-sandbox-core,agent-sandbox-docker -am clean verify \
  -Dsandbox.infrastructure.test=true                                     # needs a Docker daemon
./mvnw -pl agent-sandbox-e2b verify                                      # needs E2B_API_KEY

The Docker suite runs against a minimal ubuntu fixture pinned by digest. That fixture is a test dependency only — it is not shipped, not endorsed as an application runtime, and carries no recommendation for your own image choice.

A local dependency CVE scan is available and is deliberately not part of ordinary CI:

./mvnw -Powasp verify        # OWASP dependency-check, fails on CVSS >= 7.0
./scripts/security-scan.sh   # Trivy, HIGH/CRITICAL

Maturity

Pre-1.0 and versioned accordingly: the API may change between minor versions. LocalSandbox and DockerSandbox are exercised by the full TCK; E2BSandbox passes the same TCK against the live E2B service. agent-sandbox-docker requires Docker Engine with API version 1.44 or newer.

The 0.10.0 dependency closure has three disclosed upstream findings in Apache HttpComponents classes embedded and relocated inside the Java Docker transport. The HTTP/2/HPACK condition is not reachable on the Docker HTTP/1.1 path examined; the other two require a malicious response from the trusted, root-equivalent local Docker daemon. No path from untrusted code inside a selected container to those advisory conditions was identified under this trust model. Published Testcontainers/docker-java combinations examined do not yet embed the fixed versions, and ordinary dependency overrides cannot replace relocated classes inside the zerodep JAR. The findings remain visible and are accepted for the trusted-local-daemon use case; changing the caller-selected image cannot remove them.

Breaking change in 0.10.0. DockerSandbox no longer has a default image. The no-argument DockerSandbox() constructor is removed, and builder() now requires .image(...). Previously it silently pulled ghcr.io/spring-ai-community/agents-runtime:latest — a mutable tag in a namespace this project does not control or maintain. Pass the image you want; constructors taking an explicit image are unchanged.

Licensing

This project originated from earlier Apache-licensed work in the Spring AI Community. Its last Apache License 2.0 release was org.springaicommunity:agent-sandbox-* 0.9.1. Beginning with 0.9.2 — the first release under the io.github.markpollack coordinates — new development is licensed under the Business Source License 1.1.

The 0.9.0 and 0.9.1 artifacts published under Apache 2.0 remain available under their original terms; nothing already released has been relicensed. See Business Source License 1.1 and LICENSE-APACHE.txt.

Contributors

markpollackactions-user

Issues