feat(sandbox-policy): describe enforced file families

This commit is contained in:
NI0317
2026-07-30 18:51:29 +08:00
parent d3323494ba
commit 87a4aaa32e
48 changed files with 623 additions and 140 deletions

View File

@@ -2,5 +2,5 @@
# 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 .agents/notes/implemented/feature/2026-07-06-sandbox.md
2026-07-06-sandbox.md: ff79d2e1e4dc1501502cfeb2518ddf1065750f1a
2026-07-06-sandbox.zh.md: 1d2ab5ad556f4e1c72a124183b91b951b0ded6dd
2026-07-06-sandbox.md: f29bdf840db8b0f4cbcba9958c20cd0255097f38
2026-07-06-sandbox.zh.md: 91b3a0acf42585b2e47af2158e03fe7eee198d7d

View File

@@ -150,7 +150,7 @@ Each phase gets its full design when picked up, validated against the code at th
- **Hard-match the retry to a prior denial** — rejected: command-string identity is fragile (quoting, `workdir`, env prefixes, a pipeline retried as its failing stage) — false-rejects honest retries or is trivially satisfied; the real boundary is the human seeing command + justification. Revisit only if `allow_always` grant storage ever needs machine-checkable scopes.
- **A generic `env/state` facts map with an owner service** — rejected: approval and sandbox compose independently, so neither's state may drag in a third package; single-key folds are one `findLast` each, dissolving the owner service; no invariant spans the knobs, so atomic multi-key patches bought nothing.
- **Narrate via `agent/user-message` + a bus event** — rejected: it presupposes a turn-entry seam that does not exist (the real seam is `agent/prompt-submit`), and pre-step's position serves both the coalesced turn-entry notice and the mid-turn immediacy bound with one listener.
- **A bash-only mode label plus a switch narrator** — rejected: `Bash commands run under the "read-only" file sandbox.` caused preemptive refusal while leaving the filesystem-tool consequence and workspace scope ambiguous. The current owner-derived section is a different contract: later Web evidence showed that total absence caused false capability claims before a first tool call, and cross-family enforcement now supplies one complete file-effect policy. [The current-policy decision](2026-07-30-current-sandbox-policy-context.md) records why concise current state supersedes the absence choice without duplicating tool guidance.
- **A standing prompt statement of the sandbox mode (+ a switch narrator)** — shipped first, then removed on live evidence: with `Bash commands run under the "read-only" file sandbox.` in every request, the model refused to ATTEMPT denied-then-escalatable work (five of twelve turns in the first manual session ended with zero tool calls), turning the sandbox into a soft lockout. The denial marker names the mode at the moment it matters and the escalation fields carry the recovery; the approval knob keeps its statement because an auto-rejection is behaviorally indistinguishable from a human "no". The absence decision is superseded by [the current-policy decision](2026-07-30-current-sandbox-policy-context.md); this measurement and causal observation remain the evidence that any replacement must counter-test.
- **Track "last told" with its own bookkeeping events** — rejected: the `request/header` fold already records the exact prompt the model saw; parsing the closed candidate sentences back replaces a second bookkeeping stream — events are needed only where they ARE the store.
- **Independent sandbox and approval selectors** — rejected: one deployment-defined permission preset keeps the two policy knobs coherent for UI clients that expose runtime switching.
@@ -180,6 +180,7 @@ Costs and accepted limits:
- **A granted escalation is not a working sandbox.** An unavailable backend still fails closed even for a granted escalation to a confining mode — at `confine()` when the platform has no chain or every probe fails, at execution when an unprobed sole runner refuses (classified as a sandbox failure, not a command failure) — while a granted `danger-full-access` run never touches the provider at all: there the grant, not the probe, is the authority.
- **The approval narrator's restart baseline parses prompt prose.** The closed candidate sentence is owned by the writing module itself, so a wording change is a coordinated writer+parser edit in one file; a session whose headers predate the section silently adopts the current policy without a notice.
- **The approval and sandbox sections are dynamic prompt surfaces.** A policy switch breaks provider prompt-prefix caching for that session; unchanged state remains byte-stable, and a model acting on stale authority is worse than the bounded invalidation.
- **The model may hold a stale belief about the sandbox mode** (nothing announces a switch). Accepted deliberately in the original design: the next attempt's marker or success corrects it, and the observed failure mode of announcing — preemptive refusal — is worse than one wasted retry. [The current-policy decision](2026-07-30-current-sandbox-policy-context.md) supersedes this accepted limit with a family-aware request section while retaining the preemptive-refusal evidence as its counter-test.
## FAQ

View File

@@ -150,7 +150,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **将重试硬匹配到先前的拒绝**:否决。命令字符串同一性脆弱(引号、`workdir`、env 前缀、作为失败阶段重试的管道)——要么误拒诚实的重试,要么被轻易满足;真正的边界是人看到命令 + 理由。仅在 `allow_always` 授权存储需要机器可检查的范围时才重新考虑。
- **通用 `env/state` facts map 加拥有者服务**否决。approval 和沙箱独立组合,因此任何一方的状态都不应拖入第三个包;单键 fold 各自是一个 `findLast`,拥有者服务自然消解;没有跨旋钮的不变式,因此原子多键补丁无收益。
- **通过 `agent/user-message` + 总线事件叙述**:否决。它预设了一个不存在的轮次入口 seam真正的 seam 是 `agent/prompt-submit`),而步骤前检查点的位置使一个监听器能够同时服务合并的轮次入口通知和轮中即时性约束。
- **仅限 bash 的模式标签加切换叙述器**否决。`Bash commands run under the "read-only" file sandbox.` 会引发预防性拒绝,同时没有明确文件系统工具的后果与工作区范围。当前由归属方派生的段落采用不同契约:后续 Web 证据表明,完全缺失策略会导致模型在首次工具调用前错误声称自身能力,而跨工具族强制现在能够提供一项完整的文件操作策略。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)记录了为何用简洁的当前状态取代缺失策略的选择,同时不重复工具引导
- **在提示词中常驻声明沙箱模式(并加切换叙述器**先行交付,随后根据线上证据移除:每次请求都带有 `Bash commands run under the "read-only" file sandbox.` 时,模型会拒绝尝试本可在被拒后升级的工作(首次人工会话的十二个轮次中有五个以零工具调用结束),使沙箱变成软锁死。拒绝标记会在相关时刻指出模式,升级字段则承载恢复路径;批准旋钮之所以保留声明,是因为自动拒绝在行为上与人类回答「否」无法区分。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)取代了省略策略的决策;这项测量和因果观察仍是任何替代方案必须进行反证测试的依据
- **用专门的簿记事件追踪「上次告知」**:否决。`request/header` fold 已记录模型看到的确切提示词;将封闭的候选句子解析回来替代了第二条簿记流——事件仅在它们本身即为存储时才需要。
- **相互独立的沙箱与批准选择器**:否决。一个部署定义的权限 preset 让两个策略旋钮对暴露运行时切换的 UI 客户端保持一致。
@@ -180,6 +180,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **授权的升级不等于可工作的沙箱。** 不可用的后端即使对授权升级到约束模式也仍然失败关闭——在平台没有链或所有探测失败时于 `confine()` 阶段,在未探测的唯一 runner 拒绝时于执行阶段(归类为沙箱失败而非命令失败)——而授权的 `danger-full-access` 运行根本不触及提供方:此时授权(而非探测)是权威。
- **批准叙述器的重启基线解析提示词文本。** 封闭的候选句子由写入模块本身拥有,因此措辞变更是同一文件中写入器+解析器的协调编辑header 早于该段落的会话静默采用当前策略而不发通知。
- **批准段落与沙箱段落都是动态提示词表面。** 策略切换会破坏该会话的提供方提示词前缀缓存;状态不变时仍保持字节稳定,且模型基于过时权限行动的风险高于这种有限的缓存失效。
- **模型可能持有过时的沙箱模式认知**(没有任何内容会宣布切换)。原始设计有意接受这一点:下一次尝试的标记或成功结果会纠正认知,而观察到的宣布失败模式——预防性拒绝——比一次浪费的重试更糟。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)通过感知家族的请求段落取代了这项已接受限制,同时保留预防性拒绝证据作为其反证测试。
## FAQ

View File

@@ -2,5 +2,5 @@
# 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 .agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md
2026-07-30-current-sandbox-policy-context.md: 2854f527c62dedfcb2fa86ab684d162e892db35c
2026-07-30-current-sandbox-policy-context.zh.md: 0560251afc450136fd1c4a2e28aba3f1f16f2937
2026-07-30-current-sandbox-policy-context.md: 93353272a599e8a3a984e8e10039d400e236e9ff
2026-07-30-current-sandbox-policy-context.zh.md: 4fb9ad4ef035c3515f17acb541afc5ab23510db7

View File

@@ -12,11 +12,13 @@ The sandbox policy already enforced and logged each session's file-effect mode,
`dsh-sandbox-policy`, the owner of mode and workspace-root resolution, registers one `sandbox:policy` system-prompt section. Every agent request resolves the active session directly through `ctx.sandboxPolicy.resolve({ session })`; there is no denial-history scan, delta narrator, or in-memory “last told” state.
The section states the current file-effect mode and only its owned consequences. `read-only` says ordinary writes, edits, and file-mutating shell effects are denied while required sinks may remain writable. `workspace-write` lists the canonical writable roots returned by the shared `writableRoots()` policy: the immutable session workspace root, `/tmp`, and the platform temporary directory, deduplicated after canonicalization. `danger-full-access` says the DSH file sandbox adds no file restriction. Every form says host permissions or backend availability may restrict more and that network and process access are outside this policy.
Enforcing backends register independently disposable `filesystem`, `bash`, or `terminal` family contributions with the policy owner. The section names only registered families in canonical order, and is empty without one. This is current need, not a future extension: the shipped headless inheritance composition combines sandboxed filesystem tools with unfenced one-shot bash, while the persistent-tools composition combines sandboxed filesystem tools and terminal commands without a sandboxed one-shot bash executor. A blanket statement would be false in both.
The section states only facts shared by every enforcement dialect for each registered family. `read-only` says those operations cannot modify files. `workspace-write` states the canonical session workspace with non-exclusive wording and summarizes, without enumerating, that some platform temporary areas may also be writable. `danger-full-access` says the DSH file sandbox does not restrict those operations. Backend-selected temporary paths, `/dev/null`, runner readiness, and other policy domains are absent because `resolve()` cannot establish them at request assembly.
The provider runs during normal request assembly, after a `/permission` switch has committed its existing `sandbox/mode` event and before `request/header` is logged. The rendered system text is therefore the durable reconstruction of the exact model-visible fact. Repeated assemblies over unchanged session state produce identical bytes; resume and replay fold the same durable mode event and immutable `SessionHeader.cwd` without catch-up state.
Ownership stays narrow. Approval policy remains the separate `approval:policy` section, plan mode remains `plan:policy`, and tool plugins continue to own schemas and operation guidance. The prompt states policy; bash and filesystem backends remain the enforcement boundaries.
Ownership stays narrow. Approval policy remains the separate `approval:policy` section, plan mode remains `plan:policy`, and tool plugins continue to own schemas plus attempt, denial, and escalation guidance. The prompt states standing policy; filesystem, one-shot bash, and terminal backends remain the enforcement boundaries.
## Alternatives considered
@@ -28,10 +30,14 @@ Ownership stays narrow. Approval policy remains the separate `approval:policy` s
**Repeat tool schemas or approval and plan guidance in the section.** Rejected because those surfaces already have owners and independent lifecycles. Duplicating them would create contradictory request prefixes and broaden invalidation.
**Keep sandbox mode absent because a standing mode label once caused preemptive refusal.** Rejected by the later Web evidence and the completed cross-family policy. The earlier sentence named only a bash sandbox and did not explain the actual write/edit boundary, so it could conflict with visible tools and escalation guidance. The owner-derived section states the complete current file-effect consequence, canonical workspace scope, and explicit non-guarantees without duplicating tool instructions. This supersedes only the absence decision in the [sandbox Agent Note](2026-07-06-sandbox.md); its enforcement and escalation boundaries remain current.
**Keep sandbox mode absent because a standing mode label once caused preemptive refusal.** Rejected because a fresh Web request otherwise exposes mutation tools while withholding their standing policy, producing false capability claims before the first operation. The earlier live measurement remains a required counter-test: five of twelve turns ended without a tool call under `Bash commands run under the "read-only" file sandbox.` The committed tool-owned attempt guidance postdates that measurement, so the replacement is selected through a new positive-control experiment under the current tool contract rather than assuming the old and current conditions match.
**A separate model-context package.** Rejected because Cordis services can observe current runtime contributions directly, while approval and plan policy sections already live with their owners. A new package would add a shallow composition seam and documentation/gate surface for one internal adapter.
**Enumerate writable temporary roots.** Rejected because the backend is selected later at `confine()`: bwrap, Landlock, Seatbelt, and the in-process filesystem fence do not grant one common temporary-path set. Host-specific paths in a standing request would be both unstable and overclaimed.
## Consequences
A model can answer what file effects are currently possible before probing a tool, and the next request after `/permission` reflects the committed mode. This adds a small dynamic system section and intentionally invalidates the request prefix when policy changes; unchanged state remains cache-stable. The statement is guidance, not an enforcement guard: runtime safety still comes from `dsh-bash-sandbox` and `dsh-fs-sandbox` consuming the same resolved policy.
A model can answer what registered file operations the standing mode governs before probing a tool, and the next request after `/permission` reflects the committed mode. This adds a small dynamic system section and intentionally invalidates the request prefix when policy or enforcing-family composition changes; unchanged state remains cache-stable. The statement is guidance, not an enforcement guard: runtime safety still comes from the registered filesystem, one-shot bash, and terminal backends consuming the same resolved policy.
Focused sandbox-policy tests pin all three texts, canonical roots, switch timing, byte stability, and replay. A keyless assembled ACP snapshot pins the request header through the real Loader composition, while the Web browser scenario drives `/permission` across all modes, inspects each exact `request/header`, and checks the model completes without a probing tool call; record mode exercises the real provider.
Focused tests pin all modes, family combinations, contribution disposal, canonical roots, switch timing, and byte stability across different `TMPDIR` values. Keyless assembled snapshots pin the request header through real Loader compositions, including all three families. Real-provider selection uses pre-registered behavioral endpoints to choose wording, while keyless replay owns the selected denial-to-escalation trajectory.

View File

@@ -12,11 +12,13 @@ Status: implemented
`dsh-sandbox-policy` 负责解析模式与工作区根目录,并注册一个 `sandbox:policy` 系统提示词段落。每次 agent智能体请求都通过 `ctx.sandboxPolicy.resolve({ session })` 直接解析当前会话;不存在拒绝历史扫描、差量叙述器或内存中的「上次告知」状态。
该段落说明当前文件操作模式,且只说明归其所有的后果。`read-only` 表明普通写入、编辑和会修改文件的 shell 操作会被拒绝,但必要的写入目标可能仍可写。`workspace-write` 会列出共享 `writableRoots()` 策略返回的规范化可写根目录:不可变的会话工作区根目录、`/tmp` 与平台临时目录,并在规范化后去重。`danger-full-access` 表明 DSH 文件沙箱不会额外施加文件限制。每种形式都说明主机权限或后端可用性可能施加更多限制,且网络和进程访问不属于该策略的管辖范围
强制执行后端会向策略归属方注册可独立释放的 `filesystem``bash``terminal` 家族贡献。该段落只按规范顺序列出已注册家族,没有家族时为空。这是当前需求,而不是未来扩展:已交付的 headless inheritance 组合将沙箱化文件系统工具与不受围栏约束的一次性 bash 结合,而 persistent-tools 组合则包含沙箱化文件系统工具与终端命令,却没有沙箱化的一次性 bash 执行器。笼统声明在这两种组合中都会失实
该段落只说明每个已注册家族的所有强制执行方言所共有的事实。`read-only` 表明这些操作无法修改文件。`workspace-write` 用非排他措辞说明规范化的会话工作区,并概述某些平台临时区域可能也可写,而不逐一列举。`danger-full-access` 表明 DSH 文件沙箱不会限制这些操作。后端选择的临时路径、`/dev/null`、runner 就绪状态和其他策略领域都不会出现,因为 `resolve()` 无法在请求组装时确定它们。
提供方在正常请求组装期间运行:此时 `/permission` 切换已经提交既有 `sandbox/mode` 事件,`request/header` 尚未记录。因此,渲染后的系统文本就是模型所见确切事实的持久化重建结果。会话状态不变时,重复组装会产生完全相同的字节;恢复与回放会折叠同一条持久模式事件和不可变的 `SessionHeader.cwd`,无需追赶状态。
归属范围保持收敛。批准策略仍由独立的 `approval:policy` 段落负责,计划模式仍由 `plan:policy` 负责,工具插件也继续负责各自的 schema 与操作引导。提示词负责说明策略bash 与文件系统后端仍是强制执行边界。
归属范围保持收敛。批准策略仍由独立的 `approval:policy` 段落负责,计划模式仍由 `plan:policy` 负责,工具插件也继续负责各自的 schema,以及尝试、拒绝与升级引导。提示词负责说明常驻策略;文件系统、一次性 bash 与终端后端仍是强制执行边界。
## 曾考虑的替代方案
@@ -28,10 +30,14 @@ Status: implemented
**在该段落中重复工具 schema或批准与计划引导。** 不予采用,因为这些接口已有各自归属方和独立生命周期。重复内容会造成相互矛盾的请求前缀,并扩大缓存失效范围。
**继续省略沙箱模式,因为常驻模式标签曾引发预防性拒绝。** 后续 Web 证据与已经完成的跨工具族策略否决了这一方案。先前的句子只提到 bash 沙箱,没有说明实际的写入/编辑边界,因此可能与可见工具和升级引导冲突。由归属方派生的段落会说明完整的当前文件操作后果、规范化的工作区范围,并明确说明不作哪些保证,同时不重复工具指令。这只取代[沙箱 Agent Note](2026-07-06-sandbox.md) 中关于省略策略的决策;其中的强制执行与升级边界仍然有效
**继续省略沙箱模式,因为常驻模式标签曾引发预防性拒绝。** 不予采用,因为新的 Web 请求否则会暴露变更工具,却隐去这些工具的常驻策略,导致模型在首次操作前错误声称自身能力。先前的线上测量仍是必须执行的反证测试:使用 `Bash commands run under the "read-only" file sandbox.` 时,十二个轮次中有五个没有调用工具。已提交的工具归属方尝试引导晚于该测量,因此应通过当前工具契约下的新阳性对照实验选择替代文案,而不能假设旧条件与当前条件相同
**独立的模型上下文包。** 不予采用,因为 Cordis 服务可以直接观察当前运行时贡献,而批准与计划策略段落也已经与各自归属方放在一起。新包会为了一个内部适配器引入浅层组合 seam 和额外的文档/门禁表面。
**枚举可写临时根目录。** 不予采用,因为后端要到稍后的 `confine()` 才会选定bwrap、Landlock、Seatbelt 和进程内文件系统围栏并不授予一套共同的临时路径。常驻请求中的主机特定路径既不稳定,也会作出过度承诺。
## 后果
模型可以在试探工具前回答当前可能执行哪些文件操作,且 `/permission` 后的下一个请求会反映已提交的模式。这会增加一个小型动态系统段落,并在策略变化时有意使请求前缀缓存失效;状态不变时仍保持缓存稳定。该声明是引导,而不是强制执行护栏:运行时安全仍来自 `dsh-bash-sandbox``dsh-fs-sandbox` 消费同一项解析完成的策略。
模型可以在试探工具前回答常驻模式管辖哪些已注册文件操作,且 `/permission` 后的下一个请求会反映已提交的模式。这会增加一个小型动态系统段落,并在策略或强制执行家族组合变化时有意使请求前缀缓存失效;状态不变时仍保持缓存稳定。该声明是引导,而不是强制执行护栏:运行时安全仍来自已注册的文件系统、一次性 bash 与终端后端消费同一项解析完成的策略。
聚焦的 sandbox-policy 测试固定了三种文本、规范化根目录、切换时机、字节稳定性与回放。无密钥的组装 ACP 快照通过真实 Loader 组合固定请求 headerWeb 浏览器场景则驱动 `/permission` 在所有模式之间切换,检查每个确切的 `request/header`,并验证模型无需试探性工具调用即可完成;录制模式会使用真实提供方
聚焦测试固定了所有模式、家族组合、贡献释放、规范化根目录、切换时机,以及不同 `TMPDIR` 值下的字节稳定性。无密钥的组装快照通过真实 Loader 组合固定请求 header,包括全部三个家族。真实提供方选型使用预先登记的行为终点指标选择措辞,无密钥回放则负责固定选定的拒绝到升级轨迹