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/ShellExecuteToolinterfaces, 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.SandboxManagerpersists sandbox-side state after eachcallends (viaSandboxStateStore), and looks it up byIsolationScope+sessionId/userIdkeys on the nextacquire. - Relationship to the Harness workspace: the host still has a
WorkspaceManagerroot directory; what the sandbox sees is defined byWorkspaceSpecand 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:
- Call
SandboxFilesystemSpec#toSandboxContext(hostWorkspaceRoot)to get aSandboxContext(containingSandboxClient, isolation scope, snapshot spec,WorkspaceSpec, etc.), while packing host-side paths to project into the sandbox (AGENTS.md,skills/,subagents/,knowledge/) into aWorkspaceProjectionEntry(see §7 Workspace Projection). - Use
SandboxBackedFilesystemas the agent’sAbstractFilesystemimplementation (transparent to upper layers). - Construct
SandboxManager(client, stateStore, agentId); when no explicitSandboxFilesystemSpec#sandboxStateStoreis configured, defaults toSessionSandboxStateStore(effectiveSession, agentId), tying sandbox metadata to the currentSession. - Register
SandboxLifecycleHook(sandboxManager, filesystemProxy)(priority50): on everyPreCallit acquires →start()(including the 4-branch workspace initialization, see §6 Snapshot and 4-Branch Recovery); onPostCall/Erroritstop()(persist snapshot) → persist state → release and clears the active session on the proxy.
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.3.2 USER — User-level Sharing (Recommended for Distributed Memory)
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:
SandboxManager.acquire’s 4-level priority):
4.2 Passing Serialized State (Precise Snapshot Recovery)
If you already have aSandboxState 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 viaHarnessAgent.Builder#filesystem(SandboxFilesystemSpec). Overall assembly still follows §2: SandboxContext → SandboxBackedFilesystem → SandboxManager → SandboxLifecycleHook.
5.1 Extension Points Overview
5.2 Implementing SandboxClient and Options
- Define
MySandboxClientOptions extends SandboxClientOptions:getType()returns a stable string (e.g.acme), consistent with thetypein persistence/config. If you need to deserialize options from YAML/JSON, add Jackson registration forSandboxClientOptionspolymorphism (see framework’sDockerSandboxClientOptionsand the base class’s@JsonTypeInfo); skip this if configuring from Java code only. - Implement
SandboxClient<MySandboxClientOptions>: increateconstruct aSandboxthat has not yet beenstart()ed;serializeState/deserializeStatemust be consistent with theSandboxStatesubclass fields;deletecan be a no-op if there are no additional resources. - If
deserializeStateusesobjectMapper.readValue(json, SandboxState.class), thatObjectMappermust registerHarnessSandboxJacksonModule(which includes built-inNamedTypefordockeretc.) and yourNamedType(MySandboxState.class, "acme"). No-argnew DockerSandboxClient()automatically registersHarnessSandboxJacksonModule;new DockerSandboxClient(customMapper)requires manuallyregisterModule(new HarnessSandboxJacksonModule())andregisterSubtypes, 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 modifyingSandboxState.java. - This repo: in
io.agentscope.harness.agent.sandbox.json.HarnessSandboxJacksonModule, addregisterSubtypes(new NamedType(XxxSandboxState.class, "xxx"))for new official stores. - Application-private subclasses: call
mapper.registerSubtypes(new NamedType(MySandboxState.class, "acme"))on anyObjectMapperthat holds sandbox JSON, and ensureSandboxManager/SandboxStateStorepaths use the same mapper configuration as theSandboxClient.
5.4 Implementing SandboxFilesystemSpec
createClient(): return yourSandboxClient(or create it viaoptions.createClient(), same pattern asDockerFilesystemSpec).clientOptions(): return the mutable configuration object.snapshotSpec()/workspaceSpec(): can provide defaultNoopSnapshotSpecandnew WorkspaceSpec(); callers can still override withSandboxFilesystemSpec#snapshotSpec(...)before building the agent.
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 toHarnessAgent.builder():
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 bySandboxClient#deserializeState. -
SandboxClientOptions#getType()and thetypeinSandboxStateJSON do not conflict globally. - All **
ObjectMapper**s that doreadValue(..., SandboxState.class)have the correspondingNamedTyperegistered (includingHarnessSandboxJacksonModuleif Docker-compatible data must be read). - If using
AbstractSandboxFilesystem,ShellExecuteTooland file APIs are routed to yourSandbox#execand 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”:
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
WhenSandboxFilesystemSpec builds SandboxContext, it defaults to packing the following host paths into the projection:
SandboxFilesystemSpec#workspaceProjectionRoots(List<String>), or completely disable projection via workspaceProjectionEnabled(false).
7.2 How Projection Works
WorkspaceProjectionApplier runs at the end of Sandbox.start():
- Iterates all
WorkspaceProjectionEntrys, collects the host-side file set, sorts by path, and computes a SHA-256 content hash. - Packs these files into a tar, and decompresses them to the corresponding paths inside the sandbox workspace via
Sandbox.hydrateWorkspace(archive). - Stores the current hash in
SandboxState.workspaceProjectionHash; on the next startup, if the hash is unchanged, skips the projection (avoiding redundant transfers).
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’sSkillBox 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:
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 onSandboxFilesystemSpec#sandboxStateStore(defaults when not set).- Default
SessionSandboxStateStore: depends on theSessionselected at build time (the session abstraction shared withSessionPersistenceHooketc.; if you use a distributedSessionlike Redis, sandbox metadata becomes visible across processes). WorkspaceSessionremains responsible for per-session configuration under the workspace layout; do not conflateWorkspaceSession’s JSON with “sandbox state JSON” — the sandbox resume data source of truth isSandboxStateStore.
9. Concurrency Control: SandboxExecutionGuard
9.1 Concurrency Safety Boundaries
DifferentIsolationScopes 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
acquire; lease.close() is automatically called by Harness after release() completes, covering the entire call window:
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:
<keyPrefix><scope_lower>:<value>, for example:
myapp:sandbox:lock:user:alicemyapp:sandbox:lock:agent:shared-agentmyapp:sandbox:lock:global:__global__
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:
tryEnterblocks until execution rights are obtained or the thread is interrupted (throwsInterruptedException)lease.close()must be idempotent and must not throw exceptions (failures can only be internally logged)- 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 defaultWorkspaceSessionfile 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);IsolationScopeis only configured onSandboxFilesystemSpec, not repeated here; - Explicitly specify the
Sessionused for the sandbox (if different from the mainsession) in the options; - Use
SandboxDistributedOptions#oss/#redisetc. helper constructors for common combinations (see class JavaDoc).
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
WhenSubagentsHook 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.
13. Related Pages
- Filesystem — class hierarchy, three modes,
abstractFilesystemescape hatch - Tool —
FilesystemTool,ShellExecuteToolparameters - Session —
SessionandWorkspaceSession - Architecture — hook collaboration and lifecycle