docs(i18n): add agent-scope and sandbox pairs — the batch is complete

最后两篇超长 RFC 经流式通道产出(代理会掐非流式长请求),v4 流水
线同一契约;至此 185 篇批次全部配对,pairing 194 对全绿。
This commit is contained in:
ZiyaZhang
2026-07-22 04:20:18 -07:00
parent 515d13db7f
commit 0b1857904a
7 changed files with 622 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-06-sandbox.md: 29985b9336e7a190c90dc0e9c3b3aa78b6197e8e
2026-07-06-sandbox.zh.md: 4285ae4ceb81ee57dc3ab3d51467f9267743e06e

View File

@@ -1,5 +1,7 @@
# RFC: The subprocess sandbox — confinement seam, native runners, escalation, and per-session modes
English | [中文](2026-07-06-sandbox.zh.md)
Status: implemented
## Problem

View File

@@ -0,0 +1,212 @@
# RFC:子进程沙箱——约束 seam、原生 runner、升级机制与按会话模式
Status: implemented
[English](2026-07-06-sandbox.md) | 中文
## 问题
一个编码 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`。范围有意限定:本 RFC 命名但不设计的阶段——按会话工作区根目录、跨工具族 fs 强制、`subagent-acp` 消费方、更多环境、Windows 链——列在 § 延迟阶段,各自是后续设计,而非配置旋钮。
### 部署方式
四条 `cordis.yml` 条目即可将一个无约束的编码 agent 转变为沙箱产品路径;[`examples/acp-agent`](../../../../examples/acp-agent/README.md) 默认使用此组合:
```yaml
- 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 RFC)
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`)、`SandboxPolicy`(mode + workspace root)。
策略随每次**调用**而非提供方携带:两个消费方可以在同一时刻以不同策略约束(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 通过 [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) 交付,平台二进制由 npm 选择。该包(package)拥有路径解析、探测和 CLI flag;harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。
FIXME: Revisit the separate-repository boundary and try to maintain the launcher source and its platform package family inside this monorepo, so the native release surface and harness contract evolve together.
后端 profile 共享模式契约但在必要的主机授权上有所不同。Landlock 和 Seatbelt 在 read-only 模式下仅允许 `/dev/null`;workspace-write 还允许各自所需的主机临时目录根。每次包装携带后端特定的拒绝签名。Landlock 在较旧的 ABI 无法管控所有操作时报告 partial enforcement,而成功的 bwrap 和 Seatbelt profile 报告 full enforcement。
#### bash 消费方
`dsh-bash-sandbox` 复用本地进程执行,并请求 `ctx.sandbox` 包装确切的 bash argv。内核拒绝是独立于退出状态的结果事实,仅从所选包装的 stderr 方言推断。Runner 失败优先于拒绝,因为它意味着命令从未运行:前台调用抛出 `SANDBOX_UNAVAILABLE`,而已结算的后台任务为 `bash_output` 设置 `sandbox.runnerFailed`。这使损坏的约束与任务失败和强制拒绝保持区分。
模型看到的仅是结果事实:静态工具描述解释拒绝标记(`[sandbox: file access denied under <mode> mode]`),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试;当升级字段被公布时,被拒绝的结果还额外携带升级提示本身,使被认可的同轮次重试在决策点被提示,而非依赖模型回忆描述(§ 升级机制)。没有提示词段落声明沙箱模式(§ 按会话模式)。
#### 升级机制:拒绝后一次经批准的更宽重试
`BashExecRequest.sandboxMode` 是可选的按调用输入;解析后的 spec 使该字段显式。`BashExecutor.sandboxMode` 公布已挂载的执行器能否兑现它,因此只有约束组合才暴露升级。seam 接受任何显式模式;工具拥有「仅更宽」的升级规则。非沙箱执行器诚实地保持无约束。
`SandboxBashExecutor.resolve()` 盖章有效模式——升级授权 > 会话覆盖 > 配置默认——使 `run()`/`start()` 读取 spec 而非配置。`danger-full-access` 分支、confine 调用和结果事实都以 spec 的模式为键,且按任务的事实 map 携带每个任务的模式及其包装事实(`notifyTaskDone()` 从 map 条目盖章):一次升级调用——前台或后台——报告它**实际**运行的模式,而每个邻居保持自己的。
当约束执行器被挂载时,`bash` 公布配对的 `sandbox_permissions` 和 `justification` 字段。schema 暴露完整的封闭升级词汇,因为有效模式是按会话的;执行拒绝任何不严格宽于该调用有效模式的目标。批准在执行之前解析。`allowed-once` 仅将授权模式盖章到该请求上,而 `rejected`、`cancelled`、`unavailable`、缺失的 approval 服务或缺失的 agent 都以各自不同的结果文本失败关闭。授权不持久化。
升级是对被拒绝命令的同轮次重试,使用最窄的足够 `sandbox_permissions` 和一个 `justification`;批准提示是同意步骤。它必须基于实际的拒绝,除非会话已观察到相同的被拒绝访问;禁用或被拒绝的批准终结该命令。重试、批准决策和结果使用既有的工具和批准事件。`dsh-tool-bash` 拥有请求动作,因为执行器 seam 既没有 agent 也没有用户交互所需的 call id。
留待后续阶段处理:授权的范围标识超出沙箱模式之外是什么——确切的调用、路径、命令前缀、会话、时间窗口——这是 `allow_always` 授权存储在该选项可被公布之前必须回答的问题;以及如何为通过 `bash_output` 延迟到达的 `run_in_background` 拒绝定义升级。
#### 按会话模式:会话日志即存储
```
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/*`):
```ts
interface SessionEventMap {
'bash/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'` 门控是[批准 RFC](2026-07-06-approval-seam.md) 同一模式的另一侧。
沙箱模式不在提示词中叙述;拒绝结果在需要时报告模式,避免基于常驻标签的预防性拒绝。批准策略不同:只有 `'never'` 被声明,因为自动拒绝在行为上与用户的「不」无法区分。策略变更通知被合并,由下一个 pre-step 递送,重启后有基于日志的回退。通知来源从事件位置推断:最后一个 request header 之后的旋钮事件是用户驱动的;未记录的漂移是运维人员或配置驱动的。
**编辑器界面**是协议原生的 [Session Config Options](https://agentclientprotocol.com/protocol/session-config-options)——该规范对 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` 通过 permission 服务验证并切换,然后返回完整的刷新状态(规范契约)。
**轮次封闭是提交边界。**开放轮次中的切换立即追加。空闲切换保持在 bridge 记录上待定,在下一次 prompt 提交时、assembly 或执行之前追加到开放轮次中;每个旋钮以最后写入为准。开放性来自日志边界而非 `agent.status`,setter 不从 `session/event` 监听器内追加,因为那会重排后续观察者。锚定之前,响应叠加待定值。崩溃丢弃它,重新加载返回持久 fold。
#### 进程内工具
fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层面的策略:fs 意图门控按共享模式词汇决策(§ 延迟阶段,跨工具族),使 `read-only` 成为真正的边界而非仅限 bash 的近似——在此之前契约诚实地如此声明。没有通用的按工具沙箱运行时:主机中介的工具仅通过返回主机验证的声明式效果来离开进程,那是一次重写而非包装。
FIXME: Revisit this tool-local boundary. The follow-up design needs to determine whether sandboxing becomes a global harness capability that applies uniformly to every tool, instead of expressing in-process enforcement independently at each tool seam.
### 测试
- **单元测试:**固定平台选择和 profile、失败关闭的 runner 分类、按调用事实、升级验证和结果、permission preset fold 和写入透传、叙述器合并、ACP 公布和验证、轮次封闭的配置写入。
- **Keyless 真实 runner:**在提供方和 bash 消费方层面对 bwrap、Landlock 和 Seatbelt 执行真实文件系统效果测试;packed-install 覆盖率证明注册表 launcher 保持可执行。真实 ACP 组合固定 permission 切换并拒绝未知 preset。CI 拒绝静默全跳过。
- **With-key:**驱动真实模型、runner、bridge 应答器和磁盘效果通过授权和拒绝的升级;不可用的凭证或 runner 自动跳过。
- **快照:**固定 permission config-option 协议格式(wire format)、preset 和旋钮事件、prompt delta 和通知、以及两个脚本化的 approval 分支。快照模式以无约束启动,使无关 fixture(测试前置数据)保持平台无关;策略场景显式切换。真实拒绝 stderr 留在平台测试中,因为其方言是 runner 特定的。
## 延迟阶段
每个阶段在被拾起时获得完整设计,对照当时的代码验证,并在其涉及的层级带上单元测试、真实 API e2e 和快照覆盖率落地。
- **按会话工作区根目录**——执行器的写入边界在其生命周期内保持配置固定,而每个 ACP 会话有自己的 cwd;按会话根目录一旦设计完成即搭载同一个按调用策略载体。
- **跨工具族边界**——fs 意图门控按共享模式决策,使 `read-only`/`workspace-write` 成为 bash 之外的真正边界。
- **第二个消费方**——`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`,没有可附加 prompt 的 `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 和 sandbox 独立组合,因此任何一方的状态都不应拖入第三个包;单键 fold 各自是一个 `findLast`,拥有者服务自然消解;没有跨旋钮的不变式,因此原子多键补丁无收益。
- **通过 `agent/user-message` + 总线事件叙述**:否决。它预设了一个不存在的轮次入口 seam(真正的 seam 是 `agent/prompt-submit`),而 pre-step 的位置以一个监听器同时服务合并的轮次入口通知和轮中即时性约束。
- **提示词中常驻声明沙箱模式(+ 切换叙述器)**:先交付后移除,基于实际证据:当每个请求中都有 `Bash commands run under the "read-only" file sandbox.` 时,模型拒绝**尝试**被拒绝后可升级的工作(首次手动会话中十二个轮次有五个以零工具调用结束),将沙箱变成了软锁定。拒绝标记在需要时命名模式,升级字段承载恢复路径;批准旋钮保留其声明,因为自动拒绝在行为上与人的「不」无法区分。
- **用专门的簿记事件追踪「上次告知」**:否决。`request/header*` fold 已记录模型看到的确切 prompt;将封闭的候选句子解析回来替代了第二条簿记流——事件仅在它们**本身即为存储**时才需要。
- **ACP session modes 而非 config options**:否决。preset 已经是一个部署定义的 config-option 选择器,且 modes 计划在 ACP v2 中移除。
## 后果
已交付并固定的内容——测试中的各层级分别保障:
- 被拒绝的命令以 `sandbox_permissions` + `justification` 重试时,通过组合的应答器链提示用户;授权使**该次**调用在更宽模式下运行(结果事实如此报告),而其他所有调用保持各自的有效模式;每种非授权结果产生各自不同的错误文本且不执行任何内容。
- 升级字段恰好在已挂载的执行器约束时存在;不严格宽于调用有效模式的请求以自身文本失败关闭且不提示任何人;没有 ApprovalService 的部署对升级调用失败关闭,对普通调用不影响。
- 系统提示词从不声明沙箱模式(批准 `'never'` 策略是唯一被声明的旋钮),且整个交互——header、旋钮事件、通知、批准、结果——仅从会话日志即可重建,除两个旋钮事件外无额外事件类型。
- N 次空闲切换每个旋钮最多产生一个锚定事件(净零序列不锚定任何事件——客户端回显当前选择的无操作推送不记录任何内容);批准策略切换最多以一条合并通知叙述;轮中沙箱切换由下一次调用的盖章兑现。
- 恢复的会话的覆盖生效并报告给编辑器,无需特殊处理;进程停止期间变更的默认值在会话的首个新请求前被叙述,归因于运维人员。
- 两个并发会话永远看不到彼此的状态、通知或配置选项。
- `agent-loop` 未被触及——一切搭载 `systemPrompt.section`、`SessionEventMap` 合并、`agent.inject()`、`agent/pre-step`、`agent/prompt-submit` 和 ACP handler 表面。
代价与已接受的限制:
- **单一包装的幻觉被有意放弃。**`tools/pre-execute` 包装加 prompt 约定无法解决沙箱批准——正确的设计需要结构化拒绝、原生 runner 探测、按调用策略承载和一致的跨工具族强制,本设计为此付出了代价。
- **`read-only` 尚不是跨工具族边界。**在 fs 意图门控按共享模式决策之前,该声明仅对 bash 成立;契约诚实地如此声明(§ 进程内工具)。
- **Windows 没有后端。**其链槽保留为空——失败关闭,绝不穿透;填充它是延迟阶段。
- **Seatbelt 层级依赖 Apple 已弃用但仍交付的 `sandbox-exec` CLI。**作为 darwin 的唯一候选,它无需探测即被选中,因此未来移除会在执行时作为 runner 失败分类浮现——重新抛出 `SANDBOX_UNAVAILABLE`,命令从未运行;失败关闭,绝不开放。
- **Landlock 约束的完整度取决于运行内核的 ABI。**报告为 `enforcement: 'partial'` 而非拒绝——这是有意的权衡,使备选在旧内核主机上仍可用。
- **launcher 作为注册表依赖到达。**通过其自身仓库的发布流水线(经审查的 C 源码、原生 CI 构建器、字节固定的发布演练)加上本仓库的版本固定获得信任——真实内核 e2e 测试腿是通过安装字节为行为背书的。
- **模型可能过度请求。**在没有拒绝依据的情况下升级,或在 `workspace-write` 足够时选择 `danger-full-access`:描述引导且枚举强制阶梯,但人的 prompt 是实际门控;`approval/asked` 原因使过度请求可审计,且 `prepend` 策略应答器可以自动拒绝部署永远不想要的模式。
- **公布的目标集是静态的,而有效模式是按会话的**(schema 是注册表全局的)——已处于最宽模式的会话仍被提供这些字段。构造上无害:执行时的严格放宽检查(而非枚举)是安全边界——非放宽请求以自身文本失败且不提示任何人。
- **授权的升级不等于可工作的沙箱。**不可用的后端即使对授权升级到约束模式也仍然失败关闭——在平台没有链或所有探测失败时于 `confine()` 阶段,在未探测的唯一 runner 拒绝时于执行阶段(归类为沙箱失败而非命令失败)——而授权的 `danger-full-access` 运行根本不触及提供方:此时授权(而非探测)是权威。
- **空闲切换存在于 bridge 内存中,直到下一次 prompt 提交锚定它。**该窗口内的崩溃将其回退(在 `session/load` 时报告),且永不再提交 prompt 的会话永不持久化它——已接受,loop 拥有的空闲提交轮次留作未来工作(如果持久性成为需求)。
- **批准叙述器的重启基线解析 prompt 文本。**封闭的候选句子由写入模块本身拥有,因此措辞变更是同一文件中写入器+解析器的协调编辑;header 早于该段落的会话静默采用当前策略而不发通知。
- **批准段落仍是动态 prompt 表面**(`'never'` 切换会破坏该会话的提供方 prompt 前缀缓存)。已接受:策略切换罕见,且模型基于过时的 `'never'` 行动更糟。沙箱旋钮不再触及 prompt。
- **模型可能持有关于沙箱模式的过时信念**(没有任何东西宣布切换)。有意接受:下一次尝试的标记或成功会纠正它,而宣布的观察到的失败模式——预防性拒绝——比一次浪费的重试更糟。
## 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 工具,以及传递性的钩子命令。fs/web/todo 在进程内执行,`execve` 包装对它们机械上毫无意义;它们的 `read-only` 语义随跨工具族延迟阶段到来,在此之前契约诚实地声明仅限 bash。
- **授权的升级会持久化吗?或覆盖后台任务吗?**都不会:授权被请求它的那次调用(前台或后台)消耗,该次调用报告它实际运行的模式,而每个邻居保持自己的。如何为通过 `bash_output` 延迟浮现的后台拒绝**定义**升级,留在 § 升级机制中开放。
- **编辑器的模式切换何时生效?**轮中:立即追加,由下一次调用的盖章兑现。空闲:保持在 bridge 的会话记录上,在下一次 `agent/prompt-submit` 时锚定到其开放轮次中,N 次切换合并为最多一个事件(净零则无);锚定前崩溃回退它,`session/load` 报告真实状态。模型不被告知——其下一个命令直接在新模式下运行。
- **重启后什么存活——如果运维人员在进程停止期间改了配置默认值呢?**覆盖从会话日志回放(`effective = fold ?? config`),因此恢复的会话以零追赶机制保持其模式;离线漂移的默认值以与切换相同的方式改变行为(批准策略因被声明,还额外以运维人员/配置归因叙述)。
- **结果上的 `enforcement: 'partial'` 是什么意思?**所选后端强制其内核 ABI 管控的子集——例如 ABI v3 之前的 Landlock 不管控路径 truncate——并以结构化方式如此声明而非拒绝主机;探测的报告行区分各种情况。bwrap 和 Seatbelt profile 构造上管控所有承诺的文件操作,因此始终报告 `full`。
## 先例
本设计复制或对比的仓库内先例:
- [能力 seam RFC](../architecture/2026-06-13-capability-seams.md)——接口/实现/消费方拆分与「不要过早拆分」的时机规则(第二个消费方满足了该规则)。
- `dsh-bash` 的 request/spec 拆分及其 `owner` 字段([bash 词汇目录](../../../core-data-structures/bash.md))——`sandboxMode` 搭载的按调用载体模板,以及显式 `resolve()` 默认约定。
- [批准 seam RFC](2026-07-06-approval-seam.md)——升级请求通过的通道;其应答器 waterfall(瀑布式事件)、审计对和单包理由记录在那里。
- [事件溯源会话](../architecture/2026-06-11-event-sourced-sessions.md)与[轮次封闭不变式](../architecture/2026-06-15-turn-enclosure-invariant.md)——按会话模式 fold 所依赖的日志即存储基础,以及锚定设计遵守的提交边界。
- [拦截 seam RFC](2026-06-30-interception-seams.md)——`tools/pre-execute` 词汇,升级门控刻意不复用它(升级调用没有自己的 pre-execute 时刻)。