Filesystem¶
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 |
|
No |
Multiple replicas share |
2 · Sandbox |
|
Yes (inside sandbox) |
Isolated execution, cross-call workspace recovery, optional snapshots + distributed |
3 · Local + shell (default) |
|
Yes (host |
Single process / local / trusted env / scripts and tests |
filesystem(...)is mutually exclusive withabstractFilesystem(...); the latter is an escape hatch for fully self-managed filesystems and rarely needed.
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¶
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 |
|---|---|---|
|
Docker image |
required |
|
Isolation dimension |
|
|
Container memory limit |
Docker default |
|
CPU limit |
Docker default |
|
Docker network |
Docker default |
|
Exposed ports |
none |
|
Container environment variables |
none |
|
Workspace mount point inside the container |
|
|
Extra |
none |
|
Snapshot strategy |
|
|
Workspace mount rules |
default |
|
Concurrency guard for AGENT / GLOBAL scope |
none |
|
Enable host → sandbox static asset projection |
|
|
Root paths included in projection |
|
Kubernetes sandbox¶
HarnessAgent agent = HarnessAgent.builder()
.name("k8s-agent")
.model(model)
.workspace(workspace)
.filesystem(new KubernetesFilesystemSpec()
.image("node:20-slim")
.namespace("agents")
.serviceAccount("agent-runner")
.cpuRequest("500m")
.memoryRequest("256Mi")
.nodeSelector(Map.of("pool", "agent"))
.podLabels(Map.of("app", "agentscope"))
.isolationScope(IsolationScope.USER))
.build();
E2B sandbox¶
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¶
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)¶
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 |
|---|---|---|
|
Isolation dimension |
store-specific (usually |
|
Snapshot strategy |
|
|
Concurrency serialization guard for AGENT/GLOBAL scopes |
none |
|
Project static assets from host to sandbox |
|
|
Root paths to include in projection |
|
Snapshot strategies¶
Snapshots let the next call() restore the previous sandbox state (installed deps, generated files, etc.):
Implementation |
Description |
|---|---|
|
No snapshots (default) |
|
Snapshots stored on host local disk |
|
Snapshots stored in Redis |
|
Snapshots stored in object storage (Alibaba Cloud OSS) |
|
Snapshots stored in a |
Example: coding assistant (Docker + local snapshots)¶
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:
HarnessAgent agent = HarnessAgent.builder()
.name("local-agent")
.model(model)
.workspace(workspace)
// .filesystem(...) omitted = local + shell
.build();
All configuration options¶
.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 |
|---|---|---|
|
Shell command timeout (seconds) |
120 |
|
Max captured output bytes per command |
100,000 |
|
Add a shell environment variable |
none |
|
Inherit parent process environment |
|
|
Path resolution policy |
|
|
Project root directory (overlay lower layer + shell cwd) |
|
|
Extra host directory the agent may access |
none |
|
Batch-set extra directories |
none |
|
Route non-workspace writes to the project directory instead of workspace |
|
Path resolution policy (LocalFsMode)¶
Mode |
Behavior |
|---|---|
|
Absolute paths accepted only under |
|
All paths anchored to the workspace root; absolute paths and |
|
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 atworkspace, provides shell;Lower (read-only):
LocalFilesystem, rooted atproject.
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 |
|
Everything else |
project directory |
|
.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¶
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 |
|---|---|---|---|
|
Each sessionId is independent |
|
Multi-user SaaS, each conversation fully isolated |
|
Same |
|
Same user’s multiple sessions share long-term memory |
|
All users/sessions of this agent share |
|
Public-knowledge-base type agent |
|
One shared slot for everything |
|
Use with care |
Fallback rules per scope¶
Under
USERscope, ifRuntimeContext.userIdis absent, falls back toSESSION(isolates by sessionId).Under
SESSIONscope, ifRuntimeContext.sessionIdis absent, state lookup is skipped and a fresh environment is created.AGENTscope 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
.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
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04")
.isolationScope(IsolationScope.SESSION)) // each sessionId independent
Scenario 3: shared-knowledge customer-service agent (shared store)
.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 |
path prefix |
Shared store |
Used as KV namespace prefix |
KV key prefix |
Sandbox |
Used as sandbox snapshot slot key (paired with |
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 |
Shared store |
|
Sandbox |
Host |
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 |
|
Shared store |
|
Sandbox |
All file operations forwarded into the sandbox container |
Shell execution (execute)¶
Mode |
Shell available? |
Where it runs |
|---|---|---|
Local |
Yes |
Host |
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:
HarnessAgent.builder()
...
.abstractFilesystem(myCustomFilesystem) // mutually exclusive with filesystem(...)
.build();
Usually not needed — the three modes cover ~95% of use cases.