【AgentScope】11-沙箱(Sandbox)详解

沙箱(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()(容器继续运行)
// - 你自己决定什么时候销毁

用户管理的使用场景

  1. 多个 Agent 共享同一个沙箱
  2. 需要在调用之间保持容器运行(性能优化)
  3. 外部系统控制容器生命周期

优先级规则(谁说了算):

复制代码
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() 时再解压。这有两个问题:

  1. 耗时:大工作区打包/解压很慢
  2. 占用存储:每次都保存一份完整快照

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: 如何调试沙箱问题?

  1. 检查 SandboxStateStore 中的状态是否正确持久化
  2. 检查快照是否成功上传到 OSS/Redis
  3. 查看 SandboxLifecycleHook 的日志,了解走的是哪个分支
  4. AgentRun 可通过 GetSandbox API 查看沙箱状态