Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
2026-07-24 15:10:55 +08:00

34 KiB
Raw Blame History

Agent Note: 子进程沙箱——约束 seam、原生 runner、升级机制与按会话模式

Status: implemented

English | 中文

问题

一个编码 agent 需要如下产品路径:bash 子进程(以及依附其上的钩子命令)默认在受限的文件沙箱下执行;当且仅当沙箱实际拒绝了某个操作时,模型可以为同一操作请求一次用户批准,获批后以更宽的权限重试一次。本设计刻意不声称覆盖所有工具:fs/web/todo 在进程内执行,execve 包装对它们毫无意义(§ 进程内工具);跨工具族的统一边界属于分阶段后续工作(§ 延迟阶段)。如果没有共享词汇,每个工具都会各自重新发明批准字段、拒绝解析、重试匹配和权限状态提示。

harness 是一个 SDK,因此约束必须是开发者可组合的能力:是否启用沙箱、每个平台使用哪个后端,都应作为一等条目写在叶子 cordis.yml 中,而非藏在某个执行器的私有机制里。而首选 runner bwrap 恰恰在沙箱最重要的主机上不可用(精简容器、禁用了非特权 userns、LSM 拒绝 mount),因此备选 runner 必须随 SDK 一起交付,而不能假设主机已有。

仅有约束还留下两个缺口。拒绝后没有升级路径就是死路:模型只能放弃,这会迫使运维人员全局配置 workspace-write 或 danger-full-access,从而使沙箱形同虚设。而模型可见的旋钮(沙箱模式、批准策略)在 agent 生命周期内会变化——ACP 用户切换按会话设置、运维人员在进程停止期间编辑 cordis.yml——模型绝不能基于过时的信念行动:每次请求时的实际状态是什么、agent 存活期间发生了什么变化、无人看管时又发生了什么变化,都需要有明确答案。

决策

一个 seam、一条按平台的本地后端链、一个消费方,加上两个上层杠杆:按调用的升级路径与按会话的运行时模式。以下所有内容均从叶子 cordis.yml 组合而来;不触及 agent-loop。跨工具族 fs 强制与按会话工作区根目录已经作为后续设计落到同一策略载体上;剩余阶段——subagent-acp 消费方、更多环境与 Windows 链——仍列在 § 延迟阶段。

部署方式

四条 cordis.yml 条目即可将一个无约束的编码 agent 转变为沙箱产品路径;examples/acp-agent 默认使用此组合:

- id: sandbox
  name: '@deepseek-ai/dsh-sandbox-local'   # the per-platform runner provider (ctx.sandbox)
- id: bash
  name: '@deepseek-ai/dsh-bash-sandbox'    # the confined executor, replacing dsh-bash-local behind ctx.bash
  config:
    mode: workspace-write                  # the deployment default every session starts from
    workspaceRoot: !!js process.cwd()      # the boundary workspace-write may write under
- id: approval
  name: '@deepseek-ai/dsh-user-approval'        # the escalation gate's channel (the approval Agent Note)
  config:
    policy: ask
- id: permission
  name: '@deepseek-ai/dsh-permission'      # one product-facing select over both mechanism knobs

这一替换对 ctx.bash 的所有消费方透明:bash 工具、钩子命令和后台任务照常运行,通过提供方返回的包装 argv spawn。删除 sandbox 和 permission 条目、将 bash 替换为 @deepseek-ai/dsh-bash-local 即为退出——执行恢复为无约束,升级字段从工具 schema 中消失,因为它们是基于已挂载执行器的能力门控,而非基于配置。仅省略 approval 则保留约束但以自身错误文本关闭每次升级;permission 还要求 approval seam 和约束执行器同时存在,因此部分组合的 preset 层在加载时即大声失败。

配置错误大声失败:mode 不在封闭词汇中时在插件加载时被拒绝;主机上没有可用后端时在 confine() 阶段(命令 spawn 之前)抛出结构化的 SANDBOX_UNAVAILABLE,而非降级为无约束执行。dsh-sandbox-local 上的 runnerCommand 是运维人员对一个 bwrap 兼容 runner 的显式断言(跳过链和探测);它同时充当 keyless 测试的确定性 fake-runner seam。

被拒绝的文件操作返回 [sandbox: file access denied under <mode> mode] 标记,并附带不要绕过拒绝的指令。约束执行器添加配对的 sandbox_permissions 和 justification 字段,用于一次经批准的重试,该重试必须严格宽于会话的有效模式。授权仅放宽该次重试;拒绝则不执行任何内容,返回 the user rejected escalating this command to "<mode>",且不允许再次请求。提示词不声明沙箱模式,以避免基于常驻标签的预防性拒绝。当 dsh-permission 被组合时,ACP 暴露一个 Permissions 选择器,其 preset 同时写入两个旋钮事件;不匹配的旋钮组合显示为仅可切换离开的 custom。只有切换到确定性的 'never' 批准策略才会在提示词中声明并叙述。

设计细节

范围界定

OS 子进程约束适用于 bash 执行器(包括钩子命令),后续还将适用于 ACP subagent 子进程。文件系统、web 和其他工具在进程内执行,需要在各自的 seam 层面实施策略;argv 包装无法约束一个闭包了 ctx 的函数。既有的 bash request/spec 拆分承载按调用的覆盖,而 tools/pre-execute 和 approval seam 负责人的决策。

seam:ctx.sandbox

dsh-sandbox 拥有词汇和 SandboxProvider 契约:confine(argv, policy) 返回调用方应当 spawn 的替代 argv(经过包装,使进程及其所有子进程在约束下运行),加上所选后端达到的 enforcement 完整度、其拒绝方言(denialSignatures,该后端内核在拒绝文件操作时打印到 stderr 的子串)、以及其 runner 失败方言(runnerFailureSignatures,runner 本身失败——因而命令从未运行——时的自我标识方式);没有可用后端时抛出失败关闭的 SANDBOX_UNAVAILABLE 错误,绝不静默放行。词汇:SandboxMode(read-only / workspace-write / danger-full-access,仅限文件操作——不声称覆盖网络和进程可见性)、SandboxEnforcement(full / partial)、SandboxExecutionPolicy(每次能力调用的完整 mode + workspace root)以及 SandboxPolicy(提供给约束后端的子集)。

策略随每次调用而非提供方携带:两个消费方可以在同一时刻以不同策略约束(bash 在 read-only 下运行,而一个受约束的子 agent 保持其状态目录可写),且经批准的升级重试是一次带有更宽策略的新调用——在配置固定的提供方模式下无法表达。

该 seam 仅约束与宿主机共享文件系统和内核的子进程。容器、microVM 和远程执行器不是此 seam 的后端——它们以环境一致的组替换整个能力实现(ctx.bash、ctx.fs),因为一个 bash 在容器中运行而 fs 工具写主机的 agent 生活在两个割裂的世界中。

留待需要时再决定:网络限制是作为独立的 network_mode 到来,还是在某个 runner 同时强制两者后合并进 sandbox_mode;以及 SandboxPolicy 是现在就增加额外的可写根授权(launcher 已支持 --rw <path>),还是等到升级机制需要时再加。

本地后端与随附 launcher

dsh-sandbox-local 在提供方生命周期内选择一个平台 runner 并缓存结论。Linux 功能性探测 bwrap 然后 Landlock;macOS 使用 Seatbelt。不支持的平台和不可用的 runner 失败关闭。每次包装携带后端特定的拒绝签名和 runner 失败签名,以便 dsh-bash-sandbox 区分被拒绝的文件操作与损坏的沙箱。runnerCommand 作为运维人员对 bwrap 形状 runner 的断言跳过选择,但缺失或不可执行的命令仍被归类为沙箱失败,绝不无约束地运行负载。

launcher 是一个约 300 行的 C 程序(纯 C11,直接使用 Landlock UAPI——除静态链接的 musl 外无其他库,因此审计面仅为该文件加内核的稳定 syscall 契约):--ro <path> / --rw <path> 授权,--,被包装的 argv;它在自身上安装规则集并 exec(规则集跨 execve 继承,且它在限制前设置 no_new_privs);--probe 在一个短生命周期子进程中强制最大规则集,仅当内核确实强制时才以 0 退出;launcher 失败以 125 退出且不 exec。

Landlock launcher 源码和包工作区位于 native/landlock-run,与 harness 消费方同仓。独立的 node-addon-landlock-run 仓库是用于打包并发布 npm 包族的发布镜像;导出流程归 native/README.md 所有。平台二进制由 npm 选择,入口包拥有路径解析、探测和 CLI flag,而 harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。

后端 profile 共享模式契约但在必要的主机授权上有所不同。Landlock 和 Seatbelt 在 read-only 模式下仅允许 /dev/null;workspace-write 还允许各自所需的主机临时目录根。每次包装携带后端特定的拒绝签名。Landlock 在较旧的 ABI 无法管控所有操作时报告 partial enforcement,而成功的 bwrap 和 Seatbelt profile 报告 full enforcement。

bash 消费方

dsh-bash-sandbox 扩展 LocalBashExecutor,并把即将 spawn 的确切 ['bash', '-c', command] argv 交给 ctx.sandbox。拒绝是与其他结果正交的事实,依据当前 runner 的 stderr 方言保守分类。Runner 失败优先于拒绝:前台执行抛出 SANDBOX_UNAVAILABLE;结算后的 BashProcess 会盖章 sandbox.runnerFailed,bash 生产者再通过通用 task_output 渲染它。

模型看到的仅是结果事实:静态工具描述解释拒绝标记([sandbox: file access denied under <mode> mode]),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试;当升级字段被公布时,被拒绝的结果还额外携带升级提示本身,使被认可的同轮次重试在决策点被提示,而非依赖模型回忆描述(§ 升级机制)。没有提示词段落声明沙箱模式(§ 按会话模式)。

升级机制:拒绝后一次经批准的更宽重试

BashExecRequest.sandboxPolicy 是可选的完整按调用输入;解析后的 spec 使该字段显式。BashExecutor.sandboxMode 仍是公布已挂载执行器能否兑现该策略的能力事实,因此只有约束组合才暴露升级。seam 接受任何显式策略;工具拥有会话解析和「仅更宽」的升级规则。非沙箱执行器诚实地保持无约束。

ctx.sandboxPolicy.resolve() 在执行器运行前盖章完整执行策略——显式升级模式 > 会话覆盖 > 配置默认值,且 SessionHeader.cwd > 配置的后备根目录。SandboxBashExecutor.resolve() 在 spec 上保留该策略,或为直接的无 agent 调用方提供部署后备值,使 run()/start() 永不读取可变会话状态。每进程包装事实以返回的 BashProcess 为键;onProcessDone() 在 done 结算前分类 stderr 并给该句柄盖章,因此重叠进程各自保留自己的模式和 runner 方言。

当约束执行器被挂载时,bash 公布配对的 sandbox_permissions 和 justification 字段。schema 暴露完整的封闭升级词汇,因为有效模式是按会话的;执行拒绝任何不严格宽于该调用有效模式的目标。批准在执行之前解析。allowed-once 仅将授权模式盖章到该请求上,而 rejected、cancelled、unavailable、缺失的 approval 服务或缺失的 agent 都以各自不同的结果文本失败关闭。授权不持久化。

升级是对被拒绝命令的同轮次重试,使用最窄的足够 sandbox_permissions 和一个 justification;批准提示是同意步骤。它必须基于实际的拒绝,除非会话已观察到相同的被拒绝访问;禁用或被拒绝的批准终结该命令。重试、批准决策和结果使用既有的工具和批准事件。dsh-tool-bash 拥有请求动作,因为执行器 seam 既没有 agent 也没有用户交互所需的 call id。

仍未决定:持久授权超出沙箱模式之外的作用域标识是什么——确切调用、路径、命令前缀、会话或时间窗口——这是公布 allow_always 选项之前必须回答的问题。

按会话模式:会话日志即存储

effective(session) = findLast(the session's own knob events)?.value ?? the composition-config default

默认值是组合配置(cordis.yml)——运维人员拥有,进程范围。运行时切换是会话范围的覆盖,记录为该会话自身日志中的一条仅日志事件。重启免疫(恢复会话时回放其日志,覆盖自然恢复,无需追赶机制)和多会话隔离(一个编辑器标签页的 workspace-write 不会干扰另一个的 read-only)都是构造性的自然结果,且不存在任何外部配置存储。

每个旋钮一种事件,由其领域拥有——这是每个既有事件族已遵循的可合并扩展 SessionEventMap 惯用法(dsh-user-approval 中的 approval/*、hooks 包中的 hook/*):

interface SessionEventMap {
  'sandbox/mode': { mode: 'read-only' | 'workspace-write' | 'danger-full-access' }
  'approval/policy': { policy: 'ask' | 'never' }
}

每个拥有者导出相同的三件套:事件声明、纯 fold(effectiveSandboxMode(events) / effectiveApprovalPolicy(events)——一个 findLast,类型化到领域的封闭联合),以及唯一的写入路径(setSandboxMode(session, mode) / setApprovalPolicy(session, policy)——切换即其事件;没有任何东西在带外修改状态)。无共享拥有者服务、无通用 facts map、无注册表:第三个旋钮只需将约 40 行模式复制到自己的包中。执行在两侧都遵循 fold——bash 工具的按调用盖章将其作为 § 升级机制优先级链的中间层读取,approval seam 的 'never' 门控是批准 Agent Note 同一模式的另一侧。

沙箱模式不在提示词中叙述;拒绝结果在需要时报告模式,避免基于常驻标签的预防性拒绝。批准策略不同:只有 'never' 被声明,因为自动拒绝在行为上与用户的「不」无法区分。策略变更通知被合并,由下一个步骤前检查点递送,重启后有基于日志的回退。通知来源从事件位置推断:最后一个 request header 之后的旋钮事件是用户驱动的;未记录的漂移是运维人员或配置驱动的。

编辑器界面是协议原生的会话配置选项——该规范对 session modes 的替代(计划在 ACP v2 中移除),已有 SDK 类型。当 ctx.permission 被组合时,bridge 在 session/new 和 session/load 中公布一个 permission 选择器(category mode);其选项是部署的 preset 表,其 currentValue 是 PermissionService.current() 对会话日志加组合默认值的结果。随附的 workspace-write 和 danger-full-access preset 各自捆绑一个沙箱模式与一个批准策略,并写入两个领域 setter;preset 表之外的旋钮组合报告为仅可切换离开的 custom。session/set_config_option 通过权限服务验证并切换,然后返回完整的刷新状态(规范契约)。

轮次封闭是提交边界。 开放轮次中的切换立即追加。空闲切换保持在 bridge 记录上待定,在下一次提示词提交时、assembly 或执行之前追加到开放轮次中;每个旋钮以最后写入为准。开放性来自日志边界而非 agent.status,setter 不从 session/event 监听器内追加,因为那会重排后续观察者。锚定之前,响应叠加待定值。崩溃丢弃它,重新加载返回持久 fold。

进程内工具

fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层面的策略。fs seam 现在通过沙箱提供方强制共享模式词汇(dsh-fs-sandbox 按模式限制 write/edit;见跨工具族 fs 沙箱 Agent Note),因此 read-only/workspace-write 对文件系统工具也是真实边界,而非仅限 bash 的近似。web/todo 仍不受限制(web 的唯一效果是网络,不在文件效果模式词汇内)。没有通用的按工具沙箱运行时:主机中介的工具仅通过返回主机验证的声明式效果来离开进程,那是一次重写而非包装——后续设计选择了一个共享策略归属 ctx.sandboxPolicy,由各 seam 强制,而不是统一包装器。

测试

  • 单元测试: 固定平台选择和 profile、失败关闭的 runner 分类、按调用的模式/根目录解析、按进程事实、升级验证和结果、权限 preset fold 和写入透传、叙述器合并、ACP 公布和验证、轮次封闭的配置写入。
  • Keyless 真实 runner: 在提供方和 bash 消费方层面对 bwrap、Landlock 和 Seatbelt 执行真实文件系统效果测试;一个真实 Cordis 上下文通过已交付的 bash 和 fs 工具并发驱动两个项目会话,证明在自身根目录写入成功、在兄弟根目录写入被拒绝。Packed-install 覆盖率证明注册表 launcher 保持可执行。真实 ACP 组合固定权限切换并拒绝未知 preset。CI 拒绝静默全跳过。
  • With-key: 以只读模式启动真实 ACP 组合,让模型驱动的 bash 写入命中 runner 的拒绝标记,再通过已授权与被拒绝的 workspace-write 重试驱动 bridge 应答器和磁盘效果;不可用的凭证或 runner 自动跳过。
  • 快照: 固定权限 config-option 协议格式(wire format)、preset 和旋钮事件、提示词 delta 和通知、以及两个脚本化的 approval 分支。一个真实 ACP 示例场景把会话放在用户主目录下,同时让部署后备根目录指向 /tmp,然后固定一次成功的 workspace-write 变更;这能区分会话根目录解析与进程后备值,而不依赖 runner 特定的拒绝文本。其他快照以无约束启动,使无关 fixture(测试前置数据)保持平台无关;策略场景显式切换。

延迟阶段

每个阶段在被拾起时获得完整设计,对照当时的代码验证,并在其涉及的层级带上单元测试、真实 API e2e 和快照覆盖率落地。

  • 第二个消费方——subagent-acp 可选地约束子 agent(按调用策略;默认无约束——子 agent 必须写入自己的持久化)。
  • 更多环境——环境一致的能力组示例(如 bash+fs 对一个容器)。
  • Windows 链——PLATFORM_CHAINS.win32 保留为空(失败关闭);填充它意味着来自 AppContainer/restricted-token 家族的约束 runner,从其自己的仓库按 node-addon-landlock-run 模板交付,加上其 profile 方言和拒绝/runner 失败签名。

曾考虑的替代方案

  • 命令字符串启发式预检:否决。无法理解展开/子进程/符号链接;严格尝试(运行它,让内核决定)是唯一可信的拒绝信号。
  • 即使平台仅有一个后端也功能性探测:否决。探测用于在候选者之间仲裁;只有一个时无需决策,且探测开销对每个会话的首次约束命令征税(对未来重量级后端而言代价过高)。runner 自身执行时的失败关闭拒绝加 runnerFailureSignatures 分类承载了安全属性。
  • 提交构建好的 launcher 二进制:否决。diff 中的二进制不可审查且膨胀历史;经审查的源码 + 原生 CI 构建 + launcher 仓库的字节固定发布演练使二进制远离所有代码树。
  • 安装时编译 launcher:否决。将 C 工具链强加给每个消费方;仅在碰巧有编译器时才存在的备选不是备选。
  • 从一个构建器交叉编译两种架构:否决。仅为重建两个约 70 KB 的二进制就需要携带一个固定的交叉工具链(rustup targets、zig 或容器镜像);每架构的原生 runner 已存在,各自构建自己的平台包(node-addon-require-builtin 模式,launcher 仓库自己的流水线)。
  • 无备选(bwrap 或失败关闭):否决。将失败集中在沙箱最重要的主机上,最终因放弃而降级到 danger-full-access。
  • 将机制保留在 dsh-bash-sandbox 内部:否决。阻塞既有的第二个消费方,使未来阶段从一个 bash 插件的配置中读取模式,且无法表达升级。
  • 提供方上的配置固定模式:否决。每进程一个模式;无法服务具有不同策略的并发消费方,也无法表达一次性放宽重试。
  • 一个接口同时覆盖容器/VM:否决。confine(argv) 预设共享文件系统;环境隔离是作为一致组部署的能力兄弟后端。
  • 通用 ToolRuntime 包装任何工具:否决。对进程内工具(闭包了 ctx)机械上不成立;声明式效果重写对 fs/web/todo 而言不合理。
  • 在执行器内部(dsh-bash-sandbox)请求批准:否决。没有可路由的 agent,没有可附加提示词的 callId;添加它们会让传输 seam 了解会话和 UI——工具层持有两者并拥有面向模型的词汇。
  • 同一工具调用内自动重试:否决。日志无法重建的隐藏重入:一个 tool/call 会产生两次具有不同策略的执行——重试是一次新的带有自身参数和结果事实的已记录调用。
  • 无条件公布升级字段:否决。在 dsh-bash-local 下它们是死杠杆——公布 harness 无法兑现的选项会制造注定失败的授权;能力门控仅需注册时一次读取。
  • 默认值相对的升级阶梯(仅公布比执行器注册时默认值更宽的模式):否决。按会话覆盖使默认值成为错误的基线——切换到比默认值更窄的会话恰恰失去它需要的杠杆,而在 danger-full-access 默认值下字段完全消失,同时一个被覆盖为 read-only 的会话仍处于约束中却没有升级路径。枚举固定封闭的目标词汇;严格放宽是针对会话有效模式的按调用执行检查。
  • 按会话动态工具 schema:否决。schema 设计上是注册表全局的(一套 assembly 词汇、固定 header 快照契约),按会话重新注册只能买到执行时严格放宽检查已保证的东西,代价是按会话的 schema 表面和每次切换的 header 变动。
  • 将重试硬匹配到先前的拒绝:否决。命令字符串同一性脆弱(引号、workdir、env 前缀、作为失败阶段重试的管道)——要么误拒诚实的重试,要么被轻易满足;真正的边界是人看到命令 + 理由。仅在 allow_always 授权存储需要机器可检查的范围时才重新考虑。
  • 通用 env/state facts map 加拥有者服务:否决。approval 和沙箱独立组合,因此任何一方的状态都不应拖入第三个包;单键 fold 各自是一个 findLast,拥有者服务自然消解;没有跨旋钮的不变式,因此原子多键补丁无收益。
  • 通过 agent/user-message + 总线事件叙述:否决。它预设了一个不存在的轮次入口 seam(真正的 seam 是 agent/prompt-submit),而步骤前检查点的位置使一个监听器能够同时服务合并的轮次入口通知和轮中即时性约束。
  • 提示词中常驻声明沙箱模式(+ 切换叙述器):先交付后移除,基于实际证据:当每个请求中都有 Bash commands run under the "read-only" file sandbox. 时,模型拒绝尝试被拒绝后可升级的工作(首次手动会话中十二个轮次有五个以零工具调用结束),将沙箱变成了软锁定。拒绝标记在需要时命名模式,升级字段承载恢复路径;批准旋钮保留其声明,因为自动拒绝在行为上与人的「不」无法区分。
  • 用专门的簿记事件追踪「上次告知」:否决。request/header fold 已记录模型看到的确切提示词;将封闭的候选句子解析回来替代了第二条簿记流——事件仅在它们本身即为存储时才需要。
  • ACP session modes 而非 config options:否决。preset 已经是一个部署定义的 config-option 选择器,且 modes 计划在 ACP v2 中移除。

后果

已交付并固定的内容——测试中的各层级分别保障:

  • 被拒绝的命令以 sandbox_permissions + justification 重试时,通过组合的应答器链提示用户;授权使该次调用在更宽模式下运行(结果事实如此报告),而其他所有调用保持各自的有效模式;每种非授权结果产生各自不同的错误文本且不执行任何内容。
  • 升级字段恰好在已挂载的执行器约束时存在;不严格宽于调用有效模式的请求以自身文本失败关闭且不提示任何人;没有 ApprovalService 的部署对升级调用失败关闭,对普通调用不影响。
  • 系统提示词从不声明沙箱模式(批准 'never' 策略是唯一被声明的旋钮),且整个交互——header、旋钮事件、通知、批准、结果——仅从会话日志即可重建,除两个旋钮事件外无额外事件类型。
  • N 次空闲切换每个旋钮最多产生一个锚定事件(净零序列不锚定任何事件——客户端回显当前选择的无操作推送不记录任何内容);批准策略切换最多以一条合并通知叙述;轮中沙箱切换由下一次调用的盖章兑现。
  • 恢复的会话的覆盖生效并报告给编辑器,无需特殊处理;进程停止期间变更的默认值在会话的首个新请求前被叙述,归因于运维人员。
  • 两个并发会话永远看不到彼此的状态、通知或配置选项。
  • 同一个 Cordis 上下文中的两个并发项目会话解析各自独立的工作区根目录;bash 和 fs 写入在调用方会话的 cwd 内成功,对其相邻会话的 cwd 则失败。
  • agent-loop 未被触及——一切搭载 systemPrompt.section、SessionEventMap 合并、agent.inject()、agent/pre-step、agent/prompt-submit 和 ACP handler 表面。

代价与已接受的限制:

  • 单一包装的幻觉被有意放弃。tools/pre-execute 包装加提示词约定无法解决沙箱批准——正确的设计需要结构化拒绝、原生 runner 探测、按调用策略承载和一致的跨工具族强制,本设计为此付出了代价。
  • read-only 通过后续设计成为跨工具族边界。 本 Agent Note 最初只交付 bash 强制;跨工具族 fs 沙箱 Agent Note 通过沙箱化的 ctx.fs 提供方把同一模式词汇扩展到文件系统工具,并将 mode/root 配置和 sandbox/mode 覆盖迁移到 ctx.sandboxPolicy(§ 进程内工具)。
  • Windows 没有后端。 其链槽保留为空——失败关闭,绝不穿透;填充它是延迟阶段。
  • Seatbelt 层级依赖 Apple 已弃用但仍交付的 sandbox-exec CLI。 作为 darwin 的唯一候选,它无需探测即被选中,因此未来移除会在执行时作为 runner 失败分类浮现——重新抛出 SANDBOX_UNAVAILABLE,命令从未运行;失败关闭,绝不开放。
  • Landlock 约束的完整度取决于运行内核的 ABI。 报告为 enforcement: 'partial' 而非拒绝——这是有意的权衡,使备选在旧内核主机上仍可用。
  • launcher 作为注册表依赖到达。 通过其自身仓库的发布流水线(经审查的 C 源码、原生 CI 构建器、字节固定的发布演练)加上本仓库的版本固定获得信任——真实内核 e2e 测试腿是通过安装字节为行为背书的。
  • 模型可能过度请求。 在没有拒绝依据的情况下升级,或在 workspace-write 足够时选择 danger-full-access:描述引导且枚举强制阶梯,但人的提示词是实际门控;approval/asked 原因使过度请求可审计,且 prepend 策略应答器可以自动拒绝部署永远不想要的模式。
  • 公布的目标集是静态的,而有效模式是按会话的(schema 是注册表全局的)——已处于最宽模式的会话仍被提供这些字段。构造上无害:执行时的严格放宽检查(而非枚举)是安全边界——非放宽请求以自身文本失败且不提示任何人。
  • 授权的升级不等于可工作的沙箱。 不可用的后端即使对授权升级到约束模式也仍然失败关闭——在平台没有链或所有探测失败时于 confine() 阶段,在未探测的唯一 runner 拒绝时于执行阶段(归类为沙箱失败而非命令失败)——而授权的 danger-full-access 运行根本不触及提供方:此时授权(而非探测)是权威。
  • 空闲切换存在于 bridge 内存中,直到下一次提示词提交锚定它。 该窗口内的崩溃将其回退(在 session/load 时报告),且永不再提交提示词的会话永不持久化它——已接受,loop 拥有的空闲提交轮次留作未来工作(如果持久性成为需求)。
  • 批准叙述器的重启基线解析提示词文本。 封闭的候选句子由写入模块本身拥有,因此措辞变更是同一文件中写入器+解析器的协调编辑;header 早于该段落的会话静默采用当前策略而不发通知。
  • 批准段落仍是动态提示词表面('never' 切换会破坏该会话的提供方提示词前缀缓存)。已接受:策略切换罕见,且模型基于过时的 'never' 行动更糟。沙箱旋钮不再触及提示词。
  • 模型可能持有关于沙箱模式的过时信念(没有任何东西宣布切换)。有意接受:下一次尝试的标记或成功会纠正它,而宣布的观察到的失败模式——预防性拒绝——比一次浪费的重试更糟。

FAQ

  • 一个命令返回了 [sandbox: file access denied under read-only mode]——它失败了吗? 它运行了,内核拒绝了一个文件操作:拒绝是与退出码正交的结果事实。教学禁止绕过它重试;唯一被认可的动作是以升级请求重试同一命令一次。
  • 如何区分损坏的沙箱与失败的命令? Runner 失败在分类中优先于拒绝:匹配包装的 runnerFailureSignatures 的失败运行意味着命令从未运行——前台重新抛出结构化的 SANDBOX_UNAVAILABLE 并附带 runner 的 stderr 行,后台任务盖章 sandbox.runnerFailed 并渲染自己的标记。损坏的沙箱永远不会被读作失败的命令,且命令永远不会无约束运行。
  • 在没有后端的平台上会发生什么——今天的 Windows? confine() 抛出失败关闭的 SANDBOX_UNAVAILABLE,命令永不 spawn;win32 是保留的空链,由测试固定为同样失败关闭,直到 Windows runner 填充它(§ 延迟阶段)。
  • bwrap 已安装在我的主机上但不可用(禁用了非特权 userns、LSM 拒绝 mount)——会发生什么? 链探测是功能性的——它构建并强制一个真实 profile 而非检查 --version——因此存在但不可用的 bwrap 探测失败,选择落到注册表安装的 Landlock launcher,结论在提供方生命周期内缓存。
  • 沙箱限制网络或进程可见性吗? 不——SandboxMode 仅声称文件操作;bwrap profile 刻意不 unshare pid,没有后端声称网络。网络限制是否成为自己的旋钮留在 § seam 中开放。
  • 哪些工具实际在约束下运行? 通过 ctx.bash 的 OS 子进程——bash 工具及传递性的钩子命令——再加上通过沙箱化 ctx.fs 提供方运行的文件系统工具(read/write/edit,见跨工具族 fs 沙箱 Agent Note):bash 通过 OS runner 约束,fs 通过进程内路径围栏约束,二者都以同一个 ctx.sandboxPolicy 模式为键。web/todo 仍在进程内且不受限制(web 的唯一效果是网络,不在文件效果模式词汇内)。
  • 授权的升级会持久化吗? 不会。授权由发起请求的确切前台或后台调用消费;每个相邻调用保留自己的有效模式。后续的后台拒绝通过 task_output 呈现,并且可以作为一次新的精确命令重试的依据。
  • 编辑器的模式切换何时生效? 轮中:立即追加,由下一次调用的盖章兑现。空闲:保持在 bridge 的会话记录上,在下一次 agent/prompt-submit 时锚定到其开放轮次中,N 次切换合并为最多一个事件(净零则无);锚定前崩溃回退它,session/load 报告真实状态。模型不被告知——其下一个命令直接在新模式下运行。
  • 重启后什么存活——如果运维人员在进程停止期间改了配置默认值呢? 覆盖从会话日志回放(effective = fold ?? config),因此恢复的会话以零追赶机制保持其模式;离线漂移的默认值以与切换相同的方式改变行为(批准策略因被声明,还额外以运维人员/配置归因叙述)。
  • 结果上的 enforcement: 'partial' 是什么意思? 所选后端强制其内核 ABI 管控的子集——例如 ABI v3 之前的 Landlock 不管控路径 truncate——并以结构化方式如此声明而非拒绝主机;探测的报告行区分各种情况。bwrap 和 Seatbelt profile 构造上管控所有承诺的文件操作,因此始终报告 full。

先例

本设计复制或对比的仓库内先例:

  • 能力 seam Agent Note——接口/实现/消费方拆分与「不要过早拆分」的时机规则(第二个消费方满足了该规则)。
  • dsh-bash 的 request/spec 拆分(bash 词汇目录)——完整的 sandboxPolicy 搭载其按调用载体,以及显式 resolve() 默认约定。
  • 批准 seam Agent Note——升级请求通过的通道;其应答器 waterfall(瀑布式事件)、审计对和单包理由记录在那里。
  • 事件溯源会话与轮次封闭不变式——按会话模式 fold 所依赖的日志即存储基础,以及锚定设计遵守的提交边界。
  • 拦截 seam Agent Note——tools/pre-execute 词汇,升级门控刻意不复用它(升级调用没有自己的 pre-execute 时刻)。