- 将代码库中所有对 /mnt/user-data 的引用更改为 /home/yuxi/user-data。 - 更新测试用例以反映新的路径结构。 - 调整 Docker 配置以使用新路径。 - 修改文档以与新的路径约定保持一致。 - 增强沙盒配置器中文件读取操作的错误处理。 - 改进代理文件管理系统中的附件处理。
17 KiB
Yuxi 沙盒架构与设计
文档说明
本文描述的是 Yuxi 当前已经落地的沙盒实现,目标是解释它的职责边界、系统结构、运行机制与工程限制。
这不是理想化方案说明,也不是 API 参考手册。阅读本文时,应始终基于一个前提:
Yuxi 当前的沙盒,定位是“为线程级 Agent 运行与文件访问提供受控工作区”,而不是“对外承诺强安全边界的通用多租户沙箱平台”。
::: warning 预发布 此部分涉及的技术路线与实现细节仍可能调整,文档会随实现演进而更新。 :::
1. 设计目标与边界
1.1 设计目标
Yuxi 的沙盒主要服务于以下场景:
- 为每个对话线程提供隔离的工作目录
- 为 Agent 提供有限的命令执行能力
- 为 Agent 文件工具提供受控文件系统访问
- 为工作台文件浏览器提供真实目录浏览与文件读取能力
- 将 Skills 以只读形式暴露给 Agent 与工作台
1.2 非目标
当前实现明确不覆盖以下目标:
- 强对抗场景下的高强度多租户隔离
- 完整的资源配额与调度系统
- 持久化的沙盒控制平面
- 审计级命令记录与合规追踪
- 面向终端用户的完整文件管理系统
这一点不是缺陷描述,而是范围定义。很多工程选择都建立在这个前提上。
2. 总体设计结论
当前沙盒方案可以概括为五个关键词:
- 线程级隔离:资源归属以
thread_id为核心,采用thread_id -> sandbox_id的映射 - 容器级执行:默认使用独立 Docker 容器承载执行环境
- 固定命名空间:Agent 与工作台通过
/home/yuxi/user-data、/home/yuxi/skills访问文件系统 - 惰性获取:不再依赖公开的
/api/sandbox/*生命周期接口,首次使用时自动获取沙盒 - 双视图接入:Agent 与工作台共享底层沙盒,但使用不同的文件系统语义与接口
这意味着 Yuxi 当前沙盒的本质是:
- 一个线程隔离的容器工作区
- 一个受限的文件访问模型
- 一个围绕 Agent 与 Viewer 场景设计的工程化执行环境
3. 系统架构
3.1 架构分层
从职责上,当前实现可以分为四层:
-
Provisioner 层 负责创建、发现、复用和销毁沙盒实例。
-
Executor 层 负责在已经存在的沙盒中执行命令、读写文件、列目录、下载文件。
-
接入层 负责把沙盒能力接入 Agent 文件系统与工作台文件浏览器。
-
HTTP/UI 层 负责将文件浏览能力暴露给前端页面。
3.2 关键模块
核心实现主要位于以下位置:
backend/package/yuxi/agents/backends/sandbox/backend/package/yuxi/agents/backends/composite.pybackend/package/yuxi/services/filesystem_service.pybackend/package/yuxi/services/viewer_filesystem_service.pybackend/server/routers/filesystem_router.py
3.3 调用关系
Agent / Frontend
|
+-- Agent filesystem tool
| |
| +-- create_agent_composite_backend(...)
| |
| +-- default: sandbox backend
| +-- route /home/yuxi/skills/: readonly skills backend
|
+-- Viewer filesystem panel
|
+-- /api/viewer/filesystem/*
|
+-- viewer_filesystem_service
|
+-- thread ownership check
+-- runtime context / selected skills
+-- provider.acquire(thread_id)
Sandbox Provider
|
+-- LocalContainerBackend
| |
| +-- create/discover/destroy docker container
| +-- YuxiSandboxBackend executes inside container
|
+-- RemoteSandboxBackend
|
+-- delegate lifecycle to remote provisioner
4. 核心抽象与职责划分
这一部分是理解整套实现的关键。当前代码中有多个名字相近的 backend,但职责并不相同。
4.1 SandboxBackend
定义于 sandbox_provisioner_base.py,是生命周期管理接口,负责回答四个问题:
- 如何创建沙盒
- 如何销毁沙盒
- 沙盒是否存活
- 是否可以发现已有沙盒
它只关心 实例生命周期,不关心文件访问和命令执行。
4.2 LocalContainerBackend
定义于 sandbox_local_container.py,是默认实现,负责:
- 使用
docker run创建本地沙盒容器 - 使用
docker stop销毁容器 - 发现现存的
yuxi-sandbox-*容器 - 在本地模式下管理端口、容器命名与 warm pool
它的定位是 本地容器管理器。
4.3 RemoteSandboxBackend
定义于 sandbox_remote.py,负责通过 HTTP 与远程 provisioner 交互。
它解决的问题是:如果运行环境不适合由当前进程直接管理 Docker 容器,就把生命周期操作交给外部服务。
它的定位是 远程生命周期代理。
4.4 YuxiSandboxBackend
定义于 sandbox_executor.py,继承 deepagents 的 BaseSandbox,负责:
- 在容器内执行 shell 命令
- 读写和编辑文件
- 列目录、glob、上传、下载
- 进行路径标准化与输出遮蔽
它不负责创建和销毁容器。它工作的前提是:沙盒实例已经存在。
它的定位是 执行器,不是生命周期管理器。
4.5 YuxiSandboxProvider
定义于 sandbox_provisioner.py,是实际的协调者,负责:
- 维护
thread_id -> sandbox_id的内存态映射 - 首次访问时创建或发现沙盒
- 重复访问时复用已有沙盒
release()后放入 warm pool- 在超时或进程退出时回收资源
它的定位是 统一入口与资源调度器。
5. 运行模式与部署拓扑
5.1 默认模式:本地 Docker 容器
在默认开发环境中,系统拓扑如下:
宿主机
├── Docker Daemon
│ ├── api-dev
│ ├── web-dev
│ └── yuxi-sandbox-<sandbox_id>
└── project workspace
api-dev 容器
└── Yuxi 后端进程
└── 通过 docker CLI 或 Docker API 管理 yuxi-sandbox-* 容器
关键点:
api-dev不直接执行用户命令- 用户命令实际运行在独立的
yuxi-sandbox-*容器中 - 后端承担的是控制面职责,而不是执行面职责
5.2 远程 provisioner 模式
如果设置了 YUXI_SANDBOX_PROVISIONER_URL,provider 会切换为 RemoteSandboxBackend。
此时:
- 当前进程不直接创建本地沙盒容器
- 生命周期由远程服务负责
- 本地主路径不再是 Docker 容器管理,而是 HTTP 协调
当前主路径与主要测试覆盖仍以本地容器模式为主。
5.3 Docker CLI 与 Docker API 双路径
当前实现支持两种与 Docker 交互的方式:
- 优先使用 Docker CLI
- 当环境中不可用时,回退到 Docker Unix Socket API
这是一种工程兼容设计,目的是提升不同运行环境下的可用性。
6. 生命周期模型
6.1 线程级资源归属
当前资源归属模型是:
thread_id -> deterministic sandbox_id -> sandbox instance
sandbox_id 由 thread_id 的 SHA256 截断生成。这样做有两个直接收益:
- 同一个线程重复访问时更容易复用沙盒
- provider 重启后可以通过
discover(sandbox_id)找回仍然存活的容器
6.2 acquire() 逻辑
provider.acquire(thread_id) 的核心流程如下:
- 检查当前线程是否已绑定活跃沙盒
- 若无,检查 warm pool 中是否存在对应
sandbox_id - 若无,尝试
discover(sandbox_id)发现已有容器 - 若仍不存在,则创建新沙盒
- 将结果注册到 provider 的内存态映射中
这套流程兼顾了:
- 同线程复用
- provider 重建后的容器发现
- 空闲沙盒快速回收后的再利用
6.3 release() 与 destroy()
两者语义不同:
release(thread_id):解除线程绑定,把实例放入 warm pool,等待后续复用或超时清理destroy(thread_id):直接销毁对应实例,不再保留
这一区分是为了兼顾响应速度和资源回收。
6.4 空闲回收
provider 内部有空闲检查线程,按固定周期清理:
- 长时间无访问的活跃沙盒
- 已经进入 warm pool 且超时的沙盒
相关默认值定义在 sandbox_config.py:
DEFAULT_IDLE_TIMEOUT = 600IDLE_CHECK_INTERVAL = 60
6.5 为什么不再依赖 /api/sandbox/*
当前设计不再暴露公开的沙盒生命周期 API,原因很明确:
- 生命周期属于内部资源管理逻辑
- 前端不应承担“先准备沙盒、再访问文件”的协调责任
- 惰性获取可以减少状态同步与接口耦合
因此当前模式是:
- 首次访问文件系统时自动
acquire() - 不再要求前端显式调用沙盒准备接口
7. 文件系统模型
7.1 虚拟命名空间
当前沙盒对外暴露两个核心命名空间:
/home/yuxi/user-data/home/yuxi/skills
其中:
/home/yuxi/user-data是线程私有工作区/home/yuxi/skills是可见 Skills 的只读视图
7.2 user-data 目录结构
每个线程会在宿主机侧创建自己的 user-data 根目录,并确保以下子目录存在:
workspaceoutputsuploadslarge_tool_resultsuploads/attachments
这套目录最终映射到容器内的 /home/yuxi/user-data/*。
7.3 路径别名
为了兼容 Agent 使用习惯,当前实现保留了若干路径别名,会被统一映射到 /home/yuxi 命名空间,例如:
/workspace->/home/yuxi/user-data/workspace/outputs->/home/yuxi/user-data/outputs/uploads->/home/yuxi/user-data/uploads/attachments->/home/yuxi/user-data/uploads/attachments/skills->/home/yuxi/skills
这些别名是兼容层,不是推荐的长期抽象。新的系统语义应以 /home/yuxi/* 为准。
7.4 路径安全约束
路径校验主要由 path_security.py 负责。核心规则如下:
- 仅允许访问
/home/yuxi/user-data与/home/yuxi/skills - 禁止路径穿越
- 命令中的绝对路径只允许
/home/yuxi/*与少量系统运行时前缀
允许的系统前缀主要包括:
/bin//usr/bin//usr/local/bin//usr/lib/
这类放行是为了保证容器内基础命令与运行时依赖可用。
7.5 输出遮蔽
执行结果在返回前会做宿主机路径遮蔽,避免把宿主机真实路径直接暴露给 Agent 或前端。
这一步不是强安全措施,但它对保持虚拟路径语义一致非常重要。
8. Agent 与 Viewer 的双视图设计
8.1 为什么需要双视图
当前文件系统接入并不是一套接口复用到底,而是明确拆成两种语义:
-
Agent 视图 面向工具调用与 prompt 约束,强调“模型可以怎样访问文件”
-
Viewer 视图 面向工作台浏览、读取和下载,强调“用户可以怎样查看文件”
这不是重复建设,而是避免语义错位。
8.2 Agent 侧接入
Agent 侧通过 create_agent_composite_backend(...) 构建 composite backend:
- 默认 backend 为 sandbox backend
/home/yuxi/skills/路由到只读的SelectedSkillsReadonlyBackend
这保证了:
- 线程工作区来自沙盒
- Skills 只读且受当前 agent 配置约束
8.3 Viewer 侧接入
工作台不再直接复用 Agent backend,而是通过 viewer_filesystem_service.py 暴露独立能力:
/api/viewer/filesystem/tree/api/viewer/filesystem/file/api/viewer/filesystem/download
它的特点是:
- 只暴露浏览、读取、下载语义
- 根目录视图明确展示
user-data与skills - Skills 可见范围仍受当前 agent config 约束
8.4 旧 /api/filesystem/* 的定位
当前仍保留旧的 Agent 文件系统接口:
/api/filesystem/ls/api/filesystem/cat
它们的定位是:
- 服务于 Agent 语义下的文件浏览
- 使用 composite backend
- 保持与 Agent 可见文件系统一致
因此,不应把它视为工作台浏览器的长期接口。
9. Skills 的挂载与可见性
Skills 不是简单的宿主机目录透传,而是受运行时上下文控制的只读视图。
当前模型有两个关键点:
- Skills 物理上以只读挂载方式进入沙盒
- 逻辑上只有当前 agent config 选中的 skills 对 Agent 和 Viewer 可见
这意味着:
- “挂载存在”不等于“对当前线程可见”
- Viewer 与 Agent 必须共享同一套可见性规则
这是当前设计中很重要的一条一致性约束。
10. 典型执行流程
10.1 Agent 首次访问文件系统
Agent tool call
-> resolve_sandbox_backend(thread_id)
-> provider.acquire(thread_id)
-> create or discover sandbox
-> build composite backend
-> execute ls/read/write/command inside sandbox
10.2 工作台首次打开文件浏览器
Viewer request
-> /api/viewer/filesystem/tree
-> verify thread ownership
-> load runtime context and selected skills
-> provider.acquire(thread_id)
-> list sandbox or skills namespace
10.3 读取与下载文件
读取和下载都遵循同样的原则:
user-data路径走 sandbox backendskills路径走只读 skills backend- 非
/mnt命名空间路径会被拒绝
11. 配置项
沙盒核心配置定义于 sandbox_config.py。常用项如下:
| 配置项 | 作用 |
|---|---|
YUXI_SANDBOX_IMAGE |
沙盒镜像 |
YUXI_SANDBOX_BASE_PORT |
本地沙盒基础端口 |
YUXI_SANDBOX_CONTAINER_PREFIX |
容器名前缀 |
YUXI_SANDBOX_IDLE_TIMEOUT |
空闲超时时间 |
YUXI_SANDBOX_MAX_REPLICAS |
最大并发沙盒数 |
YUXI_SANDBOX_HOST |
宿主机访问沙盒容器的地址 |
YUXI_SANDBOX_PROVISIONER_URL |
远程 provisioner 地址 |
YUXI_SANDBOX_SECURITY_OPTS |
传递给容器的安全选项 |
YUXI_HOST_PROJECT_DIR |
宿主机项目目录映射辅助配置 |
YUXI_DOCKER_API_BASE |
Docker API base URL |
YUXI_DOCKER_API_SOCKET |
Docker Unix Socket 路径 |
12. 测试与验证
当前与沙盒相关的测试主要覆盖三类问题:
12.1 后端与 provider 行为
backend/test/test_sandbox_backends.py
覆盖内容包括:
- composite backend 与 sandbox backend 的接入关系
- provider warm pool 与生命周期逻辑
- remote backend 的基础校验
- 路径规范化与异常输入处理
12.2 文件系统接口
backend/test/api/test_filesystem_router.pybackend/test/api/test_viewer_filesystem_router.py
覆盖内容包括:
- Agent 视图与 Viewer 视图接口行为
- 用户权限与线程归属校验
- 目录浏览、文件读取与错误分支
12.3 端到端验证
backend/test/api/test_sandbox_e2e.pybackend/test/api/test_viewer_filesystem_e2e.py
覆盖内容包括:
- 真实起沙盒容器
- 在容器内执行命令
- 验证文件写入、读取、附件复制与 viewer 行为
测试中会主动清理 yuxi-sandbox-* 容器,这本身也说明当前方案依赖真实容器资源,而不是纯内存 mock。
13. 当前限制
13.1 安全强度是工程化的,不是强安全承诺
当前实现通过容器隔离、路径约束、命名空间限制来降低风险,但它并不等同于强对抗场景下的高强度沙箱。
13.2 Provider 状态未持久化
当前 YuxiSandboxProvider 的核心状态保存在进程内存中,例如:
_sandboxes_sandbox_infos_thread_sandboxes_warm_pool
这意味着它更适合当前产品架构,而不是完整的分布式控制面设计。
13.3 Viewer 仍以只读为主
当前工作台文件浏览器主要提供:
- 浏览
- 读取
- 下载
它不是完整的文件操作终端,也不是通用文件管理器。
13.4 兼容路径仍然存在
/workspace、/uploads、/attachments 等别名仍在使用,这说明路径模型尚处于兼容收敛阶段。
13.5 本地容器模式仍有工程折中
例如:
- 依赖 Docker 运行环境
- 兼容 Docker CLI 与 Docker API 双路径
- 依赖本地资源条件与端口可用性
这些都属于工程现实,而不是抽象层面的完美设计。
14. 后续演进方向
后续如需继续完善,优先方向应是:
-
强化控制面 将沙盒元数据、生命周期和回收策略从单进程状态进一步抽离。
-
收敛路径模型 逐步减少历史别名,统一到
/home/yuxi/user-data与/home/yuxi/skills。 -
强化安全边界 在容器运行时、权限模型、配额控制和审计能力上继续补强。
-
继续区分 Agent 视图与用户视图 保持两类接口语义清晰,避免再次把 Viewer 语义挤回 Agent backend。
15. 总结
Yuxi 当前的沙盒方案,本质上是一套 线程级容器工作区 + /mnt 命名空间 + provider 惰性获取 + Agent/Viewer 双视图接入模型。
它已经能够较好支撑当前产品中的 Agent 文件工具、工作台文件浏览和 Skills 只读挂载场景,但它依然是 面向当前业务目标的工程化沙盒,不是已经完成强安全、强控制面建设的通用沙箱平台。