Skip to main content
Filesystem explains where an agent’s “files and commands” come from. When these operations must be isolated from the host process, executed in a replaceable execution environment (local Unix, Docker, etc.), and able to restore the same workspace state across multiple calls, use the sandbox mode described here (filesystem(SandboxFilesystemSpec)).

1. What Sandbox Solves

  • Execution boundary: the model operates on files and commands through the same AbstractFilesystem / ShellExecuteTool interfaces, but the actual IO and processes happen in an isolated environment managed by the sandbox client — suitable for scenarios where user input cannot be fully trusted, or where decoupling from the production host is required.
  • Recoverable work unit: unlike “single HTTP requests”, multi-turn calls should be able to continue in the same logical workspace. SandboxManager persists sandbox-side state after each call ends (via SandboxStateStore), and looks it up by IsolationScope + sessionId/userId keys on the next acquire.
  • Relationship to the Harness workspace: the host still has a WorkspaceManager root directory; what the sandbox sees is defined by WorkspaceSpec and workspace projection mechanisms (syncing/mounting certain host paths into the sandbox at startup).

2. Assembly in Harness

When sandbox mode is enabled, HarnessAgent.Builder will:
  1. Call SandboxFilesystemSpec#toSandboxContext(hostWorkspaceRoot) to get a SandboxContext (containing SandboxClient, isolation scope, snapshot spec, WorkspaceSpec, etc.), while packing host-side paths to project into the sandbox (AGENTS.md, skills/, subagents/, knowledge/) into a WorkspaceProjectionEntry (see §7 Workspace Projection).
  2. Use SandboxBackedFilesystem as the agent’s AbstractFilesystem implementation (transparent to upper layers).
  3. Construct SandboxManager(client, stateStore, agentId); when no explicit SandboxFilesystemSpec#sandboxStateStore is configured, defaults to SessionSandboxStateStore(effectiveSession, agentId), tying sandbox metadata to the current Session.
  4. Register SandboxLifecycleHook(sandboxManager, filesystemProxy) (priority 50): on every PreCall it acquires → start() (including the 4-branch workspace initialization, see §6 Snapshot and 4-Branch Recovery); on PostCall / Error it stop() (persist snapshot) → persist state → release and clears the active session on the proxy.
Only when the backend implements AbstractSandboxFilesystem does HarnessAgent register ShellExecuteTool; in sandbox mode, files and shell commands go through the sandbox — the host is unaffected. For steps to implement a custom non-Docker isolation backend (SandboxClient, SandboxState, SandboxFilesystemSpec, etc.) and a self-check checklist, see §5 Extending Custom Sandbox Execution Environments.

3. Isolation Dimensions (IsolationScope)

IsolationScope controls the persistence key for sandbox state (sandbox mode) and the namespace prefix for shared storage (store mode, see Filesystem mode 1). Both modes share the same enum with consistent semantics.

3.1 SESSION — Conversation Isolation (Default)

Each conversation has its own independent sandbox with no interference. Suitable for multi-user SaaS where each session’s temporary work files and installed dependencies are isolated.
Most common distributed scenario: multiple Pods/processes serving multiple sessions for the same user in parallel, but the user’s long-term memory (MEMORY.md, memory/) must stay consistent across all replicas. Sandbox mode + USER: different sessions (different Pods) write the latest snapshot reference to the same state slot (key = userId) after each conversation ends. The next time any replica handles the same user, it restores the same workspace from that snapshot. Note this is sequential reuse, not concurrent sharing: concurrent requests each get independent containers, but both update the same state slot on stop(), with last-write winning. For AGENT / GLOBAL scopes requiring strong mutual exclusion, see §9 Concurrency Control. Remote mode + USER (equivalent without sandbox): RemoteFilesystemSpec uses userId as the KV namespace prefix, so all reads/writes routed to MEMORY.md, memory/, etc. land under the same store key — enabling memory sharing across distributed replicas without snapshots.

3.3 AGENT — Agent-level Sharing

All users and sessions of the same agent (by name) share the workspace snapshot or storage namespace. Suitable for “public knowledge base” agents: a single global workspace with write order determined by call sequence; suitable for tool-type, read-only, or admin scenarios.

3.4 GLOBAL — Global Sharing

The widest sharing scope within a store/workspace instance. Use with caution.

4. Custom Sandbox Instances and Lifecycle Management

By default, SandboxManager fully manages sandbox create / start / stop / shutdown (self-managed). When you need to reuse an existing container, share a sandbox across multiple agents, or manage the container lifecycle yourself, you can hand lifecycle control back to the caller in two ways.

4.1 Passing an Existing Sandbox Instance (User-Managed, Highest Priority)

On each call, bring in an already-started Sandbox object via SandboxContext in RuntimeContext:
Behavior rules (SandboxManager.acquire’s 4-level priority):

4.2 Passing Serialized State (Precise Snapshot Recovery)

If you already have a SandboxState JSON string saved from a previous call, you can bypass SandboxStateStore automatic lookup and directly specify the state to restore:

4.3 Sharing a Sandbox Across Multiple Agents

5. Extending Custom Sandbox Execution Environments

When your application needs an isolation backend other than Docker (custom remote executor, commercial sandbox API, local mock, etc.), no changes to the Harness source are required: implement the contract types below and plug them in via HarnessAgent.Builder#filesystem(SandboxFilesystemSpec). Overall assembly still follows §2: SandboxContextSandboxBackedFilesystemSandboxManagerSandboxLifecycleHook.

5.1 Extension Points Overview

5.2 Implementing SandboxClient and Options

  1. Define MySandboxClientOptions extends SandboxClientOptions: getType() returns a stable string (e.g. acme), consistent with the type in persistence/config. If you need to deserialize options from YAML/JSON, add Jackson registration for SandboxClientOptions polymorphism (see framework’s DockerSandboxClientOptions and the base class’s @JsonTypeInfo); skip this if configuring from Java code only.
  2. Implement SandboxClient<MySandboxClientOptions>: in create construct a Sandbox that has not yet been start()ed; serializeState / deserializeState must be consistent with the SandboxState subclass fields; delete can be a no-op if there are no additional resources.
  3. If deserializeState uses objectMapper.readValue(json, SandboxState.class), that ObjectMapper must register HarnessSandboxJacksonModule (which includes built-in NamedType for docker etc.) and your NamedType(MySandboxState.class, "acme"). No-arg new DockerSandboxClient() automatically registers HarnessSandboxJacksonModule; new DockerSandboxClient(customMapper) requires manually registerModule(new HarnessSandboxJacksonModule()) and registerSubtypes, otherwise reading back persisted state will fail.

5.3 SandboxState and Jackson

  • Design intent: the subtype table is provided at runtime via Module / registerSubtypes, making it easy for the same application or downstream jars to extend state without modifying SandboxState.java.
  • This repo: in io.agentscope.harness.agent.sandbox.json.HarnessSandboxJacksonModule, add registerSubtypes(new NamedType(XxxSandboxState.class, "xxx")) for new official stores.
  • Application-private subclasses: call mapper.registerSubtypes(new NamedType(MySandboxState.class, "acme")) on any ObjectMapper that holds sandbox JSON, and ensure SandboxManager / SandboxStateStore paths use the same mapper configuration as the SandboxClient.

5.4 Implementing SandboxFilesystemSpec

  • createClient(): return your SandboxClient (or create it via options.createClient(), same pattern as DockerFilesystemSpec).
  • clientOptions(): return the mutable configuration object.
  • snapshotSpec() / workspaceSpec(): can provide default NoopSnapshotSpec and new WorkspaceSpec(); callers can still override with SandboxFilesystemSpec#snapshotSpec(...) before building the agent.
Reference implementation: InMemorySandboxFilesystemSpec in the repo (agentscope-harness test support, harness-example-sandbox example project) simulates a sandbox with a temp directory, requires no Docker, and is a good minimal skeleton to copy and modify.

5.5 Enabling

Same as Docker, pass your spec to HarnessAgent.builder():
When distributed Session or OSS/Redis snapshots are needed, continue configuring sandboxDistributed(...) as needed (see §10); snapshot spec and isolated execution backend are chosen independently on SandboxFilesystemSpec.

5.6 Self-Check Checklist

  • JSON written by Sandbox#getState() can be losslessly read back by SandboxClient#deserializeState.
  • SandboxClientOptions#getType() and the type in SandboxState JSON do not conflict globally.
  • All **ObjectMapper**s that do readValue(..., SandboxState.class) have the corresponding NamedType registered (including HarnessSandboxJacksonModule if Docker-compatible data must be read).
  • If using AbstractSandboxFilesystem, ShellExecuteTool and file APIs are routed to your Sandbox#exec and sandbox-internal path conventions.

5.7 Optional Sandbox Backends: Kubernetes / Daytona / E2B (agentscope-harness subpackages)

These three remote sandbox implementations are in agentscope-harness’s io.agentscope.harness.agent.sandbox.impl.* subpackages (alongside io.agentscope.harness.agent.sandbox.impl.docker), available within the same artifact; dependencies like fabric8 and protobuf-java are declared in agentscope-harness’s pom.xml. Jackson assembly: register at least HarnessSandboxJacksonModule on any ObjectMapper holding sandbox state JSON, then register each backend’s SimpleModule as needed (e.g., new KubernetesHarnessSandboxJacksonModule()), consistent with §5.3. Do not add @JsonSubTypes on core SandboxClientOptions for optional stores — this would cause harness to reverse-depend on optional modules creating Maven cycles; optional stores are wired on the application side via their own FilesystemSpec + SandboxClientOptions subclass. Dependency coordinates: depend only on agentscope-harness (or harness coordinates managed by agentscope / BOM); agentscope-all includes these implementation classes alongside harness.

5.8 Workspace Bind Mount (BindMountEntry)

Place io.agentscope.harness.agent.sandbox.layout.BindMountEntry (Jackson polymorphic name bind_mount) in WorkspaceSpec#getEntries(): the key (map key) is the relative mount point under workspace root (POSIX style, e.g. data{root}/data), hostPath is the absolute path on the host (Docker machine / K8s node), readOnly controls whether read-only. Snapshots and tar: when persisting workspace, the framework appends arguments like tar --exclude=./ plus the entry relative path to avoid including external directories under mount points in the archive (aligned with the Python reference implementation). If you want a path not in snapshots but still written by the applier as initial content, use an ephemeral regular file/directory entry rather than a bind mount. Security: when hostPath comes from configuration or upstream input, restrict it to trusted directories; bind mounts are equivalent to letting container-side processes directly access the host path.

6. Snapshot and 4-Branch Recovery

Sandbox.start() decides how to initialize the workspace via 4 branches, ensuring correct recovery under all combinations of “whether the container is still available” and “whether a snapshot is available”:
When Sandbox.stop() executes, if SandboxSnapshotSpec has persistence enabled, the workspace is tarred and stored in the snapshot backend (OSS, Redis, local file, etc.), and workspaceRootReady is set to true. This tar is the archive used by Branch B/C for the next recovery. WorkspaceEntry.ephemeral flag: each entry in WorkspaceSpec can be marked as ephemeral (re-written on every start) or non-ephemeral (saved with snapshot, only written on cold start). For host-side files that may be updated at any time like skills/ and AGENTS.md, use WorkspaceProjectionEntry (next section) rather than the ephemeral flag. Available snapshot spec types:

7. Workspace Projection and Skills Sync

Workspace projection (WorkspaceProjectionEntry) is the mechanism by which Harness syncs specific directories/files from the host workspace into the sandbox at every sandbox startup — the foundation for Skills and other capabilities running inside the sandbox.

7.1 Projection Scope

When SandboxFilesystemSpec builds SandboxContext, it defaults to packing the following host paths into the projection:
You can customize which root paths to project via SandboxFilesystemSpec#workspaceProjectionRoots(List<String>), or completely disable projection via workspaceProjectionEnabled(false).

7.2 How Projection Works

WorkspaceProjectionApplier runs at the end of Sandbox.start():
  1. Iterates all WorkspaceProjectionEntrys, collects the host-side file set, sorts by path, and computes a SHA-256 content hash.
  2. Packs these files into a tar, and decompresses them to the corresponding paths inside the sandbox workspace via Sandbox.hydrateWorkspace(archive).
  3. Stores the current hash in SandboxState.workspaceProjectionHash; on the next startup, if the hash is unchanged, skips the projection (avoiding redundant transfers).
This means: when skills/ content on the host is updated, the hash changes on the next sandbox start, and new files are automatically synced in; changes to skill files inside the sandbox are not synced back to the host.

7.3 How Skills Execute Inside the Sandbox

Harness’s SkillBox mechanism injects the instructions from workspace/skills/<skill-name>/SKILL.md into the agent’s system prompt; after the model understands “this skill is needed”, it executes scripts or commands in the skill directory via ShellExecuteTool. In sandbox mode, all of this happens inside the sandbox:
Benefit: scripts run in the isolated container; pip install, apt-get, rm -rf and other operations only affect the sandbox workspace — the host is unaffected. After the sandbox is snapshotted, installed dependencies are archived along with the workspace, so the next restore can use them directly (Branch A/B/C) without reinstalling.

7.4 State Persistence of Shell Commands and Scripts

ShellExecuteTool calls AbstractSandboxFilesystem.execute(cmd, timeout)Sandbox.exec(cmd, timeout), executing commands in the sandbox. All filesystem changes from commands (new files, installed packages, written logs, etc.) are retained inside the sandbox’s overlay/container. When stop() runs, these states are persisted with the tar snapshot and restored on the next start(). Therefore, state is fully preserved across calls:

8. State: SandboxStateStore and Session

  • SandboxStateStore: abstracts persistence of “sandbox metadata (sessionId + snapshot reference) bound to an isolation key”. Easy to replace with a custom implementation; configured on SandboxFilesystemSpec#sandboxStateStore (defaults when not set).
  • Default SessionSandboxStateStore: depends on the Session selected at build time (the session abstraction shared with SessionPersistenceHook etc.; if you use a distributed Session like Redis, sandbox metadata becomes visible across processes).
  • WorkspaceSession remains responsible for per-session configuration under the workspace layout; do not conflate WorkspaceSession’s JSON with “sandbox state JSON” — the sandbox resume data source of truth is SandboxStateStore.

9. Concurrency Control: SandboxExecutionGuard

9.1 Concurrency Safety Boundaries

Different IsolationScopes have different concurrency safety guarantees: For SESSION scope, single-instance checkRunning=true (default) is sufficient; USER, AGENT, and GLOBAL in multi-replica deployments should all explicitly configure SandboxExecutionGuard to serialize access to shared slots.

9.2 SandboxExecutionGuard Interface

Lifecycle: the guard intervenes before acquire; lease.close() is automatically called by Harness after release() completes, covering the entire call window:
Priority 1 (externalSandbox) and Priority 2 (externalSandboxState) bypass the guard — user-managed sandboxes handle concurrency control on the caller side.

9.3 Built-in Redis Implementation

RedisSandboxExecutionGuard uses Redis SET NX PX leases for distributed mutual exclusion, sharing the same UnifiedJedis instance as RedisSnapshotSpec without introducing extra dependencies. It encodes IsolationScope into the lock key, so USER, AGENT, and GLOBAL each land on their own independent distributed lock:
Redis key format: <keyPrefix><scope_lower>:<value>, for example:
  • myapp:sandbox:lock:user:alice
  • myapp:sandbox:lock:agent:shared-agent
  • myapp:sandbox:lock:global:__global__
If you change isolationScope to USER, the same guard should be reused; the lock key is then bucketed by userId, protecting the shared sandbox state slot for the same user across multiple replicas. TTL note: TTL is a safety valve, not a correctness guarantee. If a call exceeds the TTL, Redis automatically releases the lock so the next waiter can enter — this prevents permanent deadlock from process crashes, but cannot guarantee the state safety of the timed-out call itself. Set leaseTtl to a reasonable upper bound of actual call duration (including LLM latency and retries).

9.4 Custom Implementation Reference

SandboxExecutionGuard is a @FunctionalInterface; any lock backend can be connected:
Implementation contract:
  1. tryEnter blocks until execution rights are obtained or the thread is interrupted (throws InterruptedException)
  2. lease.close() must be idempotent and must not throw exceptions (failures can only be internally logged)
  3. Implementation must be thread-safe

10. Distributed Operation and sandboxDistributed

When multi-replica or stateless workers need to share the sandbox recovery capability of the same logical session, you need:
  • A distributed Session (e.g., RedisSession), not just the default WorkspaceSession file store; and
  • A non-no-op SandboxSnapshotSpec (archives the workspace for re-fetching), which passes the “must be distributed” validation.
HarnessAgent.Builder#sandboxDistributed(SandboxDistributedOptions) can uniformly configure:
  • Override snapshotSpec (if provided); IsolationScope is only configured on SandboxFilesystemSpec, not repeated here;
  • Explicitly specify the Session used for the sandbox (if different from the main session) in the options;
  • Use SandboxDistributedOptions#oss / #redis etc. helper constructors for common combinations (see class JavaDoc).
If requireDistributed is true but the current effectiveSession is still WorkspaceSession or the snapshot is no-op, the build fails fast.

11. Choosing Between the Three Filesystem Modes

Sandbox is one of three declarative configuration options. For a full comparison see Filesystem; here are the key decision points:

12. Subagents

When SubagentsHook is enabled, if the parent agent is built in sandbox mode, subagents’ filesystem reuses the same SandboxBackedFilesystem session-binding strategy (per current implementation, facilitating environment sharing within the same orchestration tree). Subagents remain independent ReActAgent instances; the isolation boundary is consistent with the parent agent’s sandbox spec.
  • Filesystem — class hierarchy, three modes, abstractFilesystem escape hatch
  • ToolFilesystemTool, ShellExecuteTool parameters
  • SessionSession and WorkspaceSession
  • Architecture — hook collaboration and lifecycle