
◆ 博主名称: QuZhengRong
AI俘虏,样式苦手
目录

Agent 一旦具备读写文件、执行命令的能力,风险便不再仅限于幻觉输出,还会引发各类真实场景问题:例如误删目录、篡改宿主机配置、MCP 进程污染运行环境。一些框架会将文件操作与命令执行放在隔离环境中运行,以此降低对生产环境的影响,这个隔离环境就是沙箱。
本文将结合 AgentScope 的 workspace 模块,解析沙箱的实现原理,并根据源码拆解 DockerWorkspace 的实现思路。
一、什么是沙箱
沙箱是一套隔离的执行环境。Agent 在其中执行命令、读写文件、拉起部分工具进程;未挂载宿主机目录时,默认碰不到宿主机上的其它路径。若配置了 host_workdir 并挂载到工作区根目录,则这块宿主机目录可读写。
在 AgentScope 里,Agent 直接交互的抽象是工作区(Workspace):
- 本地无隔离场景使用
LocalWorkspace; - 需要安全隔离时,使用
SandboxedWorkspaceBase的子类,典型实现就是DockerWorkspace。
WorkspaceBase 向 Agent 对外暴露三类核心能力:
- 技能(Skills):可按需装配的技能文档
- 工具(Tools):内置文件读写、命令执行能力,以及 MCP 工具
- 上下文落盘(offload):将超长上下文、工具返回结果持久化存储,需要时再读取
外部调用常用接口:list_mcps、list_skills、offload_context。
工作区实例构造时并不会直接拉起环境。可以显式调用 initialize / close,也可通过 async with 管理生命周期
from agentscope.workspace import DockerWorkspace
async with DockerWorkspace(
host_workdir="./workspace-data", # 挂载至容器内 /workspace;不配置则容器销毁后数据全部丢失
base_image="python:3.11-slim", # 基础镜像需预装 python3
) as workspace:
mcps = await workspace.list_mcps(agent_id="a1", session_id="s1")
官方文档参考:Manage Resources、MCP Gateway。
下文基于仓库当前源码梳理,若文档描述与源码实现存在出入,以源码为准。
二、沙箱的作用
当 Agent 拥有工具执行权限时,模型的幻觉会转化为真实的副作用。沙箱主要承担 4 项职责:
- 隔离副作用:
rm删除、配置修改、常驻进程拉起等操作,被限制在沙箱内部;未挂载时默认触及不到宿主机其他业务目录。 - 统一根路径约定:Docker 模式下固定根目录为
/workspace。技能文件、会话上下文、offload 持久化数据都在这一路径下管理,路径规则统一,减少歧义。 - 支撑隔离环境内的 MCP:标准 stdio 模式的 MCP 进程运行在容器内部,宿主机无法直接建立 stdio 通道。沙箱内部启动 MCP Gateway 做中转,宿主机侧依旧使用统一的
MCPClient调用。 - 和权限体系解耦管控:沙箱负责控制在哪里运行代码、在哪里写入文件;是否允许 Agent 调用某一个工具,由 Permission 权限模块判断。两套机制叠加完成安全校验。
本地调试、Agent 逻辑完全可信的场景,可以直接使用 LocalWorkspace ;当 Agent 可操作生产目录、需要加载不可信代码时,才启用沙箱。
三、沙箱的实现思路
结合 AgentScope 的模块拆分,工作区实现可以划分为 4 个核心部分:
1、Backend 后端抽象层
Workspace 业务逻辑不直接耦合 Docker 原生 API ,而是面向统一 Backend 接口编程:
exec_shell:在沙箱环境内按 argv 列表直接执行程序read_file/write_file:跨沙箱边界读写文件join_path:遵循后端的路径规则拼接路径
更换隔离后端(容器、VM、进程隔离等)只需要替换 Backend 实现,上层 Workspace 逻辑可以复用。
2、生命周期模板
隔离环境有固定的启停流程,由 SandboxedWorkspaceBase 封装成模板方法:
- provision:创建沙箱实例,绑定对应的 Backend
- layout:初始化目录结构,创建
workdir、skills/、sessions/、data/等目录 - bootstrap / gateway:按需安装依赖、拉起沙箱内部的 MCP Gateway
- serve:按需注册会话 MCP、响应工具调用
- teardown:销毁沙箱资源;要求支持重复调用,内部捕获消化异常
子类仅需要实现三个钩子:如何 provision、如何 teardown、启动时是否需要执行 bootstrap 脚本。Gateway 启停与经 Gateway 的 MCP 路由落在 SandboxedWorkspaceBase;目录布局、.mcp 持久化等通用逻辑主要在 WorkspaceBase。
3、目录与会话隔离
Agent 在沙箱内可见的根目录就是 workdir,WorkspaceBase 约定统一目录结构:
/workspace
├── .mcp # 会话维度的 MCP 配置声明文件
├── data/ # offload 落盘文件,存放多模态数据、工具返回结果
├── skills/ # 技能种子模板,每个 Agent 独立分区
└── sessions/ # 按会话隔离存储上下文快照、工具执行记录
MCP 配置按 Agent + Session 维度存储,不同会话的 MCP 进程互相独立,避免有状态 MCP 串数据。
技能分两步:initialize 时把 skill_paths 种进 skills/.seed;某个 Agent 第一次用到技能时,再从 .seed 拷贝到该 Agent 私有分区。分区创建完成后,Agent 删除分区内的技能文件,不会自动从种子目录恢复。
4、MCP Gateway 中转层
容器内部的 MCP 进程无法直接和宿主机建立 stdio 通信,解决方案:
- 在沙箱内部启动 HTTP Gateway 服务,由 Gateway 持有已注册的上游 MCP 子进程;
- 宿主机不映射容器端口,依靠 Backend 在沙箱内执行一段 shim 脚本,访问容器内 loopback 地址和 Gateway 通信;
- 宿主机对外仍然暴露
MCPClient接口,上层调用无感。
Gateway 不需要监听宿主机网卡,通信闭环完全落在容器内部。
四、源码分析
主线链路:WorkspaceBase → SandboxedWorkspaceBase → DockerWorkspace → MCP Gateway
1、类继承关系
WorkspaceBase # 定义技能、工具、offload、目录规范的顶层抽象
└── SandboxedWorkspaceBase # 封装沙箱生命周期、内置 MCP Gateway 模板
└── DockerWorkspace # 基于 aiodocker 拉起容器,对接 DockerBackend
WorkspaceBase:定义 Agent 可调用的标准 Workspace API。文件读写、目录布局、技能分区、.mcp配置持久化,所有可以下沉给 Backend 的操作都放在这里;SandboxedWorkspaceBase:叠加隔离环境能力与 Gateway 启动、管理逻辑;DockerWorkspace:在三个生命周期钩子上,实现_provision_backend、_teardown_backend;镜像已预装 Gateway 时,沿用基类空的_bootstrap_commands,运行期不必再安装依赖。
2、initialize 初始化流程
initialize 具备幂等性:环境已经存活(is_alive=True),直接返回,不会重复初始化。
# 源码逻辑摘要(_sandboxed_base.py)
async def initialize(self) -> None:
if self.is_alive:
return
await self._provision_backend() # 创建沙箱实例,绑定 self._backend
self._mcp_specs = await self._restore_mcp_specs()
await self._ensure_workspace_layout() # 初始化全套目录结构
await self._setup_mcp_gateway() # 缺失脚本则执行 bootstrap,拉起 Gateway,轮询健康接口 /health
await self._migrate_skill_layout()
await self._setup_skills()
self.is_alive = True
close 流程:先关闭宿主机侧 GatewayClient、释放 MCP 句柄,再调用 _teardown_backend 销毁沙箱。Gateway 子进程随容器一起回收,不需要逐个关闭上游 MCP。
reset 是轻量重置:保留沙箱容器和 Gateway 进程,清空 MCP 注册记录、.mcp 配置、skills/、sessions/、data/ 目录。默认 MCP 配置会保留,下次调用 list_mcps 重新继承;但种子技能不会重复拷贝。
3、子类抽象钩子
@abstractmethod
async def _provision_backend(self) -> None:
"""创建沙箱实例,初始化 self._backend,提供 exec_shell / read_file / write_file 能力"""
@abstractmethod
async def _teardown_backend(self) -> None:
"""销毁沙箱;支持重复调用,内部自行捕获异常"""
def _bootstrap_commands(self) -> list[str]:
"""Gateway 脚本不存在时,执行环境准备命令。预打好镜像直接返回 []"""
return []
第三节提到的生命周期模板,代码落地就是这一组抽象钩子。
4、DockerWorkspace 容器实现
_provision_backend 基于 aiodocker 分两步:
- 根据 Dockerfile 和附加文件内容哈希判断镜像是否存在,存在则跳过构建;
- 镜像就绪后,创建容器实例。
关键细节:
- 构建镜像时提前把 Gateway 打包到
/root/.agentscope,运行阶段一般不需要在线 pip 安装; - 容器启动命令为
["sleep", "infinity"],容器持续保活;Gateway 意外退出后,可以在容器内重新拉起; - 配置
host_workdir才会将宿主机目录挂载到/workspace;Gateway 默认监听 5600 端口,仅容器内可访问。
config = {
"Image": self._image_tag,
"Cmd": ["sleep", "infinity"],
"WorkingDir": "/workspace",
"Labels": {
"agentscope.workspace.id": self.workspace_id,
},
}
# 配置 host_workdir 时追加挂载:
# HostConfig.Binds = ["{宿主机绝对路径}:/workspace:rw"]
容器就绪后,DockerBackend 封装 aiodocker 的容器执行接口:exec_shell 按 argv 在容器内直接 exec ;文件读写依靠 Docker API 的 get_archive / put_archive。
5、MCP Gateway 启动流程
沙箱内部 MCP 无法直接对接宿主机 Agent,_setup_mcp_gateway 完整流程:
- 检测 Gateway 脚本是否存在,缺失则执行
_bootstrap_commands准备环境,写入辅助脚本与 Gateway 主程序;脚本写入是最后一步,中途失败下次 initialize 会重试; - 先执行
pkill -f '[_]mcp_gateway_app.py'清理残留进程,再用 nohup 启动;杀进程和启动不能放在同一条 Shell,避免误杀刚拉起的服务; - 宿主机侧
GatewayClient,通过backend.exec_shell在容器内运行轻量 shim:请求写入临时文件,shim 访问容器 loopback 网关地址,结果以 JSON 输出到 stdout 返回给宿主机; - 最多等待约30秒探测
/health健康接口;超时会读取gateway.log末尾日志,一并抛出到异常信息。
Gateway 刚拉起时注册表是空的,每个会话第一次调用 list_mcps,才会向 Gateway 注册当前会话的 MCP。部分注释写 Gateway 自动读取 .mcp 文件,以 _setup_mcp_gateway 和 list_mcps 运行时逻辑为准。
五、LuckReport 项目 推荐

导航:LuckReport专栏
1、项目简介
Luck-Report 是一款面向中国式复杂报表的开源报表引擎。基于单元格迭代,可在浏览器中完成交叉表、分组表、主从表等复杂版式的设计、预览与导出。
Luck-Report 内置 AI 智能助手,可结合 大模型 配置与知识库,用自然语言完成制表、改表与答疑。后端基于 Spring Boot,支持独立部署。项目由开源 UReport2 重构而来,在保留中国式报表能力的基础上,补齐现代化架构与 AI 辅助能力。
本仓库为报表引擎后端源码,基于 Apache-2.0 开源,可免费商用。