沙箱(Sandbox)详解
一句话概括
沙箱就是把 AI Agent 关在一个隔离的安全笼子里干活------它可以在笼子里随便折腾(装软件、跑命令、写文件),但不会影响到你的主机系统,而且笼子里的东西还能被"打包带走"下次继续用。
你能学到什么
- 为什么需要沙箱?Agent 直接在主机上跑不行吗?
- 四种隔离级别(SESSION/USER/AGENT/GLOBAL)的区别和使用场景
- 4-分支恢复机制:沙箱如何"记住"上次的状态
- 自管理 vs 用户管理沙箱:谁负责创建和销毁?
- 快照机制:如何把工作区打包保存
- AgentRun 后端:阿里云函数计算沙箱怎么用
前置知识
需了解 06-filesystem.md(AbstractFilesystem)和 04-session.md(Session 和 RuntimeContext)。
本篇特有:容器化基础------需基本了解 Docker 是什么(容器化技术),理解"镜像"、"容器"、"快照"等基本概念。
核心概念
沙箱是什么?------ 安全笼子的比喻
想象你养了一只聪明的鹦鹉(Agent),它能帮你干活:查资料、写代码、整理文件。但是:
- 问题 1:信任问题 ------ 鹦鹉可能会打翻花瓶、咬坏沙发、把厨房搞乱
- 问题 2:环境问题 ------ 鹦鹉有时候需要特定的工具(比如剪刀、胶水),你想让它自己拿,但不想让它动你的保险柜
- 问题 3:连续性问题 ------ 鹦鹉今天干了一半的活,明天怎么让它从原地继续?
沙箱就是给鹦鹉准备的一个独立"工作间":
┌─────────────────────────────────────────────────────────────┐
│ 你的家(宿主机) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 沙箱(安全工作间) │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │ │
│ │ │ 工作台 │ │ 工具箱 │ │ 储物柜 │ │ │
│ │ │ (文件) │ │ (命令) │ │ (已安装的依赖) │ │ │
│ │ └─────────┘ └─────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ Agent 在这里干活,随便折腾,不影响外面 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 你的保险柜、私人文件、生产环境 ------ 完全安全! │
└─────────────────────────────────────────────────────────────┘
沙箱解决的三大问题:
| 问题 | 沙箱的解决方案 | 生活类比 |
|---|---|---|
| 执行边界 | 文件和命令都在沙箱内部执行,碰不到宿主机 | 鹦鹉只能在工作间里活动 |
| 可恢复工作单元 | 每次对话结束把工作间"打包",下次打开继续用 | 拍照记录工作间的状态 |
| 环境一致性 | Skills、知识库等自动同步到沙箱里 | 给工作间配备标准工具 |
IsolationScope ------ 四种"租间方式"
沙箱怎么分配?是每个人独享,还是共享?这由 IsolationScope(隔离范围)决定。
生活类比:酒店房间
| 隔离级别 | 生活类比 | 说明 | 适用场景 |
|---|---|---|---|
| SESSION | 每次入住开新房间 | 每个对话一个独立沙箱 | 多用户 SaaS,对话完全隔离 |
| USER | 同一个人总住同一间房 | 同一用户的所有对话共享沙箱 | 用户级记忆、跨会话连续性 |
| AGENT | 公司包下一间房公用 | 同一个 Agent 的所有用户共享 | 公共知识库型 Agent |
| GLOBAL | 整个酒店只有一个大房间 | 所有人共享一个沙箱 | 极少使用,谨慎配置 |
SESSION ------ 每次对话独立房间(默认)
java
// 每次调用传入不同的 sessionId,各自有独立的沙箱
agent.call(msgs, RuntimeContext.builder()
.sessionId("user1-session1") // 沙箱 A
.build());
agent.call(msgs, RuntimeContext.builder()
.sessionId("user1-session2") // 沙箱 B(与 A 完全独立)
.build());
生活类比:你今天开了 302 房,明天开了 505 房,两个房间互不影响。302 房里放的行李,505 房看不到。
USER ------ 同一用户共享房间
java
HarnessAgent agent = HarnessAgent.builder()
.filesystem(new DockerFilesystemSpec()
.isolationScope(IsolationScope.USER)) // 关键配置
.build();
// 两次调用,不同的 sessionId,但相同的 userId
agent.call(msgs, RuntimeContext.builder()
.userId("alice") // ← 相同的 userId
.sessionId("session-1") // 不同的 sessionId
.build());
agent.call(msgs, RuntimeContext.builder()
.userId("alice") // ← 相同的 userId,会恢复到同一个沙箱
.sessionId("session-2")
.build());
生活类比:Alice 是 VIP 客户,酒店给她预留了专属房间。不管她今天来还是下周来,都住同一间房,上次留下的东西都还在。
分布式场景:多台服务器(Pod)可以服务同一个用户,因为沙箱状态存在共享存储(如 Redis),任何一台服务器都能恢复 Alice 的沙箱。
AGENT ------ Agent 级共享
java
HarnessAgent agent = HarnessAgent.builder()
.name("knowledge-agent") // Agent 名称决定了共享范围
.filesystem(new DockerFilesystemSpec()
.isolationScope(IsolationScope.AGENT))
.build();
生活类比:公司的"公共会议室",所有员工都能用,但只有一间。谁最后用完,会议室就保持谁走时的状态。
GLOBAL ------ 全局共享
所有 Agent、所有用户、所有对话共享一个沙箱。极少使用,除非你明确知道自己在做什么。
4-分支恢复机制 ------ 沙箱的"存档读档"
这是沙箱最精妙的设计:当 Agent 结束一次对话后,下次再调用时如何恢复工作环境?
生活类比:游戏存档
想象你在玩游戏,每次退出时会存档。下次进入时,系统要判断:
开始游戏
│
▼
存档文件存在吗?
╱ ╲
是 否
│ │
▼ ▼
游戏机还在运行? 从头开始
╱ ╲ (冷启动)
是 否
│ │
▼ ▼
继续玩 读档继续
(热启动) (恢复存档)
沙箱的 4 个分支:
| 分支 | 条件 | 行为 | 生活类比 |
|---|---|---|---|
| A | 容器还在 + 工作目录完好 | 只刷新临时文件(最快) | 游戏机开着,直接继续玩 |
| B | 容器还在 + 工作目录丢失 | 从快照恢复 + 刷新临时文件 | 游戏机开着,但存档丢了,读档 |
| C | 容器没了 + 快照可用 | 创建新容器 + 从快照恢复 | 游戏机关了,读档重新开始 |
| D | 容器没了 + 快照不可用 | 创建新容器 + 全量初始化(最慢) | 新游戏,从头开始 |
图示:
Sandbox.start() 被调用
│
▼
┌─────────────────────┐
│ workspaceRootReady? │
└──────────┬──────────┘
╱ ╲
true false
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 容器内目录还在?│ │ 快照可用? │
└───────┬───────┘ └───────┬───────┘
╱ ╲ ╱ ╲
是 否 是 否
│ │ │ │
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│BranchA│ │BranchB│ │BranchC│ │BranchD│
│ 热启动 │ │读快照 │ │读快照 │ │ 冷启动 │
│ 最快 │ │+临时文件│ │+全量 │ │ 全量 │
└───────┘ └───────┘ └───────┘ └───────┘
代码层面的逻辑(简化版):
java
// 在 AbstractBaseSandbox.start() 中
public void start() {
if (workspaceRootReady) {
// 工作区标记为"已准备好"
if (containerDirectoryExists()) {
// Branch A: 容器和目录都在,热启动
justRefreshEphemeralEntries();
} else {
// Branch B: 容器在但目录丢了,从快照恢复
restoreFromSnapshot();
refreshEphemeralEntries();
}
} else {
// 工作区未标记为"准备好"
if (snapshotAvailable()) {
// Branch C: 有快照,从快照恢复
restoreFromSnapshot();
refreshAllEntries();
} else {
// Branch D: 没快照,冷启动
initializeFromWorkspaceSpec();
}
}
}
快照(Snapshot)------ 工作区的"打包带走"
生活类比:搬家打包
每次你要搬走时,把所有东西装进箱子(tar 包),存到仓库(OSS/Redis)。下次搬家到新地方,从仓库取箱子,原样摆好。
沙箱工作区 快照存储
┌──────────────┐ ┌──────────────┐
│ /workspace │ │ OSS Bucket │
│ ├── src/ │ tar 打包 │ │
│ ├── lib/ │ ──────────▶ │ snapshots/ │
│ └── data/ │ │ session-1/│
└──────────────┘ │ session-2/│
└──────────────┘
快照存储后端:
| Spec | 存储位置 | 适用场景 | 生活类比 |
|---|---|---|---|
NoopSnapshotSpec |
不存储 | 临时任务,不需要恢复 | 一次性用品 |
LocalSnapshotSpec |
本地文件 | 单机长期运行 | 家里的储物间 |
OssSnapshotSpec |
阿里云 OSS/S3 | 多副本分布式 | 云仓库 |
RedisSnapshotSpec |
Redis | 低延迟、小工作区 | 快递柜 |
配置示例:
java
// 使用 OSS 作为快照存储
OssSnapshotSpec snapshotSpec = new OssSnapshotSpec(
ossClient, // OSS 客户端
"my-bucket", // Bucket 名称
"sandbox-snapshots/" // 存储路径前缀
);
DockerFilesystemSpec spec = new DockerFilesystemSpec()
.image("ubuntu:24.04")
.snapshotSpec(snapshotSpec); // 配置快照后端
自管理 vs 用户管理沙箱 ------ 谁来开关门?
生活类比:酒店房间 vs 自家房子
| 管理方式 | 生活类比 | 特点 |
|---|---|---|
| 自管理(默认) | 酒店服务:入住时前台开门,退房时前台锁门 | Harness 框架全权负责 |
| 用户管理 | 自家房子:你自己拿钥匙开门关门 | 你自己控制生命周期 |
自管理(Self-managed)------ 默认行为
java
// 默认情况下,Harness 框架自动管理沙箱生命周期
HarnessAgent agent = HarnessAgent.builder()
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04"))
.build();
// 框架自动:
// 1. 调用前:创建/恢复沙箱
// 2. 调用后:停止沙箱、持久化快照
// 3. 不再使用时:销毁沙箱
用户管理(User-managed)------ 自己控制
java
// 你自己创建并启动沙箱
Sandbox mySandbox = dockerClient.create(workspaceSpec, snapshotSpec, options);
mySandbox.start();
// 注入到调用中
SandboxContext callCtx = SandboxContext.builder()
.client(dockerClient)
.externalSandbox(mySandbox) // ← 告诉框架:这个沙箱我自己管
.build();
agent.call(msgs, RuntimeContext.builder()
.sandboxContext(callCtx)
.build());
// 调用结束后:
// - 框架会调用 mySandbox.stop()(持久化快照)
// - 但不会调用 mySandbox.shutdown()(容器继续运行)
// - 你自己决定什么时候销毁
用户管理的使用场景:
- 多个 Agent 共享同一个沙箱
- 需要在调用之间保持容器运行(性能优化)
- 外部系统控制容器生命周期
优先级规则(谁说了算):
Priority 1: RuntimeContext 里传了 externalSandbox → 用户管理,直接用
Priority 2: RuntimeContext 里传了 externalSandboxState → 从指定状态恢复
Priority 3: SandboxStateStore 里有持久化的状态 → 按隔离范围恢复
Priority 4: 以上都没有 → 创建新沙箱
工作区投影 ------ 把"标准工具"放进笼子
生活类比:工作间的标准配置
想象你给鹦鹉准备了一个工作间,但每次都希望里面有一些基础工具:手册、常用工具、参考资料。这些工具可能会更新(你买了新工具),但你不希望鹦鹉的工作间是空的。
宿主机工作区 沙箱工作区
┌─────────────────┐ ┌─────────────────┐
│ workspace/ │ │ /workspace/ │
│ ├── AGENTS.md │ 投影同步 │ ├── AGENTS.md │
│ ├── skills/ │ ──────────▶ │ ├── skills/ │
│ ├── subagents/ │ 每次启动 │ ├── subagents/ │
│ └── knowledge/ │ │ └── knowledge/ │
└─────────────────┘ └─────────────────┘
(宿主机) (沙箱内)
投影的内容:
| 目录/文件 | 用途 |
|---|---|
AGENTS.md |
Agent 的身份定义和指令 |
skills/ |
所有 Skill 的目录(包含 SKILL.md 和脚本) |
subagents/ |
子 Agent 的定义文件 |
knowledge/ |
领域知识文件 |
投影的工作原理:
java
// 在 Sandbox.start() 末尾执行
// 1. 收集宿主机上的文件,计算 SHA-256 哈希
// 2. 与上次投影的哈希比较,如果不同则重新同步
// 3. 通过 tar 包传输到沙箱内
关键特性:
- 单向同步:宿主机 → 沙箱(沙箱内修改不会影响宿主机)
- 增量更新:哈希不变则跳过(避免重复传输)
- 每次启动执行:保证沙箱内的文件是最新的
并发控制 ------ 多人同时用怎么办?
生活类比:会议室预约系统
当多个人想用同一个会议室(共享沙箱)时,需要排队:
用户 A 想用 → 检查会议室是否空闲 → 空闲?获得使用权 → 用完释放
↓ 不空闲
等待...
不同隔离级别的并发安全性:
| 隔离级别 | 并发安全性 | 说明 |
|---|---|---|
| SESSION | 天然安全 | 每个会话独立沙箱,互不干扰 |
| USER | 需要保护 | 同一用户的多个会话可能冲突 |
| AGENT | 需要保护 | 所有用户共享一个沙箱 |
| GLOBAL | 需要保护 | 全局共享,最容易冲突 |
SandboxExecutionGuard 接口:
java
@FunctionalInterface
public interface SandboxExecutionGuard {
// 获取执行权(可能阻塞等待)
SandboxLease tryEnter(SandboxIsolationKey key) throws InterruptedException;
}
使用 Redis 分布式锁:
java
// 配置 Redis 分布式锁
UnifiedJedis jedis = new JedisPooled("redis-host", 6379);
SandboxExecutionGuard guard = RedisSandboxExecutionGuard.builder(jedis)
.leaseTtl(Duration.ofMinutes(30)) // 锁的超时时间
.retryInterval(Duration.ofMillis(500)) // 轮询间隔
.keyPrefix("myapp:sandbox:lock:") // Redis key 前缀
.build();
HarnessAgent agent = HarnessAgent.builder()
.filesystem(new DockerFilesystemSpec()
.isolationScope(IsolationScope.USER) // USER 级别共享
.executionGuard(guard)) // 加锁保护
.build();
生命周期:
tryEnter(key) ← 可能阻塞等待上一个 call 完成
└─ acquire / resume sandbox
└─ sandbox.start()
└─ [agent 执行]
└─ sandbox.stop()
└─ 持久化状态
lease.close() ← 释放锁,下一个等待者可以进入
AgentRun 后端 ------ 阿里云函数计算沙箱
生活类比:租用云工作间
Docker 沙箱就像在自己家搭工作间(需要自己买服务器、装 Docker),而 AgentRun 就像在阿里云租了一个现成的工作间:
- 你不需要自己搭建基础设施
- 按使用时间付费
- 工作间有专业的安保和备份
AgentRun 是什么?
AgentRun 是阿里云函数计算 FC 3.0 的 Sandbox API,提供托管的沙箱服务。
什么时候选 AgentRun?
| 场景 | 推荐后端 |
|---|---|
| 已使用阿里云、业务在中国大陆、需要低延迟 | AgentRun |
| 需要实例级 NAS/OSS 挂载、文件自动持久 | AgentRun(NAS-first) |
| 本地开发、有 Docker | Docker |
| 自建 K8s 集群 | Kubernetes |
| 托管沙箱服务 | Daytona / E2B / AgentRun |
AgentRun 的优势:NAS-first 模式
传统沙箱需要在 stop() 时把工作区打成 tar 包存起来,下次 start() 时再解压。这有两个问题:
- 耗时:大工作区打包/解压很慢
- 占用存储:每次都保存一份完整快照
NAS 模式下,工作区直接落在 NAS 文件系统上,沙箱销毁后文件还在:
┌─────────────────────────────────────────────────────────┐
│ 阿里云 │
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ AgentRun │ │ NAS 文件系统 │ │
│ │ 沙箱实例 │ ◀─────▶ │ /workspace/ │ │
│ │ (计算资源) │ 挂载 │ ├── src/ │ │
│ └─────────────┘ │ └── data/ │ │
│ │ └──────────────────────┘ │
│ │ │ │
│ │ 销毁后 │ 文件持久保留 │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ 下一个沙箱 │ ◀─────▶ │ 同一个 NAS 卷 │ │
│ │ (新实例) │ │ 文件自动恢复 │ │
│ └─────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
NAS-first 配置示例:
java
// 配置 NAS 挂载
AgentRunNasMountConfig nas = new AgentRunNasMountConfig()
.setServerAddr("12345abc-xxx.cn-hangzhou.nas.aliyuncs.com")
.setMountDir("/mnt/nas") // 挂载点
.setRemotePath("/") // NAS 上的路径
.setEnableTLS(false);
// 配置沙箱
AgentRunFilesystemSpec spec = new AgentRunFilesystemSpec()
.setApiKey(System.getenv("AGENTRUN_API_KEY"))
.setAccountId(System.getenv("ALIYUN_ACCOUNT_ID"))
.setRegion("cn-hangzhou")
.setTemplateName("agentscope-default")
.setMcpServerUrl(System.getenv("AGENTRUN_MCP_URL"))
.setNasConfig(nas) // NAS 配置
.setWorkspaceRoot("/mnt/nas/workspace") // 工作区在 NAS 内
.setSandboxIdleTimeoutSeconds(1800); // 30分钟无活动销毁
AgentRun 的关键约束:
| 约束 | 说明 |
|---|---|
| 单实例最多 5 个 OSS 挂载点 | 超出会报错 |
| mountDir 必须以 /home/、/mnt/ 或 /data/ 开头 | AgentRun 平台限制 |
| OSS Bucket 必须是标准存储且同地域 | 归档/低频存储不支持 |
| 沙箱最小内存 512 MiB | 平台限制 |
| MCP URL 必填 | 需要从控制台获取 |
关键代码解读
示例 1:基本沙箱配置
java
// 创建一个使用 Docker 沙箱的 Agent
HarnessAgent agent = HarnessAgent.builder()
// 1. Agent 名称,用于日志和标识
.name("code-agent")
// 2. 模型配置
.model(model)
// 3. 文件系统配置 ------ 这里指定使用沙箱模式
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04") // Docker 镜像
.snapshotSpec(new OssSnapshotSpec( // 快照存储
ossClient,
"my-bucket",
"snapshots/"
))
.isolationScope(IsolationScope.SESSION)) // 隔离级别
// 4. 构建 Agent
.build();
// 调用 Agent
agent.call(messages, RuntimeContext.builder()
.sessionId("session-123") // 会话 ID
.build());
示例 2:USER 级别共享 + 分布式部署
java
// 配置 Redis 分布式锁和会话存储
UnifiedJedis jedis = new JedisPooled("redis-host", 6379);
// 创建 Agent,支持分布式部署
HarnessAgent agent = HarnessAgent.builder()
.name("assistant")
.model(model)
.filesystem(new DockerFilesystemSpec()
.image("ubuntu:24.04")
.snapshotSpec(new OssSnapshotSpec(ossClient, "bucket", "snapshots/"))
.isolationScope(IsolationScope.USER) // USER 级别共享
.executionGuard( // 分布式锁
RedisSandboxExecutionGuard.builder(jedis)
.leaseTtl(Duration.ofMinutes(30))
.build()))
.sandboxDistributed(SandboxDistributedOptions.builder()
.session(redisSession) // Redis 会话
.requireDistributed(true) // 要求分布式
.build())
.build();
// 调用时指定 userId
agent.call(messages, RuntimeContext.builder()
.userId("alice") // ← 相同 userId = 相同沙箱
.sessionId("session-1")
.build());
示例 3:自定义工作区投影
java
// 默认投影:AGENTS.md, skills/, subagents/, knowledge/
// 可以自定义或关闭
DockerFilesystemSpec spec = new DockerFilesystemSpec()
.image("ubuntu:24.04")
// 自定义投影的根路径
.workspaceProjectionRoots(List.of(
"AGENTS.md",
"skills/",
"templates/", // 新增:模板目录
"configs/" // 新增:配置目录
))
// 或者完全关闭投影
// .workspaceProjectionEnabled(false)
;
示例 4:沙箱生命周期 Hook 的执行流程
java
// SandboxLifecycleHook 在 HarnessAgent 构建时自动注册
// 优先级 50,在每次 call 的前后执行
// === PreCall 阶段 ===
// 1. SandboxManager.acquire() - 获取沙箱
// - 检查 externalSandbox(用户管理)
// - 检查 externalSandboxState(指定状态)
// - 查询 SandboxStateStore(持久化状态)
// - 都没有则创建新沙箱
// 2. Sandbox.start() - 启动沙箱
// - 执行 4-分支恢复逻辑
// - 应用 WorkspaceProjectionEntry
// 3. SandboxBackedFilesystem 绑定到当前沙箱
// === Agent 执行 ===
// ShellExecuteTool 的命令在沙箱内执行
// 文件操作在沙箱工作区内
// === PostCall / Error 阶段 ===
// 1. Sandbox.stop() - 停止沙箱
// - 持久化快照(如果配置了 SnapshotSpec)
// 2. 持久化 SandboxState 到 SandboxStateStore
// 3. SandboxManager.release() - 释放沙箱
// - 自管理:调用 shutdown() 销毁容器
// - 用户管理:不调用 shutdown()
示例 5:AgentRun 配置详解
java
// === 前置准备 ===
// 1. 在 AgentRun 控制台创建 Template
// 2. 激活 MCP 工具:process_exec_cmd, read_file, write_file
// 3. 准备凭据:API Key 和 Account ID
// 4. 创建 NAS 文件系统(推荐)
// === NAS-first 模式(推荐)===
AgentRunNasMountConfig nas = new AgentRunNasMountConfig()
.setServerAddr("xxx.cn-hangzhou.nas.aliyuncs.com") // NAS 地址
.setMountDir("/mnt/nas") // 沙箱内挂载点
.setRemotePath("/") // NAS 上的路径
.setEnableTLS(false); // 是否启用 TLS
AgentRunFilesystemSpec spec = new AgentRunFilesystemSpec()
// === 必填项 ===
.setApiKey(System.getenv("AGENTRUN_API_KEY")) // API Key
.setAccountId(System.getenv("ALIYUN_ACCOUNT_ID")) // 账号 ID
.setRegion("cn-hangzhou") // 地域
.setTemplateName("agentscope-default") // 模板名称
.setMcpServerUrl(System.getenv("AGENTRUN_MCP_URL")) // MCP URL
// === NAS 配置(核心)===
.setNasConfig(nas) // NAS 挂载
.setWorkspaceRoot("/mnt/nas/workspace") // 工作区在 NAS 内
// === 可选项 ===
.setSandboxIdleTimeoutSeconds(1800) // 空闲超时
.isolationScope(IsolationScope.SESSION); // 隔离级别
// NAS 模式下不需要 snapshotSpec,因为文件直接持久在 NAS 上
整体流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 沙箱模式完整流程 │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐
│ agent.call() │
└────────┬─────────┘
│
▼
┌───────────────────────────────────┐
│ SandboxLifecycleHook │
│ (PreCall) │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ SandboxManager.acquire() │
│ │
│ Priority 1: externalSandbox? │──────▶ 用户管理的沙箱
│ Priority 2: externalState? │──────▶ 从指定状态恢复
│ Priority 3: stateStore 有状态? │──────▶ 按隔离范围恢复
│ Priority 4: 创建新沙箱 │──────▶ 冷启动
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ Sandbox.start() │
│ │
│ ┌─────────────────────────┐ │
│ │ 4-分支恢复逻辑 │ │
│ │ │ │
│ │ A: 热启动(容器+目录都在)│ │
│ │ B: 读快照(容器在,目录丢)│ │
│ │ C: 读快照(容器没了) │ │
│ │ D: 冷启动(全量初始化) │ │
│ └─────────────────────────┘ │
│ │
│ 应用 WorkspaceProjectionEntry │
│ (同步 AGENTS.md, skills/ 等) │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ Agent 执行 │
│ │
│ ┌─────────────────────────────┐ │
│ │ ShellExecuteTool │ │
│ │ → 在沙箱内执行命令 │ │
│ │ │ │
│ │ FilesystemTool │ │
│ │ → 在沙箱内读写文件 │ │
│ └─────────────────────────────┘ │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ SandboxLifecycleHook │
│ (PostCall/Error) │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ Sandbox.stop() │
│ │
│ 1. sync(NAS 模式) │
│ 2. 打包工作区为 tar │
│ 3. 上传到快照后端(OSS/Redis) │
│ 4. 设置 workspaceRootReady=true │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ 持久化 SandboxState │
│ (存到 SandboxStateStore) │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ SandboxManager.release() │
│ │
│ 自管理:调用 shutdown() 销毁容器 │
│ 用户管理:不销毁,容器继续运行 │
└─────────────────┬─────────────────┘
│
▼
┌──────────────────┐
│ call() 返回 │
└──────────────────┘
学习要点
核心概念速记
| 概念 | 一句话记忆 | 生活类比 |
|---|---|---|
| 沙箱 | 隔离的安全执行环境 | 给鹦鹉准备的独立工作间 |
| IsolationScope | 沙箱怎么分配 | 酒店房间分配策略 |
| SESSION | 每次对话独立沙箱 | 每次入住开新房间 |
| USER | 同一用户共享沙箱 | VIP 客户专属房间 |
| 4-分支恢复 | 如何恢复工作环境 | 游戏存档读档 |
| 快照 | 工作区打包保存 | 搬家打包存仓库 |
| 自管理 vs 用户管理 | 谁控制容器生命周期 | 酒店服务 vs 自家房子 |
| 工作区投影 | 同步标准文件到沙箱 | 给工作间配标准工具 |
| SandboxExecutionGuard | 并发访问控制 | 会议室预约系统 |
| AgentRun | 阿里云托管沙箱 | 租用云工作间 |
| NAS-first | 工作区直接落在 NAS 上 | 工作间自带储物柜 |
关键决策点
问题 1: 需要沙箱吗?
├── 不需要隔离 → 用 LocalFilesystemSpec 或默认
└── 需要隔离 → 用 SandboxFilesystemSpec
问题 2: 隔离级别?
├── 每个对话独立 → SESSION(默认)
├── 同一用户共享 → USER
├── 同一 Agent 共享 → AGENT
└── 全局共享 → GLOBAL(谨慎使用)
问题 3: 快照存储?
├── 不需要恢复 → NoopSnapshotSpec
├── 单机 → LocalSnapshotSpec
├── 分布式 → OssSnapshotSpec 或 RedisSnapshotSpec
问题 4: 沙箱后端?
├── 本地开发 → Docker
├── 自建 K8s → Kubernetes
├── 托管服务 → Daytona / E2B / AgentRun
└── 阿里云用户 → AgentRun(NAS-first)
问题 5: 并发控制?
├── SESSION 级别 → 不需要(天然隔离)
└── USER/AGENT/GLOBAL → 需要 SandboxExecutionGuard
常见问题
Q: 沙箱模式和 Remote 模式有什么区别?
| 维度 | Sandbox 模式 | Remote 模式 |
|---|---|---|
| 执行位置 | 隔离容器内 | 宿主机或远端存储 |
| Shell 命令 | 在沙箱内执行 | 在宿主机执行 |
| 适用场景 | 不可信输入、需要隔离 | 共享记忆、轻量级 |
| 复杂度 | 较高 | 较低 |
Q: 什么时候用 USER 隔离?
当需要"同一用户跨会话连续性"时使用。例如:
- 用户今天问了一半的问题,明天继续问
- 需要记住用户的偏好和历史上下文
- 多台服务器需要访问同一个用户的沙箱状态
Q: NAS-first 为什么比 tar 快照快?
传统 tar 快照:
沙箱工作区 → 打包 tar → 上传 OSS → 下次下载 tar → 解压到沙箱
(大工作区可能需要几分钟)
NAS-first:
沙箱工作区 → 直接在 NAS 上 → 下次启动直接可用
(秒级恢复)
Q: 如何调试沙箱问题?
- 检查
SandboxStateStore中的状态是否正确持久化 - 检查快照是否成功上传到 OSS/Redis
- 查看
SandboxLifecycleHook的日志,了解走的是哪个分支 - AgentRun 可通过
GetSandboxAPI 查看沙箱状态