沙箱:当前实现、八家对标与实施记录

这页分三层:我们现在怎么做(宿主原生边界、两个平台、持久开关、authority与25个执行面的当前分类)、 别人怎么做references/下八个框架逐一,包括四个明确不做沙箱的)、 我们采用了什么(设计取舍、实施顺序、验收结果与已知限制)。 设计文档:docs/reference/design/runtime/sandbox.md

层一 · 我们现在怎么做
层二 · 别人怎么做
层三 · 实施决策与记录
LAYER 1

我们现在怎么做

系统调用级实现位于openprogram/sandbox/__init__.py,统一入口是backend/local.py::_invocation。 bash、process、execute_code、cron direct、memory写入器的MCP shell和one-shot MCP均复用该入口;文件工具使用同一份写入根与hard-floor校验。 下面四节记录四个方向的边界、两个平台各自的机制、开关为什么现在传得到,以及固定权限档常量表如何参与25个执行面的判定。 执行面计数是审计清单,不代表U01—U23都应采用同一种隔离策略。

已经确定的产品方向:本地沙箱保持宿主原生实现,macOS使用Seatbelt,Linux使用bubblewrap,新安装默认workspace-write。 authority与pairing决定外部请求是否具有执行能力,沙箱限制已获准agent对宿主产生的副作用,并保留宿主真实开发环境。 Docker不是当前沙箱实现,也不是后端不可用时的自动回退;以后只有出现明确的独立Linux环境需求时,才设计为用户显式选择的第二后端。

01一张边界图

四个方向不对称:读允许访问大部分宿主文件但拒绝凭证清单,写限制到工作目录,执行不限制可执行文件但子进程继承沙箱,网络在两个平台都禁用。 凭证清单与环境变量白名单已经预置,但Linux不能实现中段glob匹配。

沙箱边界:四个方向 左边是受限子进程,中间竖线是沙箱边界,右边是宿主资源;图标表示每类访问的实际策略状态。 沙箱内的子进程 bash -c <模型给的命令> macOS由sandbox-exec包住 Linux由bwrap包住 沙箱边界 文件读 · 大部分宿主路径可读,凭证清单被拒绝 macOS: allow file-read* 之后逐条deny正则 Linux: --perms 0000 --tmpfs / --ro-bind /dev/null 已验证的具体凭证路径不可读;macOS额外支持中段glob: ~/.ssh/** ~/.aws/** ~/.openprogram/auth/** ~/.claude.json keychain macOS:**/.env 文件写 · 工作目录和临时目录,另有固定拒写路径 命令和文件工具都不能写入自动导入目录;已登记的owner安装来源只允许通过安装流程变更 macOS只允许当前进程TMPDIR;Linux的/tmp是隔离tmpfs,位于其下的工作目录由后续bind恢复 进程执行 · 两个平台都不限制,子进程继承沙箱 实测gitpython3makeclang、conda python、/usr/sbin全部可跑 按路径限制exec挡不住执行:/bin/bash -c自己就在里面,脚本可以从任何地方读进来 仍然不能执行pstop:Seatbelt不允许在profile内exec setuid二进制 网络 · 出站入站全断,两个平台一致 macOS由(deny default)拒绝网络;Linux使用--unshare-net DNS、回环、Docker socket实测都不可访问;与openclaw启用容器后的默认网络状态相同 当前边界: 网络访问被拒绝;凭证清单阻止无网条件下把已知凭证复制到记忆库。 清单之外仍然允许读取宿主文件;该范围是当前产品选择,不是最小文件视图。
图标说明:双箭头=不限制,单箭头加竖线=部分允许,叉=拒绝。 连线颜色与策略状态一致,虚线表示请求被拒绝。

02两个平台

同一个wrap_command在两个平台走完全不同的机制,各自额外放行了一些东西,也各自有一处只在实跑里才会暴露的细节。

MACOS

Seatbelt

/usr/bin/sandbox-exec -p <profile> /bin/bash -c <命令>
机制
profile内联生成,以(deny default)为默认策略,再增加具名允许规则。嵌套Seatbelt会被系统拒绝,内层不能放宽外层限制。
额外放行
/dev/null等五个字符设备用require-all加vnode-type CHARACTER-DEVICE放行读写。sysctl-read只允许hw.*和四个uname所需的kern.*名字;不再允许通用mach-lookup,剪贴板和Apple Events服务不可达。写范围只包含当前TMPDIR
屏蔽写法
每条deny glob编译成锚定正则,deny file-read*deny file-write-unlink成对发,被禁读的路径不能用删除反推存在性。工作目录拼进profile前先转义。
正则限制
Seatbelt的正则引擎不支持(?:…),使用非捕获分组会导致deny规则不生效且没有错误;引擎匹配解析符号链接后的真实路径,所以静态前缀是符号链接时要同时生成原路径和真实路径规则。
LINUX

bubblewrap

bwrap --new-session --die-with-parent --unshare-pid --unshare-ipc --unshare-uts --cap-drop ALL --ro-bind / / --proc /proc --dev /dev --unshare-net --tmpfs /tmp --bind <cwd> <cwd>
机制
挂载视图重建:根只读,工作目录单独bind成可写,net / pid / ipc / uts四个命名空间隔离,capability全丢。
屏蔽写法
目录用--perms 0000 --tmpfs遮蔽,文件用--ro-bind /dev/null替代。--cap-drop ALL移除DAC_OVERRIDE,所以容器内子进程是root时权限位仍然生效,实测返回Permission denied
进程隔离
--unshare-pid之后实测:沙箱内只看得到4个PID,读宿主进程的/proc/<pid>/environ报文件不存在,kill -9报进程不存在,宿主进程照常活着。没有它的话,洗干净子进程环境也没用。
可用性探测
不只检查PATH。每个bwrap可执行文件首次使用前,以正式策略所需的mount、PID、IPC、UTS、network和capability限制执行/bin/true;已安装但被宿主禁止创建非特权user namespace时,按后端不可用处理。
挂载顺序
--tmpfs /tmp必须位于cwd bind之前,否则/tmp下的工作目录会被tmpfs遮蔽。拒绝访问的挂载还必须跳过宿主上不存在的路径:根是只读绑定,bubblewrap无法创建挂载点,强制挂载会使调用失败。两项均由运行测试确认。
维度macOSLinux
网络断(deny default兜住)断(--unshare-net)
整盘可读是,减去屏蔽清单是,减去屏蔽清单
屏蔽清单表达力正则,中间带通配符的glob也能表达只能盖路径,**/.env这类无对应实现,被丢弃
写范围cwd + 当前TMPDIR + /tmp + /private/tmpcwd + 隔离tmpfs /tmp
/tmp语义真实/tmp,与宿主共享、可持久空tmpfs,用完即弃
exec限制不限制,子进程继承profile不限制
/dev/null可写
看得到宿主进程否,PID命名空间已隔离
环境变量白名单白名单
工作目录被遮蔽不会不会,tmpfs先挂
ps / top不能执行,setuid平台限制正常
用的shell/bin/bash(不开沙箱时是/bin/sh)/bin/bash

03开关:载体从ContextVar换成配置

开关原来是一个ContextVar:设它的地方和真正执行命令的地方不在同一个上下文里,值就丢了,三条路径里两条在丢,其中一条是用户最常用的Web。 下图是改之前的传递,留在这里是因为它说明了为什么载体必须换。

改之前:开关从设置点到执行点 红色竖线是上下文断掉的地方。跨过它,新的执行环境拿到的是空Context,读回默认值false。 ① 在哪里设开关 ② 轮次实际在哪里跑 ③ 执行点读回什么 ④ 命令的实际状态 WEB websocket的asyncio任务 sandbox_enabled.set(True) 界面立刻显示 Sandbox: ON 线程边界 裸threading.Thread 新线程拿到的是空Context 整个webui目录没有copy_context 读回 false 和开关的显示状态相反 命令无沙箱执行 开关是个no-op CLI REPL线程里的 /sandbox sandbox_enabled.set(True) 同一个线程继续往下走 同一线程、同一Context 没有任何边界要跨 读回 true 开关在这条路径上有效 命令被包住 仅限同线程的bash FUNC 父进程里开关为true CLI或Web都可能是这个起点 进程边界 mp spawn 出的新解释器 spawn不复制contextvars 读回 false 只有usage上下文被显式恢复了 全部无沙箱执行 每个agentic function里的bash
三个边界的共同点:每一个都在起一份新的执行环境。逐个补copy_context()并不等价: spawn出来的子agent确实能靠它把开关带进worker线程,但在followup线程上照样掉回默认值,这是实测的;嵌套CLI根本不是线程,补不了。 一个要跨进程存活的用户级设置,本来就不该挂在上下文变量上。

现在的载体是文件:策略在包装命令的那一刻从~/.openprogram/config.jsonsandbox.*读出来。 文件不属于任何上下文,所以三个边界都跨得过去。

路径改之前改之后(实测)
Web:asyncio任务里设,普通线程里执行读回false,命令无沙箱执行线程内写工作目录之外被拒,$OPENAI_API_KEY长度为0
CLI:REPL同线程有效有效,且跨会话保持
spawn@agentic_function的子解释器读回false,全部无沙箱执行子进程内写工作目录之外被拒,$OPENAI_API_KEY长度为0
审批bypasspermission_mode="bypass"短路到工具自己的execute照样被包住:策略在权限层之下解析,实测写越界和读SSH私钥都失败
平台后端不可用静默改为普通执行工具缺失或隔离机制探测失败时默认refuse并说明原因;warn可选
七个配置键sandbox.modewritable_rootsdeny_readdeny_writenetworkpass_envunavailable_policy, 都在config_schema.pySETTINGS里,所以openprogram config、setup向导、TUI设置页和Web设置页一起有。 新安装默认mode=workspace-write;已有用户的显式danger-full-access保持不变。平台工具不可用时默认拒绝,不退回宿主执行。

04authority tier与覆盖面

authority_tier是请求边界上的owner/paired枚举;检查点用该枚举查询进程内写死的capability常量表。请求不能携带、创建或扩展capability列表。principal标识请求使用谁的授权以及谁能作出owner审批,不等于权限档。 permission rule或精确owner approval表达对本次操作的同意,SandboxPolicy限制操作实际可访问的宿主资源,不可取消的hard constraints独立于以上输入始终执行。字段定义与传播规则同时记录在speaker identity设计中。

一次宿主操作必须同时通过四项判定 TurnRequest运行时字段 principal_id = owner authority_tier = owner | paired 01 固定档位判定 operation映射到能力名 档位常量表不包含 → deny 02 规则或精确owner审批 只有认证本地interactive可申请 cron / subagent / paired渠道不升级 03 执行边界 SandboxPolicy / 外部容器 / 固定argv 所选边界必须能实施声明的限制 04 hard constraints始终执行:任何权限档、审批、bypass、job authority或用户设置都不能取消
执行判定:每次执行以及其他模型可影响的宿主副作用都必须计算 allow = hard_constraints ∧ tier_capabilities[authority_tier].contains(capability) ∧ permission_or_exact_owner_approval ∧ enforcement_boundary。 请求只携带权限档枚举;capability集合只存在于进程内常量表。模型正文、speaker和工具参数都不能创建或扩展它。
能力允许的操作默认归属
reply向当前会话返回文本,不产生其他宿主副作用ownerpaired
memory.source.append追加带speaker、authority_tier和信任状态的来源记录ownerpaired;未配对消息不进入agent
memory.trusted.promote把已审阅记录提升为owner轮次可自动召回的可信记忆仅认证本地owner interactive
schedule.create
schedule.manage
创建映射到schedule.create;修改、重排或删除映射到schedule.manage。创建或修改获批后保存新的不可变job capability仅认证本地owner interactive;属于宿主副作用
fs.read / fs.write
process.exec / network.send
读取或修改文件、启动进程、向外部系统发送owner;仍需规则、审批和执行边界
请求来源principalauthority tier缺失字段处理
认证本地Web / CLI / TUIprincipal_id=owner
interaction=interactive
owner入口必须主动构造;认证或owner配置无效时拒绝请求
已配对渠道实例owner;稳定账号ID另存为speaker identitypaired只按稳定账号ID匹配;显示名不参与身份判断
continuation / subagent显式继承caller principal显式继承同一档位;没有交互升级序列化或恢复缺字段即状态错误并deny,不能依赖unknown fallback
cron触发创建时批准的owner不可变job authority中的owner触发时不得重算为interactive;字段或hash不完整即deny
未配对或无稳定ID的渠道消息无agent principal不进入agent;返回配对码。群组消息另存为pending证据,私聊不归档
渠道记忆保留来源信任状态:已配对渠道发言是可信来源,并可调用memory.source.appendpaired档没有文件、进程、网络、调度、审批或runtime control能力。 未配对群组发言不进入agent,只归档为pending证据。pending证据当前仍可显式检索;hold队列准入和记忆读取过滤属于第二批,不能写成已实现。 只有认证本地owner能把已审阅记录执行memory.trusted.promote。由记忆内容引出的操作仍须通过当前轮次的权限档、权限规则、审批与沙箱检查,Writer和模型输出不能改变权限档或信任状态。

实施前审计把范围固定为25个执行面:当时2个已受管、23个未统一受管。当前U01—U23已经逐项采用强制策略或记录受信边界与保留理由;一个执行面按共同的命令来源与安全边界计数,不按每次subprocess调用计数。

实施前基线:25个执行点,2个走沙箱 每个方块是一处会把内容变成进程的代码位置。 固定审计口径:25个执行面 实施前2处:bash / process → backend/local.py::_invocation,以及memory写入器MCP shell 前者还要满足:sandbox.mode打开、用local backend(docker和ssh后端不走)、平台工具在 实施前其余23个执行面未统一受管;当前均已按下表分类和收口 P0模型来源:execute_code、cron direct/prompt、文件工具、watcher自动导入、嵌套CLI内置工具 受信配置:MCP stdio、shell hooks、plugins;需要安装与来源边界,不应按模型命令处理 第一方确定性执行与管理员CLI:逐项保留宿主权限并审计,不强制经过命令沙箱 修复目标是覆盖所有模型可影响的副作用,不是把每个subprocess调用统一包装 完成判据:下面U01—U23逐项标记信任类、强制策略与测试,不允许用“其他执行点”代替。 新增模型可影响的执行面必须先更新清单;没有归类或没有验证的条目阻止默认开启。
实施前已受管的2个执行面:M01是bash/process经local backend进入backend/local.py::_invocation;M02是memory写入器的MCP shell使用同一份SandboxPolicy。下面记录U01—U23的闭合方式。
编号实施前未统一受管的执行面来源类已实施边界 / 明确保留理由
U01backend/docker.py模型命令 / 外部容器声明容器是执行边界;命令仍经过工具规则、审批与审计,不宣称使用宿主sandbox
U02backend/ssh.py模型命令 / 远端主机声明远端主机是执行边界;目标由owner配置,命令仍经过工具规则、审批与审计
U03functions/tools/execute_code模型代码local路径通过LocalBackend.run复用_invocation;解释器、cwd与临时脚本均进入同一策略测试
U04cron/worker.py direct command模型请求保存 / 无人值守创建先检查schedule.create,修改、重排或删除先检查schedule.managepaired档必须在任何job持久化变更前deny。认证本地owner批准创建或修改后保存新的不可变job authority,固化principal、authority_tier=owner、command、cwd、policy与hash;触发时强制沙箱,ask与升级均deny
U05cron/worker.py prompt job模型prompt / 无人值守同样要求owner批准创建;重建请求时显式source=cron并加载不可变job authority,内部所有副作用继续按owner档常量表检查
U06memory/agent_runtime/claude_code.py模型驱动的嵌套agent协议层关闭内置命令与文件工具,注入OpenProgram受管MCP工具
U07functions/watcher.py → _registry.exec_module模型可写Python任何模型可写目录禁止自动导入;导入目录改为owner控制并记录来源
U08webui/_functions.py动态重载模型可写Python与U07共享同一条目录所有权约束;不能在宿主进程重载模型写入的模块
U09agent/process_runner.py已注册agentic functionspawn请求显式序列化SandboxPolicy;子进程内的命令、文件与嵌套工具保持同一策略
U10plugins/loader.pyowner安装的pluginplugin清单与代码目录不可由模型修改;加载事件记录来源。trust默认untrusted且拒绝加载,只有owner显式提升信任的plugin才加载,并在宿主进程内执行(plugins/sandbox.py的subprocess隔离未实现,不宣称已进程隔离)
U11mcp/client.py stdio serverowner配置的serverMCP配置不可由模型静默改写;启动argv与环境显式保存并审计
U12webui/routes/mcp.py one-shotHTTP请求体要求已认证owner请求并进入与MCP stdio一致的启动策略;删除“已沙箱”但实现未隔离的声明
U13events/shell_hooks.py gate commandowner配置;模型事件作stdin命令本身按受信配置处理;配置目录受保护,超时或启动失败不能成为安全强制规则
U14events/shell_hooks.py notifier commandowner配置;模型事件作stdin保持通知用途并审计;不得把notifier结果用于允许高风险操作
U15providers/_shared/cli_backend/runner.py模型prompt / 外部CLI若接入生产,先声明CLI内置工具策略;保持未引用时不得计为现有保护
U16functions/tools/grep模型参数 / 固定argv保持shell=False--分隔;增加路径范围测试,不需要命令sandbox
U17functions/tools/worktree模型参数 / 固定git argv仅允许受管仓库与固定子命令;保持argv调用并测试路径规范化
U18functions/tools/agent_browser模型参数 / 浏览器进程浏览器profile与下载路径使用独立策略;启动参数保持argv并进入工具审批
U19session/project/shadow store的git plumbing第一方确定性argv保留宿主执行;不接受模型shell字符串,测试参数分隔和仓库范围
U20_cli_cmds/programs.pyowner主动安装 / 更新保留管理员CLI权限;与agent工具不可互调,记录安装来源
U21_cli_cmds/browser.pyowner主动安装保留管理员CLI权限;安装命令不暴露给模型
U22_cli_cmds/upgrade.py与updaterowner主动升级保留管理员CLI权限;下载、替换和重启必须来自显式升级操作
U23plugins/installer.pyowner主动安装保留管理员CLI权限;安装入口不暴露给模型,安装后由U10约束加载
清单结果:U03—U09与U12的模型影响面已处理;U04/U05的测试证明paired渠道不能创建、修改、重排或删除job,owner批准生成的job capability在触发时不能扩大。 U01—U02明确使用外部边界并保留审批;U10—U11、U13—U15固定owner配置边界;U16—U23通过固定argv或管理员入口及回归测试保留宿主权限。任何新增或重新接入的执行面必须先加入此表,再决定是否使用OS沙箱。

下面保留修复前的自动导入流程。它说明为什么只修改wrap_command不够:直接文件工具和进程内导入需要各自的强制边界。当前实现已经在文件工具写入检查和owner登记来源两处阻止该流程。

实施前路径:模型输出 → 宿主进程内代码 01 模型调 write functions/agentics/x.py 写文件这个动作本身不可疑 02 watcher 轮询 每2秒扫一次该目录 变化触发 _registry.rescan() 03 exec_module spec.loader.exec_module() 导入即执行模块级代码 04 在agent进程内运行 拿到宿主进程的全部权限 包括进程内存里的凭证 实施前不经工具审批或shell。当前write/edit/apply_patch先执行写入根和hard-floor检查,watcher只导入owner登记来源;任一条件不满足都不能进入exec_module
实施前的subagent bypass缺口已经关闭:旧实现让permission_mode="bypass"在自动危险工具检查之前返回,影响 bashexecute_codeprocess以及write/edit/apply_patch。 当前hard constraints与capability检查位于bypass之前;命令使用宿主原生沙箱,文件工具执行统一路径检查,subagent没有交互升级能力。
实施前P0缺口实施前源码行为影响当前闭合方式
“Always allow”过宽审批持久化时只保存工具名;一次bash批准会得到allow bash后续任意bash都可跳过审批,范围大于原命令已完成:只保存规范化后的精确操作;复杂shell只允许单次批准
文件工具与watcher组合文件工具可写工作区外;watcher对functions/agentics/变化调用exec_module模型可写Python并由宿主进程自动导入已完成:文件工具统一写入根检查;自动导入只接受owner登记来源
cron无人值守执行direct job使用Popen(..., shell=True);prompt job启动分离CLI触发时没有交互审批路径,也没有统一沙箱已完成:签名执行spec、固化owner档、强制沙箱、ask与升级均deny
execute_code本地执行模型给代码和解释器路径,随后subprocess.run不经过Backend._invocation已完成:复用local backend及同一份SandboxPolicy
命令规则按原字符串前缀匹配未先处理ANSI、NUL、Unicode NFKC或env X=1包装保存规则与实际argv不一致;复杂shell文本也不能靠前缀安全解析已完成:规范化、shell解析、透明env包装与精确规则
runtime sandbox参数被忽略create_runtime(sandbox="read-only")的provider参数接受但不执行调用方看到的配置不构成隔离已完成:进程边界显式序列化并安装SandboxPolicy snapshot
LAYER 2

别人怎么做

references/下八个框架逐一:claude-codecodex-cliopenclawopencodehermes-agentpi-monopi-aiweclaw。 其中四个完全不做系统级沙箱,这本身是一种设计立场,下面写清它们各自靠什么替代。 计数上有两点:pi-aipi-mono同上游的只读子集、没有执行面; claude-codepi-mono的扩展调的是同一个外部包,所以这一组里独立的系统调用级实现只有两份。

05八家的隔离立场

按隔离强度分四档。左边是主动移除隔离的,右边是把执行推到另一个内核视图或另一台机器的。 同一个框架出现两次,表示它跨档:左边是默认路径,右边是可选配置。

隔离强度四档 档位说的是"模型给的命令最终在什么边界里执行",不是框架的成熟度。 档 00 移除隔离 把被包的agent的沙箱关掉 weclaw sandbox: danger-full-access 审批请求一律自动答allow 档 01 只有应用层策略 规则、审批、命令模式匹配 opencode SECURITY.md明确列为非目标 allow / ask / deny 三效果规则表 pi-mono 核心 spawn(bash, -c, cmd) 带全环境 没有审批,也没有 --yolo 可绕 hermes-agent 默认 TERMINAL_ENV=local,宿主bash 三层命令守卫顶上,见第07节 档 02 系统调用级沙箱 Seatbelt / bubblewrap / seccomp claude-code 实现在外部包 sandbox-runtime 网络走逐域名弹窗的代理 codex-cli 仓库内自研,四档沙箱模式 这一组里唯一的in-tree实现 pi-mono 加扩展 调的是同一个外部包 通过整个换掉bash工具接进来 OpenProgram Seatbelt / bubblewrap,默认workspace-write 档 03 容器或远端 换一个内核视图或换一台机器 openclaw 默认off;Docker为默认sandbox backend 无网、只读根、cap-drop ALL hermes-agent 可选后端 docker / modal / daytona 等五种 容器内整个跳过守卫层 pi-ai不在图上:14个文件、没有工具层、没有spawn,没有可隔离的东西,列在文字里只为把八家覆盖完整。 四个不做系统级沙箱的是opencodepi-mono核心hermes-agent默认路径weclaw。隔离强度跟着执行地点走,不跟着框架成熟度走。
openclaw需要按配置状态与后端分别读取:出厂sandbox.mode=off,关闭时审批默认放行; 启用且选择backend=docker时创建Docker容器,默认network=none、root filesystem只读、cap-drop ALL并设置no-new-privileges。 它也支持SSH与插件提供的OpenShell后端,因此Docker配置不能用于描述所有后端,更不能用于描述其出厂执行状态。

06四个方向逐家对照

同样四个方向,加上一列子进程环境变量,因为凭证从环境里漏出去和从磁盘上漏出去是两条独立的路。 绿=收住,橙=部分,红=敞开,灰=不适用。

读 / 写 / 执行 / 网络 / 子进程环境变量 每格是这一家在这个方向上的实际状态,取自实现而不是文档描述。 框架 执行 网络 子进程环境变量 claude-code 整盘,deny列表为空 默认拒加白名单 不限,子进程继承 代理,逐域名弹窗 由runtime处理 codex-cli 整盘,引擎有但清单为空 默认拒,护住 .git 不限 默认断,可配白名单 可配,默认不过滤 openclaw(启用时) 容器内只有两个挂载 工作区挂载默认只读 不限,argv先解包 network none,host被拦 名字正则加取值启发式 pi-mono 核心 不限,无工作区约束 不限 不限,没有审批 不限 全量继承 pi-mono 加扩展 denyRead 已预置 只准 cwd 和 /tmp 不限 10个域名的白名单 全量继承,扩展没传env hermes-agent 不限,deny自称非边界 敏感系统路径拒写 47条正则加hardline 有开关但传不进去 注册表推导后剥离 opencode *.env 走 ask 只有规则,无隔离 只有规则,无隔离 不限 不过滤 weclaw 不限 不限 不限,审批自动答allow 不限 全量继承 pi-ai 没有执行面:14个文件,只有provider和协议层,全仓库零个 spawn / child_process。没有可隔离的东西,不参与横向比较。 OpenProgram 整盘减凭证deny清单 cwd 加临时目录 exec不限,子进程继承 两个平台都断 白名单传入 当前差异:网络断开与环境白名单已实现;读仍采用“整盘可读减凭证deny”,不是容器的最小挂载视图。 openclaw这一行只描述启用后的容器;它的出厂状态仍是off,关闭时审批默认放行,因此不能用启用后能力代替默认风险。
维度claude-codecodex-cliopenclawpi-monohermes-agentopencodeweclawOpenProgram
系统级隔离外部runtime包仓库内自研Docker默认;SSH/OpenShell可选只有示例扩展默认无,后端可选明确不做主动移除Seatbelt / bubblewrap,模型影响面已分类
默认开是,read-only否,mode=off核心无此概念否,默认local是,workspace-write
粒度全局×单命令opt-outper-command,工具可覆盖全局×agent×tool×session整工具级allowlistper-pattern加按上下文per-tool×per-resource全局配置,尚无per-tool覆盖
配置入口分层settings.jsonconfig.toml加CLI flagzod schema生成JSON schemasettings.json,项目覆盖全局cli-config.yaml加环境变量opencode.json加frontmatter没有安全相关的键七个sandbox.*设置键
沙箱与审批沙箱内bash免审批里面被拒→问→不带沙箱重跑off时审批默认放行;启用后由容器与tool policy处理核心没有审批守卫就是审批系统只有审批审批自动答allowscope先判定;仅本地owner可精确单次升级
凭证屏蔽出厂状态引擎有,为空引擎有,为空启用时挂载源加环境变量过滤扩展预置denyRead环境变量从注册表推导*.env→askdeny-read清单加环境白名单
后端不可用继续执行但明确告警静默none,Windows降级强制拒绝加doctor预警四条路径都静默继续非交互fail-open默认refuse,可显式warn
审计内核deny行归因后返回模型结构化违规事件加OTel专用tool-policy logger审批生命周期钩子权限事件总线只有log.Printf结构化sandbox.violation及审批事件
资源限额仅RLIMIT_CORE=0pids/memory/cpus/ulimits,默认不设pids 256、tmpfs限额、600秒无超时CPU/内存/进程数配额不在范围内
受管范围只有Bash和PowerShell只有子进程启用容器内的一切只有bash工具terminal和code_execution模型可影响的进程、文件、cron、MCP与嵌套工具面
参考实现的边界声明:openclaw面向受信任的单用户operator,审批属于操作防误触机制,不被定义为对抗同一operator的安全边界; hermes-agent的SECURITY文档同样把模式扫描与审批定义为应用层策略,只有OS级隔离才是执行不可信输入的安全边界。 OpenProgram因此不能把“默认也是off”或“有审批”当成继续延后覆盖面的理由。

07独有机制与逐维度比较

这一轮新看的五家里,有十一种机制在claude-codecodex-cli里都不存在。 下面八张卡记录逐维度可采用的机制,并标出OpenProgram的采用状态和对应步骤。

八个维度的参考机制与采用结果 每张卡依次记录维度、参考实现、具体机制和OpenProgram采用状态。 凭证不进子进程环境 hermes-agent + openclaw 前者从provider注册表推导敏感变量, 后者增加敏感名称正则。OpenProgram 采用更严格的传入白名单并保留名称限制。 已采用 · 第02步 对应17个凭证目录 命令规则匹配之前 hermes-agent 加 openclaw 前者剥ANSI、剥空字节、NFKC归一; 后者按表解包18种启动器,无法证明 无法证明wrapper透明时拒绝,且不保存永久允许。 采用 · 第07步 规则规范化 bypass不能跳过的强制规则 hermes-agent 删根、mkfs、写裸设备、关机这一类, 在读取yolo和mode=off之前即拒绝, 拒绝信息明确说明任何设置均不能覆盖。 采用 · 第04步 对应bypass短路 按上下文的审批默认值 hermes-agent 五种上下文五种策略:cron默认deny, subagent自动deny并留审计行,理由是 工作线程无法提供交互,等待会阻塞父进程。 采用 · 第04/05步 ask→deny 嵌套agent自带的执行面 openclaw 不限制整个嵌套进程,在协议层对每个 方法分类,拒绝command/和fs/, 注入由宿主沙箱执行的替代工具。 采用 · 第06步 协议级受管工具 机制不可用时怎么办 openclaw Docker缺失时拒绝并说明缺失依赖; 镜像缺失时不使用通用镜像替代; 另有doctor在执行前检查。 已采用 · 第03步 默认refuse 隔离后端不可用时的行为 pi-mono 拦截器抛非Error异常时转成"扩展失败, 阻止执行";随包的示例在没有UI时直接 拒绝;非交互环境默认deny。 已采用 · 第03步 后端不可用 对自己的安全配置做检查 openclaw 一批具名检查项跑在用户配置上,其中一条 专门覆盖"设置里写着sandbox而有效模式 是off",正是我们已经有的那类缺陷。 采用 · 第08步 默认前预检 三条不采用的机制及其对应风险 pi-mono:项目配置覆盖全局 clone下来的仓库带一个配置文件就能 关闭全局沙箱,项目级扩展也没有信任验证。 pi-mono:截断后把全文路径给模型 输出按2000行截断,全文写进临时文件 并把路径返回,模型可再次读取未截断内容。 weclaw:聊天适配器自动批准审批 同一个ACP协议,hermes转发给客户端, weclaw自己替客户端答了,还没有发送者名单。

08凭证读取不能由网络隔离替代

子进程无网不表示读取到的凭证不会离开子进程:工具输出会返回agent runtime,并由宿主网络进入下一次模型请求。 因此凭证读取限制、环境过滤和执行面覆盖都必须独立存在。

子进程网络被拒绝后,凭证仍可经工具结果进入模型请求 空deny列表不能依赖网络沙箱证明安全 整盘可读 凭证都读得到 工具输出返回runtime 不需要子进程发起网络请求 宿主构造模型请求 宿主网络不在子进程命名空间内 凭证进入模型上下文 网络沙箱没有阻止该数据流 另外三家预置了凭证保护,但分别覆盖不同执行层: openclaw 拦挂载源加环境变量名正则  hermes-agent 从provider注册表推导环境变量剥离  pi-mono的扩展 denyRead三条加denyWrite四条 这些机制只能保护经过对应层的调用。OpenProgram必须记录未受管执行面,不能用某一个调用点的deny清单代表全局覆盖。 OpenProgram当前状态 已知凭证路径被拒绝 受管shell已验证 环境变量白名单 未知变量不进入子进程 文件工具路径受管 写入根与hard floor 嵌套CLI工具受管 内置副作用工具已禁用 执行面台账已分类 U01—U23有边界与测试 当前受管shell拒绝读取~/.openprogram/auth/*/default.json;文件工具与嵌套CLI分别执行路径和协议层约束。Linux中段glob限制单独记录。
LAYER 3

实施决策与记录

沙箱批次S先完成第04步subagent bypass与第05A步cron触发隔离和U01—U23分类;随后完成身份批次I、第05B步cron创建授权、第06步嵌套agent、第07步审批联动与共享记忆pending/trusted跨scope约束;第08步将新安装默认值改为workspace-write。下面保留各步的参考来源、依赖和验收条件。

09缺口对应到谁解决了

左列取自层一的实测结果,中列是层二里已有的做法,右列是它落在修复路线的哪一步。

缺口 → 谁解决了 → 我们哪一步补 图中保留七条参考机制;OpenProgram特有的执行面缺口列在图后。 层一:我们的缺口 层二:谁解决了,怎么解决 层三:我们的步骤 整盘可读,连deny引擎都没有 SSH私钥和auth目录实测可读 openclaw 挂载源deny hermes-agent 环境剥离 pi-mono扩展 denyRead 八家里三家预置拒绝清单;最先调研的两家默认清单为空 STEP 02 环境变量全量继承 子进程看到164字符完整的API key hermes-agent 从provider注册表推导屏蔽名单 增加openclaw的敏感变量名正则;Linux还需要 --unshare-pid STEP 02 开关在三个边界上丢失 Web路径和agentic function里都是关的 codex-cli 每次exec重建argv openclaw 每个调用点解析策略 两家都不依赖隐式上下文继承,策略是显式参数 STEP 03 不可用时静默放行 用户以为已启用,命令仍无沙箱执行 openclaw 强制拒绝并提供处理提示,另有doctor预检 claude-code的源码把静默那一版直接称为security footgun STEP 03 bypass在自动危险检查之前短路 子agent的命令与文件工具都受影响 hermes-agent 把hardline列表放在所有bypass之下 配套还有按上下文的默认值:cron默认deny,subagent自动deny STEP 04 持久allow过宽,命令规则又按原文前缀匹配 一次批准放开整类工具;wrapper与Unicode未规范化 openclaw argv解包加透明性判定 hermes-agent NFKC加剥ANSI 只让透明的简单argv生成持久规则,复杂shell文本不保存 STEP 07 嵌套CLI文件工具不经过wrap_command Read / Write / Edit 在CLI进程内执行 openclaw 拦掉嵌套agent的 command/ 和 fs/ 方法并注入替代工具 替代工具把文件与命令请求交给宿主策略执行 STEP 06 嵌套agent受管工具
图外的OpenProgram执行面不能直接采用的处理采用的处理步骤
write/edit/apply_patch把所有文件操作改写成shell共享路径规范化与写入根检查;禁止模型可写目录被自动导入04
execute_code继续直接subprocess.run本地路径复用LocalBackend.run及其_invocation和SandboxPolicy05A
cron direct / prompt让paired渠道直接保存job,或触发时尝试弹交互审批05A先完成触发隔离、固定执行spec与ask→deny;身份批次I完成后,05B再让创建检查schedule.create,修改、重排或删除检查schedule.manage,paired渠道在持久化变更前deny05A / 05B
U01—U23执行面清单用“其他执行点”概括或全部机械包装逐项记录来源类、强制策略、保留理由与测试;模型可影响项全部是默认开启阻断项05A
嵌套Claude Code CLI把需要API网络的整个CLI放入无网沙箱协议层拒绝内置文件/命令工具,注入OpenProgram受管替代工具06

10修复路线

下列路线已经按依赖顺序实施:第04步先修复subagent bypass;第05A步完成cron触发隔离和U01—U23执行面;authority批次显式传播owner/paired档位;第05B、06、07步分别完成cron授权、嵌套agent受管工具和审批联动;第08步完成默认值切换。保留各步的设计与验收文字,作为实现记录。

STEP 01 · 已完成

可用性

macOS放行五个字符设备的读写;exec改成不限制,由子进程继承同一profile;Linux把--tmpfs /tmp放在cwd bind之前。
实测结果:git、python3、make、clang、conda python、/usr/sbin2>/dev/null全部正常;/tmp下的工作目录保持可见。pstop因setuid平台限制不能执行。
参考:codex-cli的字符设备规则;两个系统调用级实现都允许exec
STEP 02 · 已完成

凭证屏蔽

两个平台预置deny-read清单,macOS上deny读和deny删除成对设置。子进程环境变量采用白名单,未列出的变量不传入;名称正则作为pass_env的强制限制。Linux增加--unshare-pid。文件工具对agentics目录和agentic源注册表的禁写在策略之前判定,无条件成立;沙箱内命令的同一禁写由策略提供,danger-full-access下的shell面按该模式定义不设防。
实测结果:SSH私钥、~/.openprogram/auth/**、keychain等具体路径不可读且不可删除;macOS的**/.env规则有效,Linux不具备同等glob表达力。164字符的OPENAI_API_KEY在子进程里是空串;Linux上宿主进程的/proc/<pid>/environ不可读,且不能向宿主PID发送信号。git hook和git config保持opt-in,因为禁止后会导致git initgit clone失败。
参考:hermes-agent的provider注册表;openclaw的敏感变量名正则;claude-code的写保护范围
STEP 03 · 已完成

开关语义

移除ContextVar。策略在包装命令时从配置中的sandbox.*解析,不依赖执行线程或调用链。SETTINGS增加七个键。后端可用性按实际隔离能力探测;工具缺失或所需namespace无法创建时默认拒绝并给出明确原因。
实测结果:Web那条形状里裸线程被包住、spawn子进程被包住、审批bypass也照样被包住。wrap_command接收显式策略,但目前还没有按工具或按调用点给出不同策略的地方。
参考:codex-cli的per-command策略;openclaw的拒绝信息与doctor预检
STEP 04 · 已完成

subagent bypass之前执行强制约束

把共享强制约束放在permission_mode="bypass"判断之前,或在已经位于审批包装器外的tool.before层安装不可卸载的生产策略。该步骤不等待principal或scope字段;字段完成后,subagent只能继承caller scope的子集。subagent没有交互审批通道,命中危险命令、工作区外写入或ask规则时直接deny。
验收:构造真实source=agent_spawnpermission_mode=bypass轮次,分别调用bash、execute_code、process、write、edit和apply_patch;危险命令与越界写入在工具执行前被拒绝,安全只读调用仍可执行。测试必须证明默认生产安装点存在,不能只测试可选gate API。
参考:hermes-agent的强制规则和非交互默认值;opencode的per-resource规则
STEP 05A · 已完成

cron触发隔离与U01—U23执行面

先处理U04/U05的执行端:现有direct job把command、cwd、SandboxPolicy与hash固化为不可变执行spec,触发时强制沙箱;prompt job显式使用source=cron和固定非交互策略,内部ask与升级均deny。该部分不判断创建者身份。随后按第04节清单逐项处理U03—U09与U12;execute_code本地路径复用LocalBackend.run及其_invocation,模型可写目录不允许watcher或Web重载器自动导入。
验收:U01—U23每项都有已实现策略或明确保留理由与回归测试;U03—U09、U12没有“未分类”或“仅记录”状态。cron direct与prompt均由真实worker触发,验证固定执行spec、强制沙箱、非交互deny和日志中的policy hash;测试不依赖principal或authority scope字段。
复用:现有LocalBackend与SandboxPolicy;参考hermes-agent的cron默认deny
STEP 06 · 已完成

嵌套agent受管工具

Claude Code CLI继续保留API网络,但通过allowed_tools/can_use_tool拒绝内置Read、Write、Edit、Grep、Glob与命令工具;注入OpenProgram MCP替代工具,由宿主执行文件与命令策略。
验收:嵌套agent不能直接调用未受管的文件或命令方法;替代工具产生与主agent一致的路径检查、沙箱事件和审计记录。
参考:openclaw对嵌套app-server方法分类并注入受管替代工具
STEP 05B · 已完成

cron创建与管理授权

把cron创建、修改、重排和删除定义为宿主副作用;创建映射到schedule.create,修改、重排和删除映射到schedule.managepaired档的固定常量表不含这两项,必须在任何job持久化变更前deny。只有认证本地owner的精确批准可以生成新的不可变job authority;其中固化principal_id、creator speaker审计值、authority_tier=owner、command或prompt、cwd、SandboxPolicy与hash。触发时只能读取该记录,不能重算为interactive或改变权限档。
验收:用paired渠道成员B验证创建、修改、重排和删除均在持久化状态改变前deny;用认证本地owner批准一个精确job,证明未批准字段不能改变,direct与prompt触发时均无审批升级。
依赖:owner principal、渠道配对状态和显式序列化的权限档字段
STEP 07 · 已完成

authority tier、审批与精确升级

认证本地Web/CLI/TUI构造owner+interactive;只有通过稳定账号ID配对的渠道请求才能构造paired,陌生账号不进入agent。continuation、Task、inbox、subagent和cron显式序列化同一权限档。每个工具操作先映射到capability并查询写死的档位常量表;该档不含能力时先deny。只有带approval.request的认证本地owner轮次可申请一次性、精确到operation/cwd/env/policy的能力,paired channel、cron和subagent不得升级。审批表达owner同意,SandboxPolicy表达执行约束,hard constraints不可批准取消。workspace-write仍可能修改或删除仓库文件,不能因为命令在沙箱内就统一免审批。沙箱违规必须产生结构化事件;“Always allow”保存规范化后的精确操作或受限pattern,不能只保存工具名。
验收:同一操作分别从本地owner、已配对渠道、subagent和cron发起,证明权限档判定发生在permission rule与执行之前;Web、CLI、TUI构造owner档,陌生账号收到配对码且不进入agent,内部恢复缺字段直接deny。普通非零退出不会被误判为沙箱拒绝;cron/subagent的ask与升级均为deny。命令匹配前移除ANSI/NUL并做NFKC,解析env包装;只有简单argv可保存pattern,复杂shell文本只允许单次批准。一次bash批准不能授权后续任意bash。任何设置或批准都不能取消hard constraints。
参考:codex-cli的违规事件;openclaw的argv透明性;hermes-agent的规范化
STEP 08 · 已完成

新安装默认开启

第04步、05A、身份批次I、05B、第06和第07步,以及记忆跨scope约束与U01—U23清单全部通过后,macOS与Linux新安装在后端可用时默认workspace-write。保留现有用户明确设置;迁移只提示,不覆盖danger-full-access。Windows没有宿主原生后端,沙箱启用时默认拒绝,不能显示为已启用后直接在宿主执行。
验收:不能只依赖单元测试。用全新profile和真实子进程,在macOS与Linux分别执行下面的命令矩阵,并验证退出码、产物位置、网络结果、凭证拒读和工作区外拒写;main、subagent、cron、execute_code、文件工具和嵌套agent还要分别执行端到端策略用例。另用“共享成员写入pending记忆→owner说按之前定的执行”验证该记录不会进入owner自动上下文;显式召回后scope取交集,只有owner提升或精确审批才能增加能力。
参考:codex-cli的新会话默认;openclaw默认off只记录其产品选择,不作为OpenProgram默认值依据
默认开启真实命令矩阵必须实际执行的代表命令通过条件
Gitgit init、本地git clonestatusdiffaddcommit仓库内产物正确;hook与config行为符合显式策略;无工作区外写入
Pythonpython -m venv .venvpython -m pytest、脚本读写/tmp与cwd常规项目可运行;凭证路径、宿主进程环境和工作区外目标仍被拒绝
npmnpm ci --offlinenpm testnpm run build本地依赖和构建正常;需要公网的安装产生明确网络拒绝或精确审批,不允许静默宿主执行
makemake及包含编译器、子shell和临时文件的fixture构建成功,子进程继续受同一profile约束
condamacOS执行conda info --json;Linux使用只读base cache执行conda create --clone ... --offline -p ./envconda run工作区内环境可创建和使用;HOME、env和可写cache显式位于工作区,全局位置保持只读
路径与平台普通cwd、带空格cwd、/tmp cwd;macOS Seatbelt与Linux bubblewrap各一轮结果一致;已知ps/top与Linux中段glob限制单独报告,不以跳过表示通过
默认开启的两个准入条件已满足:安全准入中的第04步、05A、身份批次I、05B、第06和第07步、共享记忆pending/trusted边界及U01—U23清单均已实现;可用性准入已在macOS Seatbelt和Linux bubblewrap真实子进程上执行上述矩阵。平台固有限制仍按第11节单独报告。
配套的profile修补清单已完成:工作目录拼进profile前先转义;放开exec之前补上限定在同沙箱内的signalprocess-info*; Linux加--new-session --die-with-parent --unshare-pid --unshare-ipc --unshare-uts --cap-drop ALLfunctions/agentics/禁写;两条沙箱路径统一用/bin/bash; macOS的sysctl-read已收窄到硬件前缀和四个uname名字,通用mach-lookup已删除,/private/var/folders已限制到本进程TMPDIR--unshare-user不在清单里:非setuid的bubblewrap构建自己就会建用户命名空间,setuid的构建不接受这个参数。
memory写入器有两个执行面:OpenProgram进程内的MCP shell使用暂存目录和同一份SandboxPolicy; Claude Code CLI继续在外部进程访问Anthropic API,但其内置Read/Write/Edit/Grep/Glob与命令工具已由协议配置禁用。 文件和命令操作改用OpenProgram提供的受管MCP工具,因此副作用回到宿主的路径、scope、审批和沙箱判定。

11实现状态

记录日期:2026-08-10。第01—08步与身份批次I均已实现;新安装默认workspace-write,已有显式danger-full-access不被迁移覆盖。状态以当前代码、完整单元测试和双平台真实子进程矩阵为准。

步骤状态实现与验证结果剩余限制 / 记录
01 可用性已完成常用编译、Git、Python与/tmp工作区已验证;Linux会执行namespace能力探测macOS ps/top限制保留
02 凭证屏蔽已完成默认deny-read、环境白名单、Linux pid隔离已验证Linux中段glob不能声明为已强制
03 开关语义已完成Web线程、spawn与bypass bash都能读取有效配置;不可用默认refuse当前仍为全局策略,没有per-tool覆盖
04 subagent强制约束已完成hard constraint和capability检查位于bypass之前;bash/process/execute_code及文件工具的越界操作在执行前拒绝subagent始终非交互,不能申请升级
05A cron触发与执行面已完成cron保存签名不可变执行spec,worker强制sandbox并记录policy hash;execute_code、U03—U09和U12均已处理U01/U02使用配置的Docker或SSH执行后端自身边界,不计为宿主原生沙箱
06 嵌套agent已完成Claude Code内置副作用工具被禁用,文件与命令改用OpenProgram受管MCP工具CLI的API网络仍保留在外部进程
I authority scope依赖已完成owner principal、speaker、interaction和capability集合显式进入TurnRequest、Task、inbox、continuation、subagent与spawn snapshot恢复缺字段时对宿主副作用deny,不使用ContextVar补值
05B cron创建与管理已完成创建消费schedule.create,修改/重排/删除消费schedule.manage;job capability固化owner、scope、policy与内容hashpaired渠道在持久化改变前deny
07 审批联动已完成scope先判定;违规结构化;只有本地interactive owner可精确单次升级;Always allow精确到规范化operationhard floor与凭证环境过滤不能被批准取消
08 默认开启已完成sandbox.mode默认workspace-write;macOS与Linux的git/python/npm/make/conda及拒绝矩阵通过保留显式danger-full-access;未知平台默认refuse
PLATFORM LIMIT · macOS

setuid可执行文件

现状
pstop不能在Seatbelt profile内执行。
处理
记录为平台能力限制,不为它放宽整个profile;需要时由受管宿主工具提供最小进程信息。
PLATFORM LIMIT · Linux

中段glob禁读

现状
bubblewrap按具体路径遮蔽,**/.env这类中段glob不能直接强制;该规则只在macOS正则profile中有效。
处理
敏感内容使用精确路径,或/absolute/path/to/secrets/**这类具有确定前缀的目录级deny。不能依赖**/.env声明Linux已覆盖。
其他已知限制当前行为范围决定
Windows没有宿主原生沙箱后端;沙箱开启时默认拒绝执行。只有owner显式关闭沙箱或选择不安全的unavailable_policy=warn才会改变该行为不自动选择Docker,不把普通宿主执行显示为已启用沙箱
per-tool覆盖沙箱策略当前以安装为粒度;工具仍分别执行authority、权限规则、审批与hard constraints本批次不增加per-tool SandboxPolicy覆盖
CPU / 内存 / 进程数有命令timeout和Linux namespace,但没有资源配额明确不在范围内;PID namespace不能描述为进程数限制
最终验证记录 · 2026-08-10:本机完整受跟踪测试集(排除integration)为2731 passed、4 skipped、1 xfailed;GitHub Actions run 31398444213的Python 3.11、3.12、3.13、Web、文档和示例job全部通过,其中Linux Python 3.11为2723 passed、12 skipped、1 xfailed。Linux runner先启用Ubuntu 24.04的非特权user namespace能力,再执行真实cron bubblewrap用例;安装但无法工作的二进制不会被计入覆盖。 macOS在真实LocalBackend + sandbox-exec下通过git clone/status/diff/add/commit、Python venv、npm ci offline/test/build、make/clang、conda info和uname;.env、工作区外写入、网络与剪贴板访问均被拒绝。 Linux在一次性Docker Linux内核环境中执行真实bubblewrap,通过git、Python venv、npm、make、使用只读base cache的工作区conda clone/run,以及凭证拒读、工作区外拒写和网络拒绝;容器随后删除。Docker只用于Linux分支验证,不是OpenProgram沙箱实现。