> ## Documentation Index
> Fetch the complete documentation index at: https://java.agentscope.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Filesystem

> Three deployment modes: local + shell / shared store / sandbox; IsolationScope dimensions; multi-user isolation; how skills and tools behave in each mode

## Role

`HarnessAgent` abstracts the agent's view of the **workspace** away from "must be local disk" into a uniform interface. All file tools (`read_file` / `write_file` / `edit_file` / `grep_files` / `glob_files` / `list_files`) and the optional `execute` (shell) go through this abstraction.

The payoff: you can switch between three deployment modes **without changing agent code**:

* Local + shell — single process, local, trusted env;
* Shared store — multiple replicas / pods share the same long-term memory;
* Sandbox — files and commands run in an isolated container; the same workspace state is restored across calls.

## Three declarative modes

Pick one with `filesystem(...)` on `HarnessAgent.Builder` (no call = mode 3 by default):

| Mode | Config | Shell? | When to use |
| - | - | - | - |
| **1 · Shared store** | `filesystem(new RemoteFilesystemSpec(store))` | No | Multiple replicas share `MEMORY.md` / conversation logs / subtask records via KV; **no shell on the host** |
| **2 · Sandbox** | `filesystem(new DockerFilesystemSpec()...)`, or K8s / Daytona / E2B / AgentRun | Yes (inside sandbox) | Isolated execution, cross-call workspace recovery, optional snapshots + distributed |
| **3 · Local + shell** (default) | `filesystem(new LocalFilesystemSpec()...)` or **omit it** | Yes (host `sh -c`) | Single process / local / trusted env / scripts and tests |

> `filesystem(...)` is mutually exclusive with `abstractFilesystem(...)`; the latter is an escape hatch for fully self-managed filesystems and rarely needed.

***

### Mode 1: shared store (`RemoteFilesystemSpec`)

For "multi-replica, but the user's long-term memory must stay in sync". Pass a `BaseStore` implementation (Redis / JDBC / in-memory) and the framework automatically routes workspace files into the KV store by path prefix:

```java theme={null}
// minimal config (recommended: use DistributedStore for one-line setup)
DistributedStore store = RedisDistributedStore.fromJedis(jedis);

HarnessAgent agent = HarnessAgent.builder()
    .name("store-agent")
    .model(model)
    .workspace(workspace)
    .distributedStore(store)
    .filesystem(new RemoteFilesystemSpec()   // baseStore auto-injected from store
        .isolationScope(IsolationScope.USER))
    .build();
```

#### All configuration options

| Method | Description | Default |
| - | - | - |
| `isolationScope(IsolationScope)` | Namespace isolation dimension (see [IsolationScope](#isolationscope--bucketing-across-users-and-replicas) below) | `USER` |
| `anonymousUserId(String)` | Fallback identifier when `userId` is absent | `"_default"` |
| `addSharedPrefix(String)` | Route additional workspace-relative prefixes to the KV (e.g. `"prompts/"`, `"configs/"`) | none |
| `workspaceIndex(WorkspaceIndex)` | SQLite index to accelerate remote ls/glob/grep | none (falls back to full store scan) |

#### Built-in routing rules

The framework automatically routes the following paths to the shared KV, each in its own namespace segment to prevent key collisions:

| Path | KV namespace segment |
| - | - |
| `AGENTS.md`, `MEMORY.md`, `tools.json` | `root` |
| `memory/` | `memory` |
| `skills/` | `skills` |
| `subagents/` | `subagents` |
| `knowledge/` | `knowledge` |
| `agents/<agentId>/sessions/` | `sessions` |
| `agents/<agentId>/tasks/` | `tasks` |

Paths not in the table above fall through to a local `LocalFilesystem` (no shell).

#### Example: multi-replica customer-service agent

Three pods each running a `HarnessAgent`, sharing one Redis as the `BaseStore`:

```java theme={null}
DistributedStore store = RedisDistributedStore.fromJedis(
        new JedisPooled("redis://shared-redis:6379"));

HarnessAgent agent = HarnessAgent.builder()
    .name("customer-service")
    .model(model)
    .workspace(Paths.get("/opt/agent/workspace"))
    .distributedStore(store)                  // stateStore + baseStore in one call
    .filesystem(new RemoteFilesystemSpec()
        .isolationScope(IsolationScope.USER)      // one namespace per user
        .anonymousUserId("anonymous"))            // fallback for unauthenticated callers
    .build();
```

* Each pod's local `AGENTS.md` / `knowledge/` / `skills/` serve as read-only templates (git-synced);
* Runtime outputs (`MEMORY.md`, `memory/`, conversation logs) are stored in Redis automatically — any pod reads the latest state;
* Alice's memory lives under KV key `agents/customer-service/users/alice/memory/...`.

This mode **does not provide shell** — on purpose: for shell, use mode 2 (sandbox) or 3 (local).

#### Available `BaseStore` implementations

| Implementation | Description | |
| - | - | - |
| `RedisStore` | Jedis-based, for low-latency high-concurrency | `agentscope-extensions-redis` |
| `JdbcStore` | JDBC-based, for MySQL / PostgreSQL / H2 | `agentscope-extensions-mysql` |
| `InMemoryStore` | In-memory, for testing | `agentscope-harness` |

***

### Mode 2: sandbox (`SandboxFilesystemSpec` family)

For "code may run untrusted operations" or "isolate from the production host". Every file op and shell command goes to the sandbox; the host is untouched.

#### Docker sandbox

```java theme={null}
HarnessAgent agent = HarnessAgent.builder()
    .name("sandbox-agent")
    .model(model)
    .workspace(workspace)
    .filesystem(new DockerFilesystemSpec()
        .image("ubuntu:24.04")
        .isolationScope(IsolationScope.SESSION)
        .memorySizeBytes(512 * 1024 * 1024L)   // 512 MB memory limit
        .cpuCount(2L)
        .network("host")
        .exposedPorts(8080, 3000)
        .environment(Map.of("NODE_ENV", "development"))
        .snapshotSpec(new LocalSnapshotSpec("/data/snapshots")))
    .build();
```

`DockerFilesystemSpec` — all options:

| Method | Description | Default |
| - | - | - |
| `image(String)` | Docker image | required |
| `isolationScope(IsolationScope)` | Isolation dimension | `SESSION` |
| `memorySizeBytes(Long)` | Container memory limit | Docker default |
| `cpuCount(Long)` | CPU limit | Docker default |
| `network(String)` | Docker network | Docker default |
| `exposedPorts(int...)` | Exposed ports | none |
| `environment(Map)` | Container environment variables | none |
| `workspaceRoot(String)` | Workspace mount point inside the container | `/workspace` |
| `additionalRunArgs(String...)` | Extra `docker run` arguments | none |
| `snapshotSpec(SandboxSnapshotSpec)` | Snapshot strategy | `NoopSnapshotSpec` (no snapshots) |
| `workspaceSpec(WorkspaceSpec)` | Workspace mount rules | default |
| `executionGuard(SandboxExecutionGuard)` | Concurrency guard for AGENT / GLOBAL scope | none |
| `workspaceProjectionEnabled(boolean)` | Enable host → sandbox static asset projection | `true` |
| `workspaceProjectionRoots(List)` | Root paths included in projection | `AGENTS.md`, `skills`, `subagents`, `knowledge`, `.skills-cache` |

#### Kubernetes sandbox (agent-sandbox)

The Kubernetes store is fully based on [agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox): sandbox pods are managed by the agent-sandbox controller in your cluster, and image, resources, and PVCs are all declared cluster-side in a `SandboxTemplate` / `SandboxWarmPool` (not configured from Java). The Java side claims instances from the warm pool via `SandboxClaim`. Install the agent-sandbox controller and create the template and warm pool before use.

```java theme={null}
HarnessAgent agent = HarnessAgent.builder()
    .name("k8s-agent")
    .model(model)
    .workspace(workspace)
    .filesystem(new KubernetesFilesystemSpec()
        .namespace("agents")
        .warmPoolName("agent-pool")        // SandboxWarmPool name
        .isolationScope(IsolationScope.USER))
    .build();
```

Main `KubernetesFilesystemSpec` options:

| Method | Description | Default |
| - | - | - |
| `namespace(String)` | namespace of the SandboxClaim | `default` |
| `warmPoolName(String)` | `SandboxWarmPool` name | required |
| `workspaceRoot(String)` | workspace root inside the sandbox; **must be on the PVC mount declared in the template** | `/workspace` |
| `fileApiBaseDir(String)` | runtime file API base directory; must match `workspaceRoot`; blank falls back to base64-over-exec transfer | `/workspace` |
| `apiUrl(String)` | direct runtime API URL (takes precedence when set) | none |
| `gatewayName(String)` / `gatewayNamespace(String)` / `gatewayScheme(String)` | reach the sandbox through the Gateway API | none |
| `serverPort(int)` | runtime HTTP API port | `8888` |
| `kubernetesClient(KubernetesClient)` | custom fabric8 client | kubeconfig auto-loaded |
| `snapshotSpec(SandboxSnapshotSpec)` | snapshot strategy (see the sandbox page for the PVC trade-off) | `NoopSnapshotSpec` |

When neither `apiUrl` nor `gateway*` is set, a local tunnel via `kubectl port-forward` is used (good for development). The runtime image must satisfy the [runtime image contract](/v2/en/docs/harness/sandbox#runtime-image-contract); **workspace persistence depends on the PVC configured in the template** — see [Sandbox - Kubernetes state persistence](/v2/en/docs/harness/sandbox#kubernetes-state-persistence-pvc-is-the-first-layer).

#### E2B sandbox

```java theme={null}
HarnessAgent agent = HarnessAgent.builder()
    .name("e2b-agent")
    .model(model)
    .workspace(workspace)
    .filesystem(new E2bFilesystemSpec()
        .apiKey("${E2B_API_KEY}")
        .templateId("my-template")
        .sandboxTimeoutSeconds(300)
        .isolationScope(IsolationScope.SESSION))
    .build();
```

#### Daytona sandbox

```java theme={null}
HarnessAgent agent = HarnessAgent.builder()
    .name("daytona-agent")
    .model(model)
    .workspace(workspace)
    .filesystem(new DaytonaFilesystemSpec()
        .apiKey("${DAYTONA_API_KEY}")
        .controlPlaneBaseUrl("https://api.daytona.io")
        .image("python:3.12-slim")
        .cpu(2)
        .memory(4)        // GiB
        .disk(10)         // GiB
        .isolationScope(IsolationScope.USER))
    .build();
```

#### AgentRun sandbox (Alibaba Cloud)

```java theme={null}
HarnessAgent agent = HarnessAgent.builder()
    .name("agentrun-agent")
    .model(model)
    .workspace(workspace)
    .filesystem(new AgentRunFilesystemSpec()
        .apiKey("${AGENTRUN_API_KEY}")
        .accountId("your-account-id")
        .region("cn-hangzhou")
        .templateName("python3.12")
        .sandboxIdleTimeoutSeconds(600)
        .isolationScope(IsolationScope.USER))
    .build();
```

#### Common options inherited from `SandboxFilesystemSpec`

| Method | Description | Default |
| - | - | - |
| `isolationScope(IsolationScope)` | Isolation dimension | store-specific (usually `SESSION`) |
| `snapshotSpec(SandboxSnapshotSpec)` | Snapshot strategy | `NoopSnapshotSpec` |
| `executionGuard(SandboxExecutionGuard)` | Concurrency serialization guard for AGENT/GLOBAL scopes | none |
| `workspaceProjectionEnabled(boolean)` | Project static assets from host to sandbox | `true` |
| `workspaceProjectionRoots(List)` | Root paths to include in projection | `AGENTS.md`, `skills`, `subagents`, `knowledge`, `.skills-cache` |

#### Snapshot strategies

Snapshots let the next `call()` restore the previous sandbox state (installed deps, generated files, etc.):

| Implementation | Description |
| - | - |
| `NoopSnapshotSpec` | No snapshots (default) |
| `LocalSnapshotSpec(Path)` | Snapshots stored on host local disk |
| `RedisSnapshotSpec` | Snapshots stored in Redis |
| `OssSnapshotSpec` | Snapshots stored in object storage (Alibaba Cloud OSS) |
| `RemoteSnapshotSpec` | Snapshots stored in a `BaseStore` |

#### Example: coding assistant (Docker + local snapshots)

```java theme={null}
HarnessAgent codingAgent = HarnessAgent.builder()
    .name("coder")
    .model(model)
    .workspace(Paths.get(".agentscope/workspace"))
    .filesystem(new DockerFilesystemSpec()
        .image("node:20-slim")
        .isolationScope(IsolationScope.USER)
        .memorySizeBytes(1024 * 1024 * 1024L)
        .snapshotSpec(new LocalSnapshotSpec("/data/sandbox-snapshots")))
    .distributedStore(store)
    .build();

// Alice's first call: npm install inside sandbox, snapshot saved afterward
RuntimeContext rc = RuntimeContext.builder()
    .userId("alice")
    .sessionId("dev-session-1")
    .build();
agent.call(Msg.user("npm install && npm test"), rc).block();

// Alice's second call: snapshot restored, node_modules still present
agent.call(Msg.user("npm run build"), rc).block();
```

#### Workspace projection

When a sandbox starts, the framework tars the workspace's "static assets" and hydrates them into `/workspace` inside the container. These include:

* `AGENTS.md` (persona file)
* `skills/` (skill directory)
* `subagents/` (subagent declarations)
* `knowledge/` (knowledge base)
* `.skills-cache/` (skill cache)

Projection compares content by SHA-256; unchanged files skip hydration. Customize which paths are included via `workspaceProjectionRoots(List)`, or disable entirely with `workspaceProjectionEnabled(false)`.

***

### Mode 3: local + shell (default)

What you get with no `filesystem(...)` call: workspace lives at `${cwd}/.agentscope/workspace/`, shell runs on the host:

```java theme={null}
HarnessAgent agent = HarnessAgent.builder()
    .name("local-agent")
    .model(model)
    .workspace(workspace)
    // .filesystem(...) omitted = local + shell
    .build();
```

#### All configuration options

```java theme={null}
.filesystem(new LocalFilesystemSpec()
    .executeTimeoutSeconds(120)       // shell command timeout
    .maxOutputBytes(100_000)          // max output bytes per command
    .env("MY_VAR", "value")          // extra environment variables
    .inheritEnv(true)                // inherit parent process env
    .mode(LocalFsMode.ROOTED)        // path policy
    .project(Paths.get("/my/project")) // project root (shell cwd + overlay lower)
    .addRoot(Paths.get("/extra/dir"))) // extra allowed directory
```

| Method | Description | Default |
| - | - | - |
| `executeTimeoutSeconds(int)` | Shell command timeout (seconds) | 120 |
| `maxOutputBytes(int)` | Max captured output bytes per command | 100,000 |
| `env(String, String)` | Add a shell environment variable | none |
| `inheritEnv(boolean)` | Inherit parent process environment | `false` |
| `mode(LocalFsMode)` | Path resolution policy | `ROOTED` |
| `project(Path)` | Project root directory (overlay lower layer + shell cwd) | `System.getProperty("user.dir")` |
| `addRoot(Path)` | Extra host directory the agent may access | none |
| `additionalRoots(Collection)` | Batch-set extra directories | none |
| `projectWritable(boolean)` | Route non-workspace writes to the project directory instead of workspace | `false` |

#### Path resolution policy (`LocalFsMode`)

| Mode | Behavior |
| - | - |
| `ROOTED` (default) | Absolute paths accepted only under `workspace` + `project` + `additionalRoots`; `..` traversal rejected |
| `SANDBOXED` | All paths anchored to the workspace root; absolute paths and `..` both rejected |
| `UNRESTRICTED` | Absolute paths pass through unchanged. Only for tests or fully trusted environments |

#### Overlay filesystem

Local mode actually produces an `OverlayFilesystem`:

* **Upper** (read-write): `LocalFilesystemWithShell`, rooted at `workspace`, provides shell;
* **Lower** (read-only): `LocalFilesystem`, rooted at `project`.

Reads check workspace first, then fall back to project (copy-on-write semantics). Shell `pwd` is the project directory, so `ls` shows project files.

#### Project-writable mode (`projectWritable`)

By default all writes land in the workspace — fine for read/analyze scenarios, but if the agent's job is to **generate code** (e.g. scaffold a microservice), files end up in `.agentscope/workspace/` instead of the project directory.

Enable `projectWritable(true)` and the framework routes writes by path:

| Path type | Written to | Examples |
| - | - | - |
| Workspace metadata | workspace | `MEMORY.md`, `memory/`, `agents/`, `skills/`, `knowledge/`, `plans/`, `subagents/`, `rules/`, `tools.json` |
| Everything else | project directory | `src/main/java/App.java`, `pom.xml`, `README.md`, `docker-compose.yml` |

```java theme={null}
.filesystem(new LocalFilesystemSpec()
    .projectWritable(true)      // code files go to the project directory
    .inheritEnv(true))
```

Read behavior is unchanged — workspace first, project fallback.

#### Example: local development assistant

```java theme={null}
HarnessAgent devHelper = HarnessAgent.builder()
    .name("dev-helper")
    .model(model)
    .workspace(Paths.get(".agentscope/workspace"))
    .filesystem(new LocalFilesystemSpec()
        .project(Paths.get("/Users/alice/my-project"))
        .addRoot(Paths.get("/Users/alice/.config"))
        .mode(LocalFsMode.ROOTED)
        .inheritEnv(true)
        .executeTimeoutSeconds(300))
    .build();
```

The agent can read/write files under `/Users/alice/my-project` and `/Users/alice/.config`, run shell commands with cwd at `/Users/alice/my-project`, but cannot access other host directories.

***

## IsolationScope — bucketing across users and replicas

Both mode 1 (shared store) and mode 2 (sandbox) use the same `IsolationScope` concept to decide **who shares state with whom**:

| Scope | Meaning | Namespace key | Typical use |
| - | - | - | - |
| `SESSION` | Each sessionId is independent | `agents/<agentId>/sessions/<sessionId>/...` | Multi-user SaaS, each conversation fully isolated |
| `USER` (default) | Same `userId` shares across sessions | `agents/<agentId>/users/<userId>/...` | Same user's multiple sessions share long-term memory |
| `AGENT` | All users/sessions of this agent share | `agents/<agentId>/shared/...` | Public-knowledge-base type agent |
| `GLOBAL` | One shared slot for everything | `global/...` | Use with care |

### Fallback rules per scope

* Under `USER` scope, if `RuntimeContext.userId` is absent, falls back to `SESSION` (isolates by sessionId).
* Under `SESSION` scope, if `RuntimeContext.sessionId` is absent, state lookup is skipped and a fresh environment is created.
* `AGENT` scope uses the agent name (fixed at build time) as the namespace key — it never degrades due to missing context fields.

### Concurrency in sandbox mode

`IsolationScope` in sandbox mode is **sequential-reuse** sharing, not live-instance sharing. Concurrent calls at the same scope key each get their own running container; at call end, the last-written snapshot wins. For `AGENT` / `GLOBAL` scopes where multiple users share state, use `executionGuard(SandboxExecutionGuard)` to serialize concurrent access.

### Example: scope combinations for different business needs

**Scenario 1: per-user coding sandbox, preserving installed deps across sessions**

```java theme={null}
.filesystem(new DockerFilesystemSpec()
    .image("python:3.12")
    .isolationScope(IsolationScope.USER)       // all of Alice's sessions share one snapshot
    .snapshotSpec(new LocalSnapshotSpec("/snapshots")))
```

**Scenario 2: per-conversation disposable sandbox**

```java theme={null}
.filesystem(new DockerFilesystemSpec()
    .image("ubuntu:24.04")
    .isolationScope(IsolationScope.SESSION))   // each sessionId independent
```

**Scenario 3: shared-knowledge customer-service agent (shared store)**

```java theme={null}
.distributedStore(store)
    .filesystem(new RemoteFilesystemSpec()
    .isolationScope(IsolationScope.AGENT))     // all users and sessions share memory / skills
```

***

## How multi-user isolation works

`RuntimeContext.userId` is the key to multi-user splitting:

| Mode | What userId does | Physical manifestation |
| - | - | - |
| Local | User-level files land in `workspace/<userId>/...`, e.g. `workspace/alice/skills/code-reviewer/SKILL.md` only applies to Alice | path prefix |
| Shared store | Used as KV namespace prefix `agents/<agentId>/users/<userId>/...` | KV key prefix |
| Sandbox | Used as sandbox snapshot slot key (paired with `IsolationScope.USER`) | sandbox instance isolation |

Without `userId`, single-tenant default applies and everyone shares one root.

### Runtime data vs static assets

**Runtime data** (conversation logs, tasks, memory) follows `IsolationScope` / `userId` and is automatically isolated.

**Static assets** (`AGENTS.md`, `tools.json`, `knowledge/`) are shared across all users and are **not** auto-partitioned by userId. Differentiation is only possible through per-user override directories:

```
workspace/
├── skills/code-reviewer/SKILL.md     ← shared (visible to everyone)
└── alice/
    └── skills/code-reviewer/SKILL.md ← only applies to Alice; overrides shared
```

***

## How skills and tools behave in each mode

### Skills

`DynamicSkillMiddleware` merges skills from the repository list before each reasoning turn and renders them into the system prompt. Skill file loading goes through the `AbstractFilesystem` interface, so it works transparently across all three modes:

| Mode | How skills load |
| - | - |
| Local | Read directly from `workspace/skills/` on local disk; `<userId>/skills/` for per-user overrides |
| Shared store | `skills/` routes to KV — checks remote first, falls back to local template. Admin edits take effect on the next reasoning turn across all replicas |
| Sandbox | Host `skills/` are injected into the sandbox's `/workspace/skills/` via workspace projection at startup |

The four-layer priority is unchanged (low → high): `projectGlobalSkillsDir` → `skillRepository` → `workspace/skills/` → `<userId>/skills/`.

### File tools (read\_file / write\_file / edit\_file / ...)

All file tools call through the `AbstractFilesystem` interface, passing the current `RuntimeContext` on every operation. The filesystem implementation decides the actual read/write location. Agent code is completely unaware of the mode.

| Mode | Read/write behavior |
| - | - |
| Local | `OverlayFilesystem`: writes land in workspace (upper); reads check workspace first, then project (lower). With `projectWritable(true)`, non-metadata writes are routed to the project directory |
| Shared store | `CompositeFilesystem`: routed paths go through KV overlay (remote upper + local template lower); others go local |
| Sandbox | All file operations forwarded into the sandbox container |

### Shell execution (execute)

| Mode | Shell available? | Where it runs |
| - | - | - |
| Local | Yes | Host `sh -c`, cwd = `project` directory |
| Shared store | No | Shell not provided |
| Sandbox | Yes | Inside the sandbox container |

### tools.json / MCP servers

`tools.json` is read once from the workspace at `build()` time (through `WorkspaceManager`, supporting two-layer reads), registering MCP servers and applying allow/deny filters. **Behavior is the same across all three modes** — configuration is read at build time, unaffected by the runtime filesystem mode.

Under shared-store mode, `tools.json` also follows the "remote upper, local template lower" overlay: modifying `tools.json` via an admin console requires **re-building the agent to take effect** (MCP server registration is a one-time operation).

***

## Two-layer reading in the workspace

Key files like `AGENTS.md`, `MEMORY.md`, `KNOWLEDGE.md` have a "two-layer fallback" on reads: look in your configured filesystem first, fall back to local disk if not found. This is useful for **"template files" in mode 1 (shared store)**: the first replica's local has the template `AGENTS.md` so it works immediately; later replicas read the up-to-date version from the shared store.

Writes always go through the configured filesystem store.

## Fully self-managed: `abstractFilesystem(...)`

If none of the three modes fits, pass a fully self-implemented filesystem:

```java theme={null}
HarnessAgent.builder()
    ...
    .abstractFilesystem(myCustomFilesystem)   // mutually exclusive with filesystem(...)
    .build();
```

Usually not needed — the three modes cover \~95% of use cases.

## Related Pages

* [Sandbox](/v2/en/docs/harness/sandbox) — runtime details of mode 2 (container lifecycle, snapshot recovery chain)
* [Workspace](/v2/en/docs/harness/workspace) — directory layout, loading mechanics, the "lower layer" of two-layer reads
* [Context](/v2/en/docs/building-blocks/context) — `AgentState` and `AgentStateStore`, `(userId, sessionId)` addressing
* [Skills](/v2/en/docs/harness/skill) — four-layer composition, self-learning loop, the `<available_skills>` block
* [Tools](/v2/en/docs/building-blocks/tool) — `read_file` / `write_file` / `execute` parameters
* [Architecture](/v2/en/docs/harness/architecture) — how filesystem and runtime context cooperate


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.