Skip to content

沙盒与文件系统机制

本页解释 Agent 的文件和命令如何进入动态沙盒,以及 UserWorkspace、Project Workdir、Skills、Viewer 和 provisioner 的关系。部署参数见沙盒配置与运维

一句话理解

Yuxi 让多个访问入口看到同一份持久文件,但给它们不同的访问能力:

  • Agent 通过带认证的 provisioner 代理访问动态 Sandbox;
  • Viewer、附件和 artifact API 直接访问 UserWorkspace 的持久文件;
  • 知识库通过知识库工具访问,不挂载为沙盒目录。

模型和产品接口只使用虚拟路径。宿主机路径、容器路径和对象存储 URL 不能混用;每个文件入口都在所属文件系统边界内校验路径、用户和权限。

运行链路

mermaid
flowchart LR
    Model["模型 / 文件工具 / shell"] --> Backend["Agent Sandbox backend"]
    Backend --> Provider["Sandbox provider\nidentity / cache / keepalive"]
    Provider --> Provisioner["sandbox-provisioner\ncreate / discover / proxy / delete"]
    Provisioner --> Runtime["Docker container 或 Kubernetes Pod"]
    Runtime --> UserData["UserWorkspace\n/home/gem/user-data rw"]
    Runtime --> Skills["Skill projection\n/home/gem/skills ro"]
    Viewer["Viewer / artifact / attachment API"] --> Workspace["Workspace + Workdir\n持久 no-follow 访问"]
    Workspace --> UserData

Graph 创建时,Agent backend 取得 uid、根运行 scope 和 workdir_path。实际沙盒惰性创建;API/worker 只持有 provisioner 代理地址,不直接访问动态容器或 NodePort。

Identity、Workdir 和生命周期

runtime_scope_id 是一次顶层执行树的沙盒分组键,当前使用根 Conversation 的 thread ID。根 Agent 和子 Agent 共享这个 scope,因此可以共享同一个运行时、/tmp、环境和文件挂载;子 Agent 的 child thread 只隔离 LangGraph checkpoint。

Conversation 通过 project_id 绑定 Project;Project 拥有这项绑定和 workdir_path,UserWorkspace 拥有该路径下的实际文件字节。workdir_path 是当前用户 UserWorkspace 下的合法相对 POSIX 路径,例如 projects/<uuid>,不能包含 ..、反斜杠或符号链接。linked Project 只能引用已经存在的目录,目标不存在时请求失败;managed Project 使用服务分配并物化的 projects/<uuid> 目录,目录创建失败时请求失败。Workdir 决定当前工作目录和 Viewer 文件范围,但不决定 sandbox identity,也不把同一用户的其他 Project 变成安全隔离边界。两个顶层 Conversation 即使绑定同一 Workdir,也会创建不同 runtime。

运行类型checkpointruntime scopeWorkdir
普通 Agent当前 thread根 thread当前 Project 的 Workdir
子 Agentchild thread根 thread继承根 Conversation
远程 Skill 安装临时 thread临时 thread无持久用户目录,inherit_env=False

uid + runtime_scope_id 派生稳定 sandbox_id。同一 runtime 存活期间不能改绑到另一个 Workdir。根执行树终态后,worker 清理 runtime,但保留 UserWorkspace 文件。

挂载和文件 Owner

虚拟路径内容Agent 权限
/home/gem/user-data当前用户的整个 UserWorkspace读写
/home/gem/skills当前用户已授权的共享/内置 Skill 投影只读
/home/gem/user-data/<workdir_path>当前 Project 的工作目录默认 cwd

uploads/outputs/ 在首次使用时创建。个人 Skill 位于 /home/gem/user-data/agents/skills/<slug>,不会复制到共享 projection。

同一用户的 Sandbox 能看到整个 UserWorkspace,所以 Project A 可以读取 Project B。系统提示词要求 Agent 未经用户明确要求不要在当前 Workdir 外写入,但这只是行为约束,不是安全隔离;真正的边界由用户挂载、Workdir ownership 查询和工具路径校验提供。

文件访问使用相对路径和 no-follow 原语,拒绝 ..、符号链接、特殊文件和跨用户根目录。普通运行服务以 1000:1000 访问数据;storage migrator 只在停机迁移中承担一次性 root 文件操作。

Docker 和 Kubernetes

Docker backend 为每个 runtime 创建独立 bridge 网络,不发布沙盒端口,也不加入应用 app-network。网络只连接 provisioner 和对应沙盒,因此沙盒不能互访,也不能直接访问 PostgreSQL、Redis、MinIO、Milvus 或 Neo4j。provisioner 复用实例前会检查 uid、Workdir、挂载和网络身份。

Kubernetes backend 创建 Pod 和 NodePort Service。Pod 从 User Data PVC 的 shared/<uid>/workspace 挂载 /home/gem/user-data,从 Skill PVC 的 skill-projections/<uid> 只读挂载 /home/gem/skills。Pod 默认不自动挂载 ServiceAccount token;具体安全性仍取决于 namespace、PVC、NetworkPolicy 和 ServiceAccount 配置。

memory backend 只记录 ID 到 URL 的映射,不创建隔离环境或准备持久目录,不能作为生产隔离承诺。

环境变量和信任边界

API/worker 使用 SANDBOX_PROVISIONER_TOKEN 调用 provisioner。动态沙盒会收到全局 sandbox.env 与当前用户 Agent 环境的合并值,用户值覆盖同名全局值;这些值都可能被沙盒内代码读取和外传。provisioner token、数据库凭据、对象存储管理凭据和云平台管理员密钥不能进入这两类环境。

远程 Skill 安装使用不继承环境变量的一次性 Sandbox,也不挂载持久用户根。Skill 文件是只读的,但其中脚本仍可能被执行;脚本如需写文件,应写入当前 Project Workdir 或 User Data。

失败、恢复和观察边界

现象能证明什么不能证明什么
provisioner /health 正常进程和 backend 已初始化某个 runtime 已创建、挂载或权限正确
create/discover 返回代理 URL找到 identity 匹配的实例Workdir 内容和 Viewer 权限正确
Viewer 读到文件API 完成 ownership 和 no-follow 访问Agent runtime 当前可用
runtime 被回收动态进程生命周期结束持久文件被删除或内容正确
迁移器成功迁移目标和路径约束通过回读未来 Agent 行为都正确

Viewer 和 Agent 看到不同内容时,先核对同一 uid、Conversation 绑定的 project_id、Project 的 workdir_path、runtime scope、宿主 bind/PVC subPath 和 generation。不要用对象 URL 推断文件系统权限,也不要用相邻 Run 的路径猜测当前结果。

源码定位与验证

修改 identity、挂载、路径或清理语义时,除了相关 unit,还要验证真实 Docker/PVC 挂载和最终 POSIX 文件字节。

本项目基于 MIT License 开源,欢迎使用和贡献。