Sandbox、Filesystem 与观察策略
进程和文件如何共享一个执行世界与根目录策略
结论:这里没有一个万能的“沙箱”,而是四条需要同时成立、却彼此独立的策略链
DeepSeek Harness 把安全与一致性拆成四层:进程 argv confinement 限制 shell 的文件写副作用;Filesystem provider 定义路径身份、读取与原子发布;进程内写围栏按 sandbox mode 拒绝越界文件 mutation;observation policy要求 Session 在修改前见过目标及其版本。权限预设与升级审批只负责选择更宽的 mode,不能替代上述任何一层。这个分解很清晰,但名称容易制造过强直觉:read-only 不是“不可读取秘密”,workspace-write 也不是网络、进程、设备或凭据隔离。SANDBOX-SEAMSANDBOX-DOC-BOUNDARY
shipped base composition 把这些层正确拼在一起,但核心 service 并不证明它们必然来自同一安全世界。自定义 composition、共享临时目录、进程外写入者、符号链接竞态和远端进程身份复用,仍是部署者必须理解的剩余边界。
1. 先分清四种 contract
| 层 | 回答的问题 | 不回答的问题 |
|---|---|---|
SandboxProvider | 这组精确 argv 应如何被 runner 包装,写文件时允许到哪里 | 命令能读什么、能否联网、能看见哪些进程 |
FileSystem | 路径如何解析、目标如何定址、文件如何读写与发布 | 默认不承诺 root containment,也不承诺读前见过 |
fs-sandbox | 一次 write/edit 的目标是否位于当前 mode 的 writable roots | 不围住 read,也不是内核级任意代码边界 |
fs-observation-policy | 当前 Session 是否观察过“存在/缺席 + 版本”,修改前是否仍匹配 | 不是身份授权、路径授权,也不证明看过完整文件 |
Sandbox mode 只有 read-only、workspace-write、danger-full-access 三个值,而且生产类型明确把它定义为“file-effect confinement”。provider 返回的不只是 argv,还返回 full/partial enforcement、拒绝签名和 runner failure 规则;调用方必须执行返回的 argv,不能静默回退到原命令。SANDBOX-SEAM
2. 默认值有两层:service 默认只读,shipped base 默认 workspace-write
sandbox-policy 自身的配置默认是 read-only;base bundle 则显式把部署默认覆盖为 workspace-write,并把 approval policy 设为 ask。因此缺省 service 实例和 shipped 产品组合得到不同结果,两者都是源码事实。SANDBOX-SHIPPED-POLICYSANDBOX-POLICY
每次调用的有效策略按固定优先级解析:已获批的显式 mode → Session event 中最后一个 mode → 部署默认。policy root 来自当前 Session cwd;没有 Session 时回退到已规范化的部署 root。workspace-write 的可写集合是 workspace root、固定 /tmp 和平台 os.tmpdir(),经过 realpath 尽力规范化与去重;read-only 没有 writable root。SANDBOX-WRITABLE-ROOTS
3. Mode 是 Session 事件,不是 shell 进程上的可变全局变量
deployment default
↓
last sandbox/mode event in this Session
↓
approved per-call override (one execution only)
↓
SandboxExecutionPolicy { mode, root, sessionId }
Session mode 从 sandbox/mode 事件 fold 得出;唯一正式写入口也只是追加该事件。这样 resume/replay 能重建 permission 状态,而一次性批准不会偷偷变成 Session 的持久默认。SANDBOX-MODE-EVENT
4. 升级审批是一条严格单调的 pre-execution 流程
调用只能从只读升级到 workspace/full,或从 workspace 升级到 full;相同或更窄 mode 都不是合法“升级”。sandbox_permissions 与 justification 必须成对出现,审批发生在任何执行之前,并把取消 signal 传给 approval service。只有 allowed-once 返回更宽的单次 policy;拒绝、取消和 unavailable 都不会执行原命令。SANDBOX-ESCALATION
approval 回答“这一次是否允许扩大边界”,sandbox provider 回答“扩大后的边界如何落实”。把前者当成后者,会在 approval service 缺失、runner 不可用或 partial enforcement 时得出错误安全结论。
5. Local backend 是平台方言链,不是一个跨平台同构实现
| 平台 | 候选链 / profile | enforcement | 关键差异 |
|---|---|---|---|
| Linux | bwrap → Landlock | bwrap full;Landlock 可为 partial | bwrap 把 / 只读 bind,并给 workspace mode 一个临时 /tmp;Landlock 允许写 host temp 与 workspace |
| macOS | Seatbelt | 声明 full | 默认 allow,显式 deny file-write,再为 writable roots 放行写入 |
| Windows | ACL helper | 始终 partial | 通过受限 SID/ACE 与临时目录物化能力,无法消除 Everyone/hard-link 等旁路 |
Linux 多候选时会 probe 并缓存 verdict,全部失败就结构化 fail closed;单一平台候选直接选取。每种方言还有独立 denial 与 runner-failure 签名,供调用方区分“命令失败”“被沙箱拒绝”和“沙箱根本不可用”。SANDBOX-LOCAL-DIALECTSSANDBOX-PROFILESSANDBOX-LOCAL-SELECT
6. 自定义 runner 是 operator trust boundary;stderr 分类只是诊断
配置非空 runnerCommand 会跳过平台探测,并把该 runner 视为 full enforcement。框架验证命令形状与 failure signatures,但无法证明外部程序真的实现了声明的 confinement;这是明确的部署信任输入。SANDBOX-LOCAL-WRAP
一次性 shell 会执行 provider 返回的精确 argv,记录 enforcement,并依据 exit code、spawn attribution 和 stderr signatures 分类 sandbox denial/runner failure。因为分类依赖带内 stderr,子进程可以输出相同文本制造错误归因;这会影响可用性与诊断,不会让它逃过已经施加的 confinement。SANDBOX-BASH-CONSUMERSANDBOX-RESULT-CLASSIFIER
7. Windows ACL 是诚实标注的 partial capability
workspace root 的受限 ACE 是 standing grant;Session temp 则建立随机 SID 与随机目录,失败时回滚半物化状态。dispose 会尽力撤销临时 grant 和目录,但 workspace ACE 保留,进程崩溃也可能留下 residue。实现因此明确报告 partial,而非用“沙箱”一词掩盖 Everyone ACE、hard-link 与 cleanup 的限制。SANDBOX-WINDOWS-CAPS
8. Filesystem seam 把“显示路径”和“目标身份”分开
FileSystem 使用 opaque target、opaque version、存在/缺席 observation、guarded create/replace intent,以及结构化 FsError。write/edit intent 是 single-slot waterfall,observed 是同步 recorder;provider 可选暴露 sandboxMode,这决定工具是否能宣传 mutation escalation。SANDBOX-FS-TYPESSANDBOX-FS-SEAM
local provider 的 cwd 只负责相对路径解析,绝不是 containment boundary。它把人类可读绝对路径与 canonical target key 分开:存在目标用 native realpath;不存在目标从最近存在祖先开始规范化,再拼回缺失后缀,从而让别名与多数 symlink 路径共享锁和 observation 身份。SANDBOX-FS-LOCALSANDBOX-FS-IDENTITY
9. 读取路径分层,发布原子,但 replace guard 不是跨进程 CAS
local read 区分 whole-file UTF-8、bounded raw bytes 与 stream;二进制探测只采样前 8192 bytes,raw-byte read 有明确上限,stream 接受取消。底层 readText 本身没有 maxBytes,因此“所有长读取都天然有界”并不是 provider 保证;模型工具会另行选择 window 或 stream 路径。SANDBOX-FS-READ-IO
mutation 在每个 canonical target 的 provider 内锁中完成:先核对 create/replace guard,再在 mode 0700 的 sibling 临时目录里写 mode 0600 文件并 sync,最后通过 hard-link no-replace create、Windows ReplaceFile 或 rename 发布。取消在 commit 前再次检查;commit 后 cleanup 仅 best effort。SANDBOX-FS-LOCAL-MUTATIONSANDBOX-FS-ATOMIC
createIfAbsent 在发布点用 no-replace 原语,保证较强;replaceIfVersion 的版本检查却发生在 staging/rename 之前,且锁只覆盖当前 provider 进程。另一个进程可在检查后、发布前写入并被覆盖。因此它是“同一 provider 内串行 + 发布前元数据检查”,不是跨进程线性化 CAS。官方限制也承认 metadata version 与 process-local lock 的边界。SANDBOX-FS-LOCAL-LIMITS
10. fs-sandbox 是 mutation-only 路径围栏
fs-sandbox 继承 local filesystem:read/list 原样保留,只有 write/edit 在委托前检查 mode。只读拒绝所有 mutation;workspace mode 在调用时重新 resolve 目标,并要求目标落在 canonical writable roots 内;full 直接通过。它明确定位为 trusted in-process provider / model path 的纵深防御,而非约束任意原生代码的内核边界。SANDBOX-FS-FENCE
containment 同时做 separator-aware lexical check 与存在 root/target 的 file identity fallback,处理 Windows alias/case。测试覆盖绝对路径、..、已存在 symlink escape、symlink 下新文件、stale target 与 per-call override。SANDBOX-CONTAINMENTSANDBOX-FS-FENCE-TEST
11. read / write / edit 的真实控制流
read(path)
resolve from Session cwd → stat/read bounded window → emit present/absent observation
write/edit(path, optional escalation)
resolve requested policy → approval if wider → resolve from policy root
→ fs write/edit intent waterfall → provider guard + mutation
→ map structured denial → emit new present observation
Session cwd 会在解析时重新 canonicalize,以免 symlink-sensitive .. 继承陈旧字符串;sandbox policy root 优先于普通 cwd。read 成功后记录 present,确认 ENOENT 时记录 absent。write 与 edit 都在真正 I/O 前取得 policy 并触发 intent,只有成功后才刷新 observation。SANDBOX-SESSION-CWDSANDBOX-READ-TOOLSANDBOX-READ-TARGETSANDBOX-WRITE-TOOLSANDBOX-EDIT-TOOL
只有 mounted fs provider 声明 sandboxMode 时,工具 schema 才接受升级字段;结构化 FS_SANDBOX_DENIED 会被映射为共享 denial marker 与提示。bare local fs 和默认 E2B fs 不伪装成可升级的本地沙箱。SANDBOX-FS-ESCALATION
12. Observation policy 是按 Session 对象和 canonical target 的易失状态机
| 最近 observation | write intent | edit intent |
|---|---|---|
| 无 | 拒绝覆盖;不能证明目标缺席 | 拒绝 |
| absent | createIfAbsent | 拒绝 |
| present(version) | replaceIfVersion(version) | replaceIfVersion(version) |
状态存于 WeakMap<Session object, Map<targetKey, observation>>;Session owner 被垃圾回收后,弱引用条目可随之释放。源码没有 Session-dispose hook,显式重置整张表发生在 policy plugin dispose/HMR。成功的 read/write/edit 会刷新状态;没有 Session owner 的 mutation 也不能借用别人的 observation。SANDBOX-OBSERVATION
write/edit intent 是 first-listener-wins 的 waterfall,注册顺序不属于稳定 contract;卸载 policy 后 mutation 会退化为 provider 的无条件调用。生产测试固定了 present/absent 决策、owner 隔离、首 listener 胜出,以及 policy plugin disposal 后状态重置。SANDBOX-OBSERVATION-DOCSANDBOX-OBSERVATION-TEST
13. “Read before edit” 精确保证 freshness,不保证 completeness 或 authorization
第一,observation 不持久化,resume 后必须重新读;第二,绕过 tool 直接调用 ctx.fs.read* 不会自动产生 observation;第三,只读了一个 window 也足以授权同版本的 whole-file overwrite。它证明“调用者见过这个目标的某个当前版本”,不证明调用者看过全文件,更不证明其有权访问路径。SANDBOX-OBSERVATION-LIMITS
因此正确组合顺序是:身份/部署授权决定能否拥有 capability,sandbox 决定能写哪里,observation guard 防止基于陈旧或从未见过的状态覆盖,atomic publication 决定落盘提交语义。四者不能互相代偿。
14. Permission preset 同步三项状态,但只验证 shell confinement
permission preset 需要 confining ctx.shell 与 approval service;shipped presets 对应 workspace-write + ask 与 danger-full-access + never。用户切换时会同时写 permission/preset、sandbox/mode 和 approval/policy,并为新 Session pin 初始状态。SANDBOX-PERMISSION-PRESETSSANDBOX-PERMISSION-WRITE
preset service 只检查 shell 是否 confining,并不证明 ctx.fs 也由 fs-sandbox 提供、使用同一 policy。shipped base 确实同时装了 observation、tool-fs 与 fs-sandbox,所以当前基线一致;自定义 composition 却可能展示“workspace-write”权限,同时把 write/edit 接到 bare filesystem。跨 capability 的 same-policy invariant 由部署组合承担,而不是由类型或启动检查强制。SANDBOX-SHIPPED-FSSANDBOX-SHIPPED-FS-PROVIDER
15. Persistent terminal 继承 confinement,却丢失部分结构化观察面
terminal provider 在 PTY 创建/活动期间禁止更改 Session sandbox mode,避免一个长寿命 shell 跨越权限边界。创建时按 Session 解析 policy、把 shell argv 交给 sandbox provider 包装,并在 init failure 时清理。SANDBOX-TERMINAL-FENCE
但 terminal 只使用包装后的 argv,没有像 one-shot shell 那样把 enforcement、diagnostic dialect 和 runner classification 带到每次结果中。runner 拒绝仍会 fail,但 Host 对 persistent PTY 的结构化“为何失败/partial enforcement”可见性更弱。这是 observation surface 差异,不是 confinement 绕过。
16. E2B 不是 SandboxProvider 的另一个 runner,而是整套执行世界替换
E2B owner 在一个 Context 内持有一个 remote sandbox handle,验证 API key 与 absolute Linux cwd,创建 mode 0700 的 runtime root,并在 bootstrap 失败时 kill 回滚。SDK 不可避免的 login shell 被赋予随机 HOME 以降低启动文件副作用。这个 ownership scope 意味着:同一 Context 下多个 Session 若共用该 owner,也共用远端 filesystem/process 世界;Session 本身不是 VM 隔离单位。SANDBOX-E2B-OWNER
remote fs 在远端用 GNU realpath 建立 canonical identity,提供 bounded raw-byte read、process-local target locks、guard check 和原子 staging/publish;readText 则调用 SDK 的整文件读取。publication 开始后不再把取消 signal 传入 link/rename,刻意避免“调用方收到 canceled,却不知道提交是否发生”的模糊状态。SANDBOX-E2B-FSSANDBOX-E2B-PUBLICATION
remote subprocess 也在同一个 sandbox 中解析 executable、管理进程组与 terminal;group signaling 仍有 numeric PGID reuse 的已知竞态。官方限制还明确:没有 host 文件同步、路径 reopen 有竞态、锁只在当前进程。SANDBOX-E2B-SUBPROCESSSANDBOX-E2B-PGIDSANDBOX-E2B-LIMITS
17. Remote composition 的核心不变量是“能力同世界”
headless E2B 示例明确禁用 local fs 与 local subprocess,再同时装 E2B owner、remote fs 和 remote subprocess;内层 sandbox policy 设为 full,因为真正边界是远端机器,而不是 host 上的本地 runner。相同路径字符串不会上传 host 文件,所有输入必须经显式 API/文件写入进入远端。SANDBOX-E2B-COMPOSITION
还有一个容易漏掉的 seam:glob/grep 不经 ctx.fs,而是用 ctx.subprocess 执行 host package 解析出的 ripgrep 绝对路径,并假设 workdir 与可读路径处于同一世界。自定义 remote adapter 若看不见该 host binary,搜索就不能仅靠替换 fs provider 工作;composition 必须同时解决 executable 与 filesystem 共址。SANDBOX-SEARCH-WORLD
18. 取消、失败与 cleanup:不同 commit point 有不同答案
| 路径 | 取消边界 | 失败语义 | 残留风险 |
|---|---|---|---|
| Approval | 执行前,可直接取消 | 未获批绝不执行 | 无 mutation |
| Local shell | 传给 runner/subprocess | spawn、denial、runner failure 分型 | 子进程自己的外部副作用取决于终止时点 |
| Local fs | commit 前最后检查 | commit 后 cleanup failure 不回滚已发布文件 | sibling staging residue 可 best-effort 清理 |
| E2B fs | publication 前可取消;commit 原语不取消 | 避免 ambiguous canceled commit | 远端 staging cleanup 仍是 best effort |
| Windows ACL | grant/revoke 各自有回滚路径 | 半物化失败尽力撤销 | 崩溃可留下 SID/ACE/temp residue |
| E2B process group | signal/kill ladder | 远端 numeric identity 驱动 | PGID reuse 小窗口 |
Filesystem API 没有通用超时参数;bounded raw-byte reads、streaming 与 AbortSignal 是主要资源控制。这里的设计优先保证 commit point 可解释,而不是承诺任何时刻的“瞬间取消”。
19. 静态安全审计:已确认的边界与风险
| 级别 | 发现 | 影响 / 正确处理 |
|---|---|---|
| 边界 | 三种 mode 只约束文件写副作用 | 不能据此声称网络、读取、进程、设备或凭据隔离 |
| 中 | Landlock/Seatbelt/fs fence 共享 host temp | 同 OS 用户的 Session 可发生临时名冲突和相互干扰;敏感并行任务应使用隔离执行世界 |
| 中 | fs fence 明确接受 missing-ancestor symlink TOCTOU | trusted/model path 的纵深防御,不应用作强对抗边界 |
| 中 | permission preset 不验证 fs/shell same-policy | shipped 组合无此错配;自定义组合需额外启动审计 |
| 中 | replace version guard 非跨进程 CAS | 外部写入者可在检查和 publish 之间被覆盖 |
| 低 | observation first-wins、可卸载、window read 可授权 whole-file write | 它是 freshness guard,不是授权链或内容完整性证明 |
| 低 | runner/denial 依赖 stderr signatures | 可伪造诊断归因,不能由此逃逸 confinement |
| 部署信任 | custom runner 不 probe 即声明 full | operator 必须独立验证 runner,不能把配置值当证明 |
| 低 | Windows cleanup residue 与 E2B PGID reuse | 隔离是 partial / best effort,监控和生命周期回收仍需要部署兜底 |
20. 文档漂移:两个具体、可修复的不一致
| 文档 | 当前说法 | 源码事实 |
|---|---|---|
packages/fs/fs/README.md | 列举 FsErrorCode 时遗漏 sandbox denial | 类型表已包含 FS_SANDBOX_DENIED |
packages/sandbox/sandbox/README.md | 实现只概括 Linux/macOS,consumer 只提 bash | 生产实现已有 Windows ACL、PowerShell 路径和 filesystem sandbox consumer |
第一处会让 provider/consumer 作者漏掉结构化错误分支;第二处会让架构读者低估当前平台面与 fs policy 的存在。SANDBOX-DOC-ERROR-CODESANDBOX-DOC-BACKENDS
21. 验证覆盖与本章边界
生产测试已覆盖 local runner profiles、custom runner、候选 fallback/fail-closed;fs fence 的三种 mode、.. 与 symlink escape;observation 的 absent/present、Session owner 隔离、first-wins 和 disposal。SANDBOX-LOCAL-TESTSANDBOX-FS-FENCE-TESTSANDBOX-OBSERVATION-TEST
本章基于固定 commit 的生产源码、shipped composition、README 与现有测试做静态控制流核对,并逐项区分源码保证、官方限制与本章 assessment。本次交付运行了覆盖审批、preset、扩权、文件、进程、Job 与 Terminal 的 15 个 targeted Vitest 文件:508 项通过,1 项跳过;这验证现有行为测试保持绿色,但不把静态安全审计升级为动态穿透测试结论。
最值得保留的是“能力 seam + 单次 policy + guarded mutation + durable Session mode”的分层;最需要补强的是跨 capability composition invariant、跨进程 replace CAS,以及对共享 temp/partial enforcement 的产品级可见性。这个系统的真实安全性来自各层同时成立,而不是来自 sandbox 这个名字。
我的学习体会
内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。