文件系统(Filesystem)详解
版本基准 :本文档基于 AgentScope 2.0 GA(
v2.0.0)编写。具体版本号以 Release Notes 为准。
一句话概括
文件系统是 Agent 的"住处选择器"------它决定了 Agent 的文件存在哪里、命令在哪里跑:是住快捷酒店(共享存储,只住不干活)、租公寓(沙箱,独立空间随意折腾)、还是自己家(本机,自由但没隔离)。
你能学到什么
- 为什么需要"抽象文件系统"?直接用 Java 的 File API 不行吗?
- 三种声明式模式的区别:RemoteFilesystemSpec(共享存储,无 shell)、DockerFilesystemSpec 等 SandboxFilesystemSpec(沙箱)、LocalFilesystemSpec(本机+shell,默认)
- IsolationScope 四种隔离作用域(SESSION / USER / AGENT / GLOBAL)怎么选,各自的降级规则
- 两层读取架构(filesystem-first + local fallback)的设计意图
- LocalFsMode 三种路径解析策略(ROOTED / SANDBOXED / UNRESTRICTED)与 GA 的 ROOTED 前导
/修复 abstractFilesystem(...)自定义文件系统的逃生口- 如何根据你的场景选择正确的文件系统配置
前置知识
详见 README.md 前置知识部分。本篇额外需要:
- 工作区布局 :了解
AGENTS.md、MEMORY.md等文件的目录结构,详见 02-workspace- RuntimeContext :了解
sessionId、userId的含义,详见 01-overview
核心概念
为什么需要抽象文件系统 ------ 就像"换办公室不用改工作习惯"
HarnessAgent 把 Agent 对工作区 的访问从"一定是本机磁盘"抽象成统一接口(AbstractFilesystem)。所有文件工具(read_file / write_file / edit_file / grep_files / glob_files / list_files)和可选的 execute(shell)都从这个抽象走。
这样做让你能在三种部署模式之间切换,而不改 Agent 代码:
- 本机 + shell ------ 单进程、本地、信任环境;
- 共享存储 ------ 多副本 / 多 pod 共享同一份长期记忆;
- 沙箱 ------ 文件与命令都在隔离容器里执行,跨调用恢复同一份工作区。
三种声明式模式 ------ 就像"住酒店 vs 租公寓 vs 自己家"
假设你刚到一个新城市工作,需要找个地方住。你有三种选择:
-
住快捷酒店(共享存储 / RemoteFilesystemSpec):你只是在这里睡觉,所有行李统一存在前台的大保险箱里。多个房客共用同一个保险箱(当然有编号区分),但你不能在房间里做饭、搞装修(没有 shell)。好处是换一家酒店,行李自动跟着走------你的东西存在云端,哪家分店都能取。
-
租一间公寓(沙箱 / DockerFilesystemSpec 等 SandboxFilesystemSpec):你有独立的空间,可以做饭、钉钉子、养宠物(有 shell)。就算你把厨房炸了,也只是自己这间的事,隔壁邻居完全不受影响。而且下出差回来,公寓会帮你保留原样------包括冰箱里没吃完的披萨。
-
住自己家(本机+shell / LocalFilesystemSpec):你就是房东,想干啥干啥。拆墙、挖地、装新电线都随你。但问题是:如果你不小心把水管弄爆了,整栋楼都得赔。而且你只能住这一套房子,没法"异地共享"。
技术对比表:
| 模式 | 配置方法 | 提供 Shell? | 适用场景 |
|---|---|---|---|
| 1 · 共享存储 | filesystem(new RemoteFilesystemSpec(store)) |
没有(故意设计) | 多副本共享记忆,不需要执行脚本 |
| 2 · 沙箱 | filesystem(new DockerFilesystemSpec()...) 或 K8s / Daytona / E2B / AgentRun |
有(沙箱内执行) | 隔离执行不可信代码、跨调用恢复状态 |
| 3 · 本机+shell(默认) | filesystem(new LocalFilesystemSpec()...) 或不配 |
有(宿主 sh -c) |
单机开发、测试、受信任环境 |
注意:filesystem(...) 和 abstractFilesystem(...) 互斥。后者是完全自管文件系统的逃生口,正常用法不需要。
下面逐一详解这三种模式。
模式一:共享存储(RemoteFilesystemSpec)------ "住酒店"
生活类比:你在快捷酒店开了一间房。房间本身没有厨房、没有工具箱------你只能睡觉和看书。但你有一个云保险箱(KV 存储),所有贵重物品都存在那里。你出差到了另一个城市的同品牌酒店,用同一个房卡(userId),保险箱里的东西一模一样。
技术细节 :传入一个 BaseStore(Redis / JDBC / 内存),框架按路径前缀把工作区文件路由到这个 KV 存储:
java
// 最小配置(推荐通过 DistributedStore 一键配置 stateStore + baseStore)
DistributedStore store = RedisDistributedStore.fromJedis(jedis);
HarnessAgent agent = HarnessAgent.builder()
.name("store")
.model(model)
.workspace(workspace)
.distributedStore(store) // stateStore + baseStore 一键配置
.filesystem(new RemoteFilesystemSpec() // baseStore 由 store 自动注入
.isolationScope(IsolationScope.USER)) // 按用户隔离
.build();
所有配置项:
| 方法 | 说明 | 默认值 |
|---|---|---|
isolationScope(IsolationScope) |
命名空间隔离维度 | USER |
anonymousUserId(String) |
userId 为空时的兜底标识 |
"_default" |
addSharedPrefix(String) |
额外路由到 KV 的工作区相对路径前缀(如 "prompts/" / "configs/") |
无 |
workspaceIndex(WorkspaceIndex) |
加速远端 ls/glob/grep 的 SQLite 索引 | 不加索引,走全量扫描 |
内置路由规则:框架自动把以下路径路由到共享 KV,每个路径段各自独立命名空间,互不污染:
| 路径 | KV 命名空间段 |
|---|---|
AGENTS.md、MEMORY.md、tools.json |
root |
memory/ |
memory |
skills/ |
skills |
subagents/ |
subagents |
knowledge/ |
knowledge |
agents/<agentId>/sessions/ |
sessions |
agents/<agentId>/tasks/ |
tasks |
其余路径落到本地 LocalFilesystem(无 shell)。
BaseStore 可用实现:
| 实现 | 说明 | 模块 |
|---|---|---|
RedisStore |
基于 Jedis,适合低延迟高并发 | agentscope-extensions-redis |
JdbcStore |
基于 JDBC,适合 MySQL / PostgreSQL / H2 | agentscope-extensions-mysql |
InMemoryStore |
内存实现,适合测试 | agentscope-harness |
为什么故意不提供 Shell? 设计目标是跨节点一致的长记忆与日志。如果允许在宿主机执行 Shell,多个节点可能同时操作产生冲突。需要 Shell 能力请选模式 2 或模式 3。
适用场景:
- 生产环境,需要多副本共享数据
- Agent 不需要执行 Shell 命令
- 需要按用户/会话隔离数据(多租户)
模式二:沙箱(SandboxFilesystemSpec 系列)------ "租公寓"
生活类比:你租了一间独立公寓。公寓里有厨房、工具箱------想做什么实验都行。就算你把厨房弄得一团糟,也只影响自己这间房,隔壁邻居完全不知道。而且公寓有个神奇的功能:你出差前拍张照片(快照),回来后公寓会自动恢复成拍照时的样子,连冰箱里的披萨都在。
技术细节(以 Docker 为例):所有文件操作和 Shell 命令都在沙箱容器内执行,宿主完全不受影响:
java
// 模式 2:沙箱(Docker)
HarnessAgent agent = HarnessAgent.builder()
.name("sandbox")
.model(model)
.workspace(workspace)
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04") // 沙箱镜像
.isolationScope(IsolationScope.SESSION) // 按会话隔离
.memorySizeBytes(512 * 1024 * 1024L) // 512 MB 内存
.cpuCount(2L) // 2 核
.snapshotSpec(new LocalSnapshotSpec("/data/snapshots")))
.build();
DockerFilesystemSpec 所有配置项:
| 方法 | 说明 | 默认值 |
|---|---|---|
image(String) |
Docker 镜像 | 必填 |
isolationScope(IsolationScope) |
隔离维度 | SESSION |
memorySizeBytes(Long) |
容器内存限制 | Docker 默认 |
cpuCount(Long) |
CPU 限制 | Docker 默认 |
network(String) |
Docker network | Docker 默认 |
exposedPorts(int...) |
暴露端口 | 无 |
environment(Map) |
容器环境变量 | 无 |
workspaceRoot(String) |
容器内工作区挂载点 | /workspace |
additionalRunArgs(String...) |
额外的 docker run 参数 |
无 |
snapshotSpec(SandboxSnapshotSpec) |
快照策略 | NoopSnapshotSpec(不快照) |
workspaceSpec(WorkspaceSpec) |
工作区挂载规则 | 默认 |
executionGuard(SandboxExecutionGuard) |
AGENT/GLOBAL scope 下的并发串行化守卫 | 无 |
workspaceProjectionEnabled(boolean) |
是否从宿主投影静态资产到沙箱 | true |
workspaceProjectionRoots(List) |
投影的根路径列表 | AGENTS.md, skills, subagents, knowledge, .skills-cache |
工作原理:
- 所有文件操作和 Shell 命令都在沙箱容器内执行,宿主完全不受影响
- 沙箱有独立的生命周期管理:创建、快照、恢复、销毁
- 下次
call()时连同node_modules、pip install都能恢复回来 - 框架启动时把工作区的"静态资产"(
AGENTS.md、skills/、subagents/、knowledge/、.skills-cache/)打成 tar,hydrate 进沙箱;按内容 SHA-256 做增量比对,没变的跳过
可选沙箱后端:Docker / Kubernetes / E2B / Daytona / AgentRun(阿里云)。
快照策略:
| 实现 | 说明 |
|---|---|
NoopSnapshotSpec |
不快照(默认) |
LocalSnapshotSpec(Path) |
快照存宿主本地磁盘 |
RedisSnapshotSpec |
快照存 Redis |
OssSnapshotSpec |
快照存对象存储(阿里云 OSS) |
RemoteSnapshotSpec |
快照存 BaseStore |
适用场景:
- 需要执行不受信任的代码
- 需要强隔离的多租户环境
- 需要可恢复的执行状态(暂停后继续)
- 需要跨调用保留运行时环境
详细配置参见 07-sandbox.md。
模式三:本机+shell(LocalFilesystemSpec)------ "自己家"
生活类比 :你住在自己买的房子里。你就是房东,想怎么改就怎么改。要拆墙?行。要挖地?行。想装什么软件、跑什么脚本都可以------但如果你不小心 rm -rf /,整台机器就废了。而且只有这一套房子,没有"异地共享"这回事。
技术细节 :什么都不写就是这个;工作区落到 ${cwd}/.agentscope/workspace/,shell 在宿主上跑 sh -c:
java
// 模式 3:本机 + shell(什么都不写就是这个)
HarnessAgent agent = HarnessAgent.builder()
.name("local")
.model(model)
.workspace(workspace)
// .filesystem(...) 不写 = 本机 + shell
.build();
// 需要调整超时、环境变量、路径策略时
.filesystem(new LocalFilesystemSpec()
.executeTimeoutSeconds(120) // Shell 命令超时 120 秒
.maxOutputBytes(100_000) // 单条命令最大输出 100 KB
.env("MY_VAR", "value") // 注入环境变量
.inheritEnv(false) // 不继承宿主环境变量
.mode(LocalFsMode.ROOTED) // 路径解析策略
.project(Paths.get("/my/project")) // 项目根(shell 的 cwd + overlay 下层)
.addRoot(Paths.get("/extra/dir"))) // 额外可访问目录
LocalFilesystemSpec 所有配置项:
| 方法 | 说明 | 默认值 |
|---|---|---|
executeTimeoutSeconds(int) |
单条 shell 命令超时(秒) | 120 |
maxOutputBytes(int) |
单条命令最大捕获输出字节数 | 100,000 |
env(String, String) |
添加 shell 环境变量 | 无 |
inheritEnv(boolean) |
是否继承父进程环境 | false |
mode(LocalFsMode) |
路径解析策略 | ROOTED |
project(Path) |
项目根(overlay 下层 + shell cwd) | System.getProperty("user.dir") |
addRoot(Path) |
额外允许访问的宿主目录 | 无 |
additionalRoots(Collection) |
批量设置额外目录 | 无 |
projectWritable(boolean) |
非元数据写入落到项目目录而非 workspace | false |
Overlay 文件系统 :本机模式实际产出的是一个 OverlayFilesystem------
- 上层 (读写):
LocalFilesystemWithShell,根在workspace,提供 shell; - 下层 (只读):
LocalFilesystem,根在project。
读取时先看 workspace,没有再退到 project(copy-on-write 语义)。shell 的 pwd 是 project 目录,所以 Agent 执行 ls 看到的是项目文件。
项目可写模式(projectWritable) :默认所有写入都落到 workspace------这对阅读/分析类场景足够,但如果 Agent 的核心任务是生成代码 (如写一个微服务),文件会全写到 .agentscope/workspace/ 而不是项目目录。开启 projectWritable(true) 后,框架按路径自动路由:
| 路径类型 | 写入位置 | 示例 |
|---|---|---|
| 工作区元数据 | workspace | MEMORY.md、memory/、agents/、skills/、knowledge/、plans/、subagents/、tools.json |
| 其他所有文件 | 项目目录 | src/main/java/App.java、pom.xml、README.md |
读取行为不变------仍是 workspace 优先、project 兜底。
安全提醒:因为能直接在宿主机执行 Shell,如果 Agent 被诱导执行危险命令,后果很严重。生产环境请谨慎使用。
适用场景:
- 开发测试阶段,想快速跑起来
- 信任 Agent 执行的代码(不会恶意操作)
- 单机部署,不需要多机器共享数据
LocalFsMode 三种路径解析策略 ------ 就像"门禁卡的权限范围"
生活类比:小区门禁有三种规则。ROOTED(默认)= 卡只能开你自己家和小区公共区,去别人家直接被拦;SANDBOXED = 只能待在自己屋里,连走廊都不能去;UNRESTRICTED = 物业超级卡,哪都能进(危险,只给维修工用)。
| 模式 | 行为 |
|---|---|
ROOTED(默认) |
绝对路径只允许 workspace + project + additionalRoots 范围内;.. 穿越被拒绝 |
SANDBOXED |
所有路径强制锚定到 workspace 根,绝对路径和 .. 全部拒绝 |
UNRESTRICTED |
绝对路径原样透传,不做限制。仅用于测试或完全信任的环境 |
GA 的 ROOTED 前导 / 修复
早期版本的 ROOTED 对"带前导 / 的绝对路径"处理不够友好------如果路径不在 allow-list 内,直接抛 SecurityException,导致 Agent 写出的 /src/App.java 这类本意是工作区内文件的路径被拒。
GA 修复后的行为 (LocalFilesystem.resolveRooted):按以下顺序判定------
- 路径在 allow-list(
workspace/project/additionalRoots)内 → 直接放行 - 路径已存在但不 allow → 抛 SecurityException(防止窥探宿主其他文件)
- 路径以
/开头但不在 allow-list 内且不存在 → 剥掉前导/,锚定到 workspace 根 (workspace.resolve(stripped)),再校验..穿越
text
Agent 写 "/src/App.java"
│
▼
┌────────────────────────────────────────┐
│ /src/App.java 在 allow-list 内? │
│ 否(/src 不属于 project/workspace) │
└──────────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ /src/App.java 在宿主存在? │
│ 否 │
└──────────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ 剥掉前导 /,锚定到 workspace 根 │
│ 实际写入 workspace/src/App.java │
│ 再校验 .. 穿越 → 放行 │
└────────────────────────────────────────┘
这条修复让 ROOTED 模式既守住"不允许越界访问宿主真实文件"的安全底线,又对 Agent 习惯性写出的"伪绝对路径"保持兼容,不再频繁误拒。
IsolationScope ------ 就像"大厦的分租规则"
生活类比:想象一栋写字楼,里面有四层分租规则。最严格的是"每家公司一间办公室"(SESSION),最宽松的是"整栋楼共享一个大开间"(GLOBAL)。你的公司选哪种规则,取决于你有多少个部门、要不要共享办公设备。
模式 1(共享存储)和模式 2(沙箱)都共用同一个 IsolationScope,决定谁和谁共享同一份状态:
| 作用域 | 含义 | 命名空间键 | 典型场景 |
|---|---|---|---|
USER(默认) |
同一 userId 跨 session 共享 |
agents/<agentId>/users/<userId>/... |
同一用户的多个会话共享长期记忆 |
SESSION |
每个 sessionId 独立 | agents/<agentId>/sessions/<sessionId>/... |
多用户 SaaS,每段对话各跑各的 |
AGENT |
该 agent 的所有用户/会话共享 | agents/<agentId>/shared/... |
公共知识库型 agent |
GLOBAL |
全局共享一份 | global/... |
谨慎使用 |
各 scope 的降级规则(GA):
USERscope 下,如果RuntimeContext.userId为空 → 降级为SESSION(按 sessionId 隔离)SESSIONscope 下,如果sessionId为空 → 跳过状态查找,创建全新环境AGENTscope 的命名空间键由 agent name(build 时固定)决定,不会因缺少上下文字段而降级
沙箱模式下的并发行为 :IsolationScope 在沙箱模式下是顺序复用 的共享,不是实时实例共享。同一 scope key 的并发调用各自启动独立容器;每次调用结束时,最后写入的快照胜出。对 AGENT / GLOBAL 这种多用户共享 scope,如果需要串行化,使用 executionGuard(SandboxExecutionGuard) 做并发守卫。
实际应用 :要让 alice 在不同设备上的多个对话共享同一份长期记忆------选 USER,再把 userId="alice" 放进 RuntimeContext。
java
// 按用户隔离:alice 的所有会话共享同一份记忆
HarnessAgent agent = HarnessAgent.builder()
.name("store")
.model(model)
.workspace(workspace)
.filesystem(new RemoteFilesystemSpec(redisStore)
.isolationScope(IsolationScope.USER)) // 同一用户跨会话共享
.build();
// 调用时传入 userId
agent.call("帮我回忆一下上周的讨论",
RuntimeContext.builder()
.sessionId("sess-123")
.userId("alice") // alice 的命名空间
.build());
两层读取架构 ------ 就像"查本地书架,再查中央图书馆"
生活类比:想象你在一家连锁书店工作。顾客要一本书时,你先看看自己店里有没有(快,但库存有限)。如果店里没有,你再打电话去中央仓库调货(慢,但什么都有)。这就是"两层读取"------先查近的,没有再查远的。
技术解释 :对所有被注入到 prompt 的关键文件(AGENTS.md、MEMORY.md、knowledge/KNOWLEDGE.md、additionalContextFile),框架走两层读取:
1. 问当前配置的 AbstractFilesystem:有没有这个文件?
├── 有 → 返回该内容(filesystem 层,"覆盖"层)
└── 没有 → 走第 2 步
2. 读本地磁盘 workspace.resolve(path)
写入永远走第 1 层(filesystem 后端),从不直接落到本地磁盘。
这套设计的价值在共享存储模式最明显:
- 第一个副本启动时,本地磁盘上有团队 git 同步过来的
AGENTS.md模板,立刻可用 - 之后任何节点对
AGENTS.md的修改会写到共享 KV - 所有副本下一次
call()就读到最新版本 - 本地模板是 fallback(兜底),远端覆盖是事实
abstractFilesystem(...) ------ "自己盖房子"
生活类比 :前三种模式就像开发商盖好的精装房、公寓、酒店------拎包入住。但如果你是个建筑设计师,想要完全定制的房子?abstractFilesystem(...) 就是给你一块空地,你自己从地基开始盖。
java
// 完全自管文件系统(与 filesystem(...) 互斥)
HarnessAgent agent = HarnessAgent.builder()
.name("custom")
.model(model)
.workspace(workspace)
.abstractFilesystem(myCustomFilesystem) // 你自己实现的文件系统
.build();
通常不需要------三种声明式模式覆盖了绝大多数场景。只有当你需要对接特殊的存储后端(如 S3、HDFS、自研存储)时才用得到。
关键代码解读
1. 三种模式的配置与选择
java
// ===== 模式 1:共享存储(无 shell)=====
HarnessAgent agent1 = HarnessAgent.builder()
.name("store")
.model(model)
.workspace(workspace)
.distributedStore(store) // 一键配齐 stateStore + baseStore
.filesystem(new RemoteFilesystemSpec() // baseStore 由 store 自动注入
.isolationScope(IsolationScope.USER)) // 按用户隔离
.build();
// ===== 模式 2:沙箱(隔离执行)=====
HarnessAgent agent2 = HarnessAgent.builder()
.name("sandbox")
.model(model)
.workspace(workspace)
.filesystem(new DockerFilesystemSpec() // Docker 沙箱
.image("ubuntu:24.04") // 镜像
.isolationScope(IsolationScope.SESSION) // 按会话隔离
.snapshotSpec(new LocalSnapshotSpec("/snapshots")))
.build();
// ===== 模式 3:本机+shell(默认)=====
HarnessAgent agent3 = HarnessAgent.builder()
.name("local")
.model(model)
.workspace(workspace)
// .filesystem(...) 不写 = 默认本机+shell
.build();
逐行注释:
- 模式 1 :
RemoteFilesystemSpec+ KV 存储后端,运行时数据存到远端。注意:这个模式不注册 Shell 工具,Agent 无法执行命令 - 模式 2 :
DockerFilesystemSpec,Agent 在容器内操作。isolationScope决定沙箱实例的分配粒度 - 模式 3:什么都不配就是默认。最简单,但生产环境需谨慎
2. LocalFilesystemSpec 的参数调优
java
// 本机模式的详细配置
HarnessAgent agent = HarnessAgent.builder()
.name("local")
.model(model)
.workspace(workspace)
.filesystem(new LocalFilesystemSpec()
.project(Paths.get("/Users/alice/my-project")) // 项目根(shell cwd + overlay 下层)
.addRoot(Paths.get("/Users/alice/.config")) // 额外可访问目录
.mode(LocalFsMode.ROOTED) // 路径策略(默认)
.executeTimeoutSeconds(300) // Shell 命令最长跑 300 秒
.maxOutputBytes(200_000) // 单条命令输出上限
.env("MY_VAR", "value") // 给 Shell 环境注入变量
.inheritEnv(true)) // 继承宿主机的环境变量
.build();
逐行注释:
project:设定项目根,Agent 的 shellpwd落在这里;读取静态资产(AGENTS.md、knowledge/)从 project 兜底addRoot:把额外宿主目录加入 allow-list,让 ROOTED 模式允许 Agent 访问mode:ROOTED(默认,兼顾安全与兼容)、SANDBOXED(最严)、UNRESTRICTED(不限)executeTimeoutSeconds:防止 Shell 命令卡死,超时后自动终止进程maxOutputBytes:限制单条命令输出,防止日志爆炸env:按需注入环境变量,避免在宿主机上全局配置inheritEnv:设为false可以隔离宿主环境,防止敏感信息(如AWS_SECRET_KEY)泄露给 Agent
3. 代码生成型 Agent:开启 projectWritable
java
// Agent 的核心任务是写代码到项目目录
HarnessAgent coder = HarnessAgent.builder()
.name("coder")
.model(model)
.workspace(workspace)
.filesystem(new LocalFilesystemSpec()
.project(Paths.get("/Users/alice/my-project"))
.projectWritable(true) // 代码文件直接落到项目目录
.inheritEnv(true))
.build();
开启后,src/main/java/App.java、pom.xml 这类非元数据写入路由到 project 目录;MEMORY.md、plans/ 等工作区元数据仍落 workspace。读取仍是 workspace 优先、project 兜底。
4. 多用户隔离的路径映射
java
// IsolationScope 决定了"运行时数据"的物理落点
// 以下展示不同模式下,用户 alice 的 MEMORY.md 实际存储位置
// 本机模式:路径前缀
// 物理路径 = workspace/alice/MEMORY.md
// 共享存储模式:KV 命名空间
// Redis key = "agents/store/users/alice/MEMORY.md"
// 沙箱模式:沙箱状态 slot
// slot key = "alice-session-123"(配合 IsolationScope.USER)
要点:
RuntimeContext.userId是切多用户的钥匙- 不传
userId时走单租户默认,所有人共享一个根 - 静态资产 (
AGENTS.md、knowledge/)不按 userId 切,所有用户共享 - 运行时数据(sessions、tasks、memory)才会跟着 userId 走
整体流程图
三种模式的工作流程
┌─────────────────────────────────────────────────────────────────┐
│ Agent 调用文件操作 │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 选择哪种文件系统模式? │
└─────────────┬───────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 模式 1:共享 │ │ 模式 2:沙箱 │ │ 模式 3:本机 │
│ 存储 │ │ │ │ │
│ │ │ Docker 容器 │ │ 本地磁盘 │
│ 无 Shell │ │ 有 Shell │ │ 有 Shell │
│ │ │ 完全隔离 │ │ 宿主执行 │
└──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ │ │ │ │ │
│ MEMORY.md │ │ 沙箱容器内 │ │ workspace/ │
│ → Redis │ │ 独立文件系统 │ │ ├── MEMORY.md │
│ memory/ │ │ 独立进程空间 │ │ ├── memory/ │
│ → Redis │ │ 可快照/恢复 │ │ └── sessions/ │
│ sessions/ │ │ │ │ │
│ → Redis │ │ │ │ │
│ AGENTS.md │ │ │ │ │
│ → 本地 │ │ │ │ │
│ │ │ │ │ │
└──────────────┘ └──────────────────┘ └──────────────────┘
两层读取架构
┌─────────────────────────────────────────────────────────────────┐
│ WorkspaceManager.readWithOverride() │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 第 1 层:当前 Filesystem │
│ (远端 KV / 沙箱 / 本机) │
└─────────────┬───────────────┘
│
┌─────────┴─────────┐
│ │
找到了 没找到
│ │
▼ ▼
┌──────────┐ ┌─────────────────────┐
│ 返回内容 │ │ 第 2 层:本地磁盘 │
│ (覆盖层)│ │ workspace/<path> │
└──────────┘ │ (模板/兜底层) │
└──────────┬──────────┘
│
▼
┌──────────┐
│ 返回内容 │
│ 或空 │
└──────────┘
注意:写入永远走第 1 层,不直接落到本地磁盘
IsolationScope 四种作用域
┌────────────────────────────────────────────────────────────────┐
│ 数据隔离粒度(从严到宽) │
├────────────────────────────────────────────────────────────────┤
│ │
│ SESSION USER(默认) AGENT GLOBAL │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │sess-001 │ │ alice │ │ │ │ │ │
│ │ ──────── │ │├─sess-001│ │ 所有 │ │ 全局 │ │
│ │ 独立数据 │ │├─sess-002│ │ 用户 │ │ 共享 │ │
│ │ 其他会话 │ │└─sess-003│ │ 共享 │ │ 一份 │ │
│ │ 互不可见 │ │ 共享记忆 │ │ 数据 │ │ 数据 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 每个会话独立 同一用户多会话 同一 Agent 所有人共享 │
│ 互不干扰 共享长期记忆 所有用户共享 谨慎使用 │
│ │
│ 降级规则: │
│ · USER 缺 userId → 降级为 SESSION │
│ · SESSION 缺 sessionId → 跳过状态查找,全新环境 │
│ · AGENT 按 agent name 寻址,不降级 │
└────────────────────────────────────────────────────────────────┘
模块关系与学习顺序
| 模块 | 与本篇的关联 |
|---|---|
| 工作区 | 两层读取的"下层"来源、工作区目录布局 |
| 上下文 | AgentState 与 AgentStateStore、(userId, sessionId) 寻址 |
| 沙箱 | 沙箱模式的运行时细节,包括容器生命周期和快照恢复链路 |
| 技能 | 技能文件的三模式加载、四层合成优先级 |
学习要点
必须记住
- 三种模式对应三种部署形态:共享存储(多副本共享记忆,无 shell)、沙箱(隔离执行,有 shell)、本机+shell(默认,最简单但最不安全)
- IsolationScope 是多租户的钥匙 :
USER(默认,推荐生产)、SESSION(每段对话独立)、AGENT(公共知识库)、GLOBAL(慎用);USER缺 userId 时降级为SESSION - 两层读取是 fallback 机制:先查文件系统后端,没有再查本地磁盘。写入永远走文件系统后端
- 共享存储模式故意没有 Shell:设计目标是跨节点一致性。需要 Shell 请选沙箱或本机模式
- LocalFsMode 默认 ROOTED :GA 修复了前导
/路径的处理------不在 allow-list 且不存在的/xxx会剥掉/锚定到 workspace,不再误拒 filesystem(...)与abstractFilesystem(...)互斥:前者声明式选模式,后者完全自管。95% 的场景用前者就够了
容易混淆
-
RemoteFilesystemSpec vs DockerFilesystemSpec:
RemoteFilesystemSpec(共享存储):文件存到 KV 后端,没有 Shell,重点是"数据共享"DockerFilesystemSpec(沙箱):文件和命令都在容器里,有 Shell,重点是"执行隔离"
-
IsolationScope.SESSION vs IsolationScope.USER:
SESSION:每个对话独立,适合"一问一答"型服务USER:同一用户跨对话共享记忆,适合"长期助手"型服务
-
两层读取 vs 用户覆盖:
- 两层读取:同一个文件的 fallback 机制(远端有就用远端,没有用本地模板)
- 用户覆盖:不同用户的文件隔离(
alice/skills/覆盖skills/,这是目录优先级)
-
ROOTED vs SANDBOXED:
ROOTED:允许访问 allow-list 内的绝对路径,GA 对带前导/的伪绝对路径做锚定兼容SANDBOXED:所有路径强制锚定 workspace,绝对路径一律拒绝
-
projectWritable 关闭 vs 开启:
- 关闭(默认):所有写入落 workspace
- 开启:非元数据写入落 project 目录(适合代码生成型 Agent)
实践建议
- 开发阶段用本机模式,上线前切沙箱或共享存储:不要在生产环境用默认的本机模式跑不可信代码
- 多副本部署必须用共享存储或沙箱:否则每个副本各自为政,记忆和会话状态不共享
- 多用户场景选
IsolationScope.USER:这是最常用的生产配置,同一用户跨设备、跨对话共享记忆 - 代码生成型 Agent 开
projectWritable(true):让 Agent 写出的代码直接落到项目目录,而不是被堆进 workspace - 不要直接用
java.nio.Files读写工作区文件 :在沙箱或共享存储模式下会写到错误的位置。通过harnessAgent.getWorkspaceManager()操作
常见问题
Q:我选了共享存储模式,但 Agent 提示找不到 Shell 工具,怎么办?
A:这是正常的。共享存储模式故意不提供 Shell。如果你的 Agent 需要执行命令,请改用沙箱模式(DockerFilesystemSpec)或本机模式(LocalFilesystemSpec)。
Q:三种模式可以混合使用吗?比如文件存 Redis、命令跑 Docker?
A:不能。filesystem(...) 三选一。如果你需要更复杂的组合逻辑,用 abstractFilesystem(...) 完全自管。
Q:IsolationScope 影响哪些文件?
A:只影响运行时数据 (sessions、tasks、memory)。静态资产(AGENTS.md、knowledge/、共用 skills/)不按用户隔离。用户级目录(<userId>/skills/)可以覆盖共用版本,但这是目录优先级机制,不是 IsolationScope。
Q:两层读取在沙箱模式下也有用吗?
A:有。沙箱启动时框架会把本地模板 hydrate 进去。之后如果远端(沙箱内)有更新,以沙箱内为准。本地磁盘上的模板只是初始种子。
Q:Agent 写出 /src/App.java 这种带前导 / 的路径,在 ROOTED 模式下会被拒吗?
A:GA 之前可能会误拒;GA 之后不会------只要 /src 不是宿主真实存在的目录、且不在 allow-list 内,框架会剥掉前导 /,锚定到 workspace 根,最终写入 workspace/src/App.java。这既守住安全底线(不会越界访问宿主真实文件),又兼容了 Agent 的伪绝对路径习惯。