feat(sandbox-policy): expose current file policy

This commit is contained in:
NI0317
2026-07-30 15:48:05 +08:00
parent 007659b78f
commit 7407c26bc1
31 changed files with 506 additions and 43 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: 42b78ad8341dd52c4dd146a2207a5ae909d28f1e
2026-07-06-sandbox.zh.md: dfa3349e4d74d6f2c4944414c25fe3726d4a9b5a
2026-07-06-sandbox.md: ff79d2e1e4dc1501502cfeb2518ddf1065750f1a
2026-07-06-sandbox.zh.md: 1d2ab5ad556f4e1c72a124183b91b951b0ded6dd

View File

@@ -40,7 +40,7 @@ The swap is invisible to every consumer of `ctx.bash`: the bash tools, hook comm
Misconfiguration fails loud: `mode` outside the closed vocabulary is rejected at plugin load, and a host with no usable backend throws the structured `SANDBOX_UNAVAILABLE` — at `confine()` before the command ever spawns — rather than degrading to unconfined execution. `runnerCommand` on `dsh-sandbox-local` is the operator's explicit assertion of a bwrap-compatible runner (chain and probes skipped); it doubles as the deterministic fake-runner seam for keyless tests.
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The prompt does not announce sandbox mode, avoiding preemptive refusal. When `dsh-permission` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP automation composition](../../../../examples/acp-agent/README.md) does not mount that UI service and selects its deployment mode explicitly.
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The owner-derived prompt section states the current file policy without replacing those enforcement boundaries. When `dsh-permission` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP automation composition](../../../../examples/acp-agent/README.md) does not mount that UI service and selects its deployment mode explicitly.
### Design detail
@@ -72,7 +72,7 @@ Backend profiles share the mode contract but differ in necessary host grants. La
`dsh-bash-sandbox` extends `LocalBashExecutor` and hands `ctx.sandbox` the exact `['bash', '-c', command]` argv it is about to spawn. A denial is an orthogonal result fact, conservatively classified from the active runner's stderr dialect. A runner failure outranks denial: foreground execution throws `SANDBOX_UNAVAILABLE`; a settled `BashProcess` stamps `sandbox.runnerFailed`, and the bash producer renders it through generic `task_output`.
The model's view is result facts only: the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`), encourages attempting commands that may be denied, and forbids retrying around a denial; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). No prompt section states the sandbox mode (§ Per-session modes).
The model sees the current effective file policy in the owner-derived `sandbox:policy` request section, while the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`), encourages attempting commands that may be denied, and forbids retrying around a denial; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). [The current-policy decision](2026-07-30-current-sandbox-policy-context.md) owns the section's rationale and boundaries.
#### Escalation: one approved wider retry after a denial
@@ -105,7 +105,7 @@ interface SessionEventMap {
Each owner exports the same three-piece kit: the event declaration, a pure fold (`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)` — a `findLast`, typed to the domain's closed union), and THE write path (`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)` — a switch IS its event; nothing mutates state out of band). No shared owner service, no generic facts map, no registry: a third knob copies the ~40-line pattern into its own package. Execution follows the fold on both sides — the bash tool's per-call stamp reads it as the middle rung of the § Escalation precedence chain, and the approval seam's `'never'` gate is [the approval Agent Note](2026-07-06-approval-seam.md)'s side of the same pattern.
Sandbox mode is not narrated in the prompt; denial results report the mode when it matters, avoiding preemptive refusal based on a standing label. Approval policy is different: only `'never'` is stated because automatic rejection otherwise looks like a user decision. Policy-change notices are coalesced and delivered by the next pre-step, with log-derived fallback after restart. The notice source is inferred from event position: a knob event after the last request header is user-driven; unlogged drift is operator or config driven.
Sandbox policy is rendered from the current mode and workspace root before every request; the existing `request/header` records the exact section, so no switch narrator or last-told state is needed. Approval policy stays separate: only `'never'` is stated because automatic rejection otherwise looks like a user decision, and its owner retains its own switch narration.
**The optional UI surface** is `PermissionService`: a deployment-defined preset table whose entries bundle one sandbox mode with one approval policy. The shipped `workspace-write` and `danger-full-access` presets write through to both domain setters; a knob combination outside the table is reported as `custom`. UI adapters may expose that table as a selector. The automation-only ACP transport advertises no configuration selector and mounts no permission-preset service.
@@ -120,7 +120,7 @@ fs/web/todo execute in-process, so their sandbox semantics are policy at their s
- **Unit:** pin platform selection and profiles, fail-closed runner classification, per-call mode/root resolution, per-process facts, escalation validation and outcomes, permission preset folding and write-through, and narrator coalescing.
- **Keyless real-runner:** exercise bwrap, Landlock, and Seatbelt against real filesystem effects at provider and bash-consumer layers; one real Cordis context concurrently drives two project sessions through shipped bash and fs tools, proving own-root success and sibling-root denial. Packed-install coverage proves the registry launcher remains executable. CI rejects a silent all-skip.
- **With-key:** start the real ACP composition in read-only mode, let a model-driven bash write hit the runner's denial marker, then drive the bridge answerer and disk effect through granted and rejected workspace-write retries; unavailable credentials or runners self-skip.
- **Snapshot:** pin prompt deltas and notices plus both scripted approval branches. A real ACP example scenario places its session under the user home while the deployment fallback points at `/tmp`, then pins a successful deployment-selected workspace-write mutation; this distinguishes session-root resolution from the process fallback without depending on runner-specific denial text. Other snapshots start unconfined so unrelated fixtures remain platform-independent.
- **Snapshot:** pin the current policy section and both scripted approval branches. A real ACP example scenario places its session under the user home while the deployment fallback points at `/tmp`, then pins both the workspace-write request header and a successful deployment-selected mutation; this distinguishes session-root resolution from the process fallback without depending on runner-specific denial text. Other snapshots start unconfined so unrelated fixtures remain platform-independent.
## Deferred phases
@@ -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 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".
- **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.
- **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.
@@ -160,7 +160,7 @@ What shipped pins — the tiers in Testing hold each:
- A denied command retried with `sandbox_permissions` + `justification` prompts the user through the composed answerer chain; a grant runs THAT call under the wider mode (result facts say so) while every other call keeps its own effective mode; every non-grant outcome produces its distinct error text and executes nothing.
- The escalation fields exist exactly when the mounted executor confines; a request that is not strictly wider than the call's effective mode fails closed with its own text and prompts no one; a deployment with no ApprovalService fails escalating calls closed and leaves plain calls untouched.
- The system prompt never states the sandbox mode (an approval `'never'` policy is the one stated knob), and the whole exchange — headers, knob events, notices, approvals, results — reconstructs from the session log alone, with no event types beyond the two knob events.
- The system prompt states sandbox policy and approval policy in separate owner-derived sections; the whole exchange — headers, knob events, approval notices, approvals, and results — reconstructs from the session log alone, with no event types beyond the two knob events.
- One preset selection records only changed knob values, while a no-op selection records nothing; an approval-policy switch is narrated in at most one coalesced notice, and a committed sandbox switch is honored by the next call's stamp.
- A resumed session's overrides apply with no catch-up state; a default changed while the process was down is narrated before the session's first new request, attributed to the operator.
- Two concurrent sessions never see each other's state or notices.
@@ -179,8 +179,7 @@ Costs and accepted limits:
- **The advertised target set is static while the effective mode is per-session** (schemas are registry-global) — a session already at the widest mode is still offered the fields. Harmless by construction: the strict-wider check at execution, not the enum, is the safety boundary — a non-widening request fails with its own text and never prompts anyone.
- **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 section is still a dynamic prompt surface** (a `'never'` switch breaks provider prompt-prefix caching for that session). Accepted: policy switches are rare, and a model acting on a stale `'never'` is worse. The sandbox knob no longer touches the prompt at all.
- **The model may hold a stale belief about the sandbox mode** (nothing announces a switch). Accepted deliberately: 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 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.
## FAQ
@@ -191,7 +190,7 @@ Costs and accepted limits:
- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam.
- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary).
- **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry.
- **When does a runtime mode switch take effect?** Once its session event commits, the very next capability resolution folds and stamps the new mode. The model is not told a standing mode; its next command simply behaves under the new policy, and any denial names that policy at the point of use.
- **When does a runtime mode switch take effect?** Once its session event commits, the next request section and the next capability resolution fold the new mode. The request header records what the model was told, and any later denial names the same policy at the point of use.
- **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline changes behavior the same way a switch does (the approval policy, being stated, is additionally narrated with operator/config attribution).
- **What does `enforcement: 'partial'` on a result mean?** The selected backend enforces the subset its kernel ABI governs — e.g. Landlock before ABI v3 does not govern path truncate — and says so structurally instead of refusing the host; the probe's report line distinguishes the cases. The bwrap and Seatbelt profiles govern every promised file effect by construction, so they always report `full`.

View File

@@ -40,7 +40,7 @@ harness 是一个 SDK因此约束必须是开发者可组合的能力
配置错误大声失败:`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` 与某个 UI 适配器一起组合时,一个 preset 同时选定两个旋钮值;不匹配的组合折叠为 `custom`。[ACP 自动化组合](../../../../examples/acp-agent/README.md)不挂载该 UI 服务,而是显式选定其部署模式。
被拒绝的文件操作返回 `[sandbox: file access denied under <mode> mode]` 标记,并附带不要绕过拒绝的指令。约束执行器添加配对的 `sandbox_permissions``justification` 字段,用于一次经批准的重试,该重试必须严格宽于会话的有效模式。授权仅放宽该次重试;拒绝则不执行任何内容,返回 `the user rejected escalating this command to "<mode>"`,且不允许再次请求。由归属方派生的提示词段落会说明当前文件策略,但不会取代这些强制执行边界。当 `dsh-permission` 与某个 UI 适配器一起组合时,一个 preset 同时选定两个旋钮值;不匹配的组合折叠为 `custom`。[ACP 自动化组合](../../../../examples/acp-agent/README.md)不挂载该 UI 服务,而是显式选定其部署模式。
### 设计细节
@@ -72,7 +72,7 @@ Landlock launcher 源码和包工作区位于 `native/landlock-run`,与 harnes
`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]`),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试当升级字段被公布时,被拒绝的结果还额外携带升级提示本身,使被认可的同轮次重试在决策点提示,而非依赖模型回忆描述(§ 升级机制)。没有提示词段落声明沙箱模式(§ 按会话模式)
模型会在归属方派生的 `sandbox:policy` 请求段落中看到当前有效的文件策略;静态工具描述解释拒绝标记(`[sandbox: file access denied under <mode> mode]`),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试当升级字段被公布时,被拒绝的结果还携带升级提示本身,使被认可的同轮次重试在决策点获得提示,而非依赖模型回忆描述(§ 升级机制)。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)负责该段落的理由与边界
#### 升级机制:拒绝后一次经批准的更宽重试
@@ -105,7 +105,7 @@ interface SessionEventMap {
每个拥有者导出相同的三件套:事件声明、纯 fold`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)`——一个 `findLast`,类型化到领域的封闭联合),以及唯一的写入路径(`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)`——切换即其事件;没有任何东西在带外修改状态)。无共享拥有者服务、无通用 facts map、无注册表第三个旋钮只需将约 40 行模式复制到自己的包中。执行在两侧都遵循 fold——bash 工具的按调用盖章将其作为 § 升级机制优先级链的中间层读取approval seam 的 `'never'` 门控是[批准 Agent Note](2026-07-06-approval-seam.md) 同一模式的另一侧。
沙箱模式不在提示词中叙述;拒绝结果在需要时报告模式,避免基于常驻标签的预防性拒绝。批准策略不同:只有 `'never'` 被声明,因为自动拒绝在行为上与用户的「不」无法区分。策略变更通知被合并,由下一个步骤前检查点递送,重启后有基于日志的回退。通知来源从事件位置推断:最后一个 request header 之后的旋钮事件是用户驱动的;未记录的漂移是运维人员或配置驱动的
每次请求前都会根据当前模式与工作区根目录渲染沙箱策略;现有 `request/header` 会记录确切段落,因此无需切换叙述器或「上次告知」状态。批准策略保持独立:只有 `'never'` 被声明,因为自动拒绝在行为上与用户的「不」无法区分,其归属方继续保留自己的切换叙述
**可选的 UI 界面**是 `PermissionService`:一张部署定义的 preset 表,每个条目捆绑一个沙箱模式与一个批准策略。随附的 `workspace-write``danger-full-access` preset 写入两个领域 setterpreset 表之外的旋钮组合报告为 `custom`。UI 适配器可以把该表暴露为选择器。仅面向自动化的 ACP 传输层不公布任何配置选择器,也不挂载权限 preset 服务。
@@ -120,7 +120,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **单元测试:** 固定平台选择和 profile、失败关闭的 runner 分类、按调用的模式/根目录解析、按进程事实、升级验证和结果、权限 preset fold 和写入透传、以及叙述器合并。
- **Keyless 真实 runner** 在提供方和 bash 消费方层面对 bwrap、Landlock 和 Seatbelt 执行真实文件系统效果测试;一个真实 Cordis 上下文通过已交付的 bash 和 fs 工具并发驱动两个项目会话证明在自身根目录写入成功、在兄弟根目录写入被拒绝。Packed-install 覆盖率证明注册表 launcher 保持可执行。CI 拒绝静默全跳过。
- **With-key** 以只读模式启动真实 ACP 组合,让模型驱动的 bash 写入命中 runner 的拒绝标记,再通过已授权与被拒绝的 workspace-write 重试驱动 bridge 应答器和磁盘效果;不可用的凭证或 runner 自动跳过。
- **快照:** 固定提示词 delta 和通知,以及两个脚本化的 approval 分支。一个真实 ACP 示例场景把会话放在用户主目录下,同时让部署后备根目录指向 `/tmp`,然后固定一次成功的、由部署选定的 workspace-write 变更;这能区分会话根目录解析与进程后备值,而不依赖 runner 特定的拒绝文本。其他快照以无约束启动,使无关 fixture测试前置数据保持平台无关。
- **快照:** 固定当前策略段落和两个脚本化的 approval 分支。一个真实 ACP 示例场景把会话放在用户主目录下,同时让部署后备根目录指向 `/tmp`,然后固定 workspace-write 请求 header 与一次成功的、由部署选定的变更;这能区分会话根目录解析与进程后备值,而不依赖 runner 特定的拒绝文本。其他快照以无约束启动,使无关 fixture测试前置数据保持平台无关。
## 延迟阶段
@@ -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 commands run under the "read-only" file sandbox.` 时,模型拒绝尝试被拒绝后可升级的工作(首次手动会话中十二个轮次有五个以零工具调用结束),将沙箱变成了软锁定。拒绝标记在需要时命名模式,升级字段承载恢复路径;批准旋钮保留其声明,因为自动拒绝在行为上与人的「不」无法区分
- **仅限 bash 的模式标签加切换叙述器**否决。`Bash commands run under the "read-only" file sandbox.` 会引发预防性拒绝,同时没有明确文件系统工具的后果与工作区范围。当前由归属方派生的段落采用不同契约:后续 Web 证据表明,完全缺失策略会导致模型在首次工具调用前错误声称自身能力,而跨工具族强制现在能够提供一项完整的文件操作策略。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)记录了为何用简洁的当前状态取代缺失策略的选择,同时不重复工具引导
- **用专门的簿记事件追踪「上次告知」**:否决。`request/header` fold 已记录模型看到的确切提示词;将封闭的候选句子解析回来替代了第二条簿记流——事件仅在它们本身即为存储时才需要。
- **相互独立的沙箱与批准选择器**:否决。一个部署定义的权限 preset 让两个策略旋钮对暴露运行时切换的 UI 客户端保持一致。
@@ -160,7 +160,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- 被拒绝的命令以 `sandbox_permissions` + `justification` 重试时,通过组合的应答器链提示用户;授权使该次调用在更宽模式下运行(结果事实如此报告),而其他所有调用保持各自的有效模式;每种非授权结果产生各自不同的错误文本且不执行任何内容。
- 升级字段恰好在已挂载的执行器约束时存在;不严格宽于调用有效模式的请求以自身文本失败关闭且不提示任何人;没有 ApprovalService 的部署对升级调用失败关闭,对普通调用不影响。
- 系统提示词从不声明沙箱模式(批准 `'never'` 策略是唯一被声明的旋钮),且整个交互——header、旋钮事件、通知、批准结果——仅从会话日志即可重建,除两个旋钮事件外无额外事件类型。
- 系统提示词在由各自归属方派生的独立段落中分别声明沙箱策略与批准策略;整个交互——header、旋钮事件、批准通知、批准结果——仅从会话日志即可重建,除两个旋钮事件外无额外事件类型。
- 一次 preset 选择只记录发生变化的旋钮值,而无操作的选择不记录任何内容;批准策略切换最多以一条合并通知叙述,已提交的沙箱切换由下一次调用的盖章兑现。
- 恢复的会话的覆盖直接生效,无需追赶状态;进程停止期间变更的默认值在会话的首个新请求前被叙述,归因于运维人员。
- 两个并发会话永远看不到彼此的状态或通知。
@@ -179,8 +179,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **公布的目标集是静态的,而有效模式是按会话的**schema 是注册表全局的)——已处于最宽模式的会话仍被提供这些字段。构造上无害:执行时的严格放宽检查(而非枚举)是安全边界——非放宽请求以自身文本失败且不提示任何人。
- **授权的升级不等于可工作的沙箱。** 不可用的后端即使对授权升级到约束模式也仍然失败关闭——在平台没有链或所有探测失败时于 `confine()` 阶段,在未探测的唯一 runner 拒绝时于执行阶段(归类为沙箱失败而非命令失败)——而授权的 `danger-full-access` 运行根本不触及提供方:此时授权(而非探测)是权威。
- **批准叙述器的重启基线解析提示词文本。** 封闭的候选句子由写入模块本身拥有,因此措辞变更是同一文件中写入器+解析器的协调编辑header 早于该段落的会话静默采用当前策略而不发通知。
- **批准段落是动态提示词表面**`'never'` 切换会破坏该会话的提供方提示词前缀缓存)。已接受:策略切换罕见,且模型基于过时的 `'never'` 行动更糟。沙箱旋钮不再触及提示词
- **模型可能持有关于沙箱模式的过时信念**(没有任何东西宣布切换)。有意接受:下一次尝试的标记或成功会纠正它,而宣布的观察到的失败模式——预防性拒绝——比一次浪费的重试更糟。
- **批准段落与沙箱段落都是动态提示词表面** 策略切换会破坏该会话的提供方提示词前缀缓存;状态不变时仍保持字节稳定,且模型基于过时权限行动的风险高于这种有限的缓存失效
## FAQ
@@ -191,7 +190,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **沙箱限制网络或进程可见性吗?** 不——`SandboxMode` 仅声称文件操作bwrap profile 刻意不 unshare pid没有后端声称网络。网络限制是否成为自己的旋钮留在 § seam 中开放。
- **哪些工具实际在约束下运行?** 通过 `ctx.bash` 的 OS 子进程——bash 工具及传递性的钩子命令——再加上通过沙箱化 `ctx.fs` 提供方运行的文件系统工具(`read`/`write`/`edit`,见[跨工具族 fs 沙箱 Agent Note](2026-07-14-cross-family-fs-sandbox.md)bash 通过 OS runner 约束fs 通过进程内路径围栏约束,二者都以同一个 `ctx.sandboxPolicy` 模式为键。web/todo 仍在进程内且不受限制web 的唯一效果是网络,不在文件效果模式词汇内)。
- **授权的升级会持久化吗?** 不会。授权由发起请求的确切前台或后台调用消费;每个相邻调用保留自己的有效模式。后续的后台拒绝通过 `task_output` 呈现,并且可以作为一次新的精确命令重试的依据。
- **运行时模式切换何时生效?** 一旦其会话事件提交,紧接着的下一次能力解析会折叠并盖章新模式。模型不被告知常驻模式;其下一个命令直接在新策略下运行,任何拒绝都会在使用点命名策略。
- **运行时模式切换何时生效?** 一旦其会话事件提交,下一个请求段落与下一次能力解析会折叠新模式。请求 header 会记录模型收到的内容,之后的任何拒绝都会在使用点命名同一策略。
- **重启后什么存活——如果运维人员在进程停止期间改了配置默认值呢?** 覆盖从会话日志回放(`effective = fold ?? config`),因此恢复的会话以零追赶机制保持其模式;离线漂移的默认值以与切换相同的方式改变行为(批准策略因被声明,还额外以运维人员/配置归因叙述)。
- **结果上的 `enforcement: 'partial'` 是什么意思?** 所选后端强制其内核 ABI 管控的子集——例如 ABI v3 之前的 Landlock 不管控路径 truncate——并以结构化方式如此声明而非拒绝主机探测的报告行区分各种情况。bwrap 和 Seatbelt profile 构造上管控所有承诺的文件操作,因此始终报告 `full`

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 .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

View File

@@ -0,0 +1,37 @@
# Agent Note: Current sandbox policy context
Status: implemented
English | [中文](2026-07-30-current-sandbox-policy-context.zh.md)
## Problem
The sandbox policy already enforced and logged each session's file-effect mode, but a fresh model request did not contain that state. In a Web session under `read-only`, write and edit schemas remained visible, so the model claimed it could write and learned otherwise only after a denied call. After `/permission danger-full-access`, the next request carried the approval-policy change but still omitted the sandbox mode. Denial results were therefore the first model-visible policy source even when the user asked about capability before any operation.
## Decision
`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.
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.
## Alternatives considered
**Narrate only mode changes.** Rejected because it leaves a fresh session uninformed and makes the first denied operation the policy-discovery mechanism. It also requires a baseline definition that is unnecessary when current state can be rendered directly.
**Scan denial history or remember the last narrated mode.** Rejected because denial events describe attempted operations, not authoritative current state, while process-local bookkeeping does not survive resume. The owner can fold the durable policy directly on every request.
**A generic runtime-facts registry.** Rejected because the existing system-prompt registry already evaluates owner-provided sections with the live agent at request time. One policy owner has no cross-domain invariant that justifies another package or registry.
**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.
## 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.
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.

View File

@@ -0,0 +1,37 @@
# Agent Note: 当前沙箱策略上下文
Status: implemented
[English](2026-07-30-current-sandbox-policy-context.md) | 中文
## 问题
沙箱策略已经强制执行并记录每个会话的文件操作模式,但新的模型请求并不包含这一状态。在 `read-only` 下的 Web 会话中write 与 edit schema 仍然可见,因此模型会声称自己能够写入,直到一次被拒绝的调用后才发现事实并非如此。执行 `/permission danger-full-access` 后,下一个请求带有批准策略变更,却仍省略沙箱模式。因此,即使用户在任何操作前询问能力,拒绝结果也是模型可见的首个策略来源。
## 决策
`dsh-sandbox-policy` 负责解析模式与工作区根目录,并注册一个 `sandbox:policy` 系统提示词段落。每次 agent智能体请求都通过 `ctx.sandboxPolicy.resolve({ session })` 直接解析当前会话;不存在拒绝历史扫描、差量叙述器或内存中的「上次告知」状态。
该段落说明当前文件操作模式,且只说明归其所有的后果。`read-only` 表明普通写入、编辑和会修改文件的 shell 操作会被拒绝,但必要的写入目标可能仍可写。`workspace-write` 会列出共享 `writableRoots()` 策略返回的规范化可写根目录:不可变的会话工作区根目录、`/tmp` 与平台临时目录,并在规范化后去重。`danger-full-access` 表明 DSH 文件沙箱不会额外施加文件限制。每种形式都说明主机权限或后端可用性可能施加更多限制,且网络和进程访问不属于该策略的管辖范围。
提供方在正常请求组装期间运行:此时 `/permission` 切换已经提交既有 `sandbox/mode` 事件,`request/header` 尚未记录。因此,渲染后的系统文本就是模型所见确切事实的持久化重建结果。会话状态不变时,重复组装会产生完全相同的字节;恢复与回放会折叠同一条持久模式事件和不可变的 `SessionHeader.cwd`,无需追赶状态。
归属范围保持收敛。批准策略仍由独立的 `approval:policy` 段落负责,计划模式仍由 `plan:policy` 负责,工具插件也继续负责各自的 schema 与操作引导。提示词负责说明策略bash 与文件系统后端仍是强制执行边界。
## 曾考虑的替代方案
**仅叙述模式变更。** 不予采用,因为这会让新会话不了解策略,并把首次被拒绝的操作变成策略发现机制。如果可以直接渲染当前状态,也就无需额外定义基线。
**扫描拒绝历史或记住上次叙述的模式。** 不予采用,因为拒绝事件描述的是尝试过的操作,而不是权威的当前状态;进程本地的簿记也无法跨恢复保留。归属方可以在每次请求时直接折叠持久策略。
**通用运行时事实注册表。** 不予采用,因为现有系统提示词注册表已经会在请求时使用当前 agent 评估归属方提供的段落。单一策略归属方不存在需要另一个包或注册表来承载的跨领域不变式。
**在该段落中重复工具 schema或批准与计划引导。** 不予采用,因为这些接口已有各自归属方和独立生命周期。重复内容会造成相互矛盾的请求前缀,并扩大缓存失效范围。
**继续省略沙箱模式,因为常驻模式标签曾引发预防性拒绝。** 后续 Web 证据与已经完成的跨工具族策略否决了这一方案。先前的句子只提到 bash 沙箱,没有说明实际的写入/编辑边界,因此可能与可见工具和升级引导冲突。由归属方派生的段落会说明完整的当前文件操作后果、规范化的工作区范围,并明确说明不作哪些保证,同时不重复工具指令。这只取代[沙箱 Agent Note](2026-07-06-sandbox.md) 中关于省略策略的决策;其中的强制执行与升级边界仍然有效。
## 后果
模型可以在试探工具前回答当前可能执行哪些文件操作,且 `/permission` 后的下一个请求会反映已提交的模式。这会增加一个小型动态系统段落,并在策略变化时有意使请求前缀缓存失效;状态不变时仍保持缓存稳定。该声明是引导,而不是强制执行护栏:运行时安全仍来自 `dsh-bash-sandbox``dsh-fs-sandbox` 消费同一项解析完成的策略。
聚焦的 sandbox-policy 测试固定了三种文本、规范化根目录、切换时机、字节稳定性与回放。无密钥的组装 ACP 快照通过真实 Loader 组合固定请求 headerWeb 浏览器场景则驱动 `/permission` 在所有模式之间切换,检查每个确切的 `request/header`,并验证模型无需试探性工具调用即可完成;录制模式会使用真实提供方。

View File

@@ -0,0 +1,126 @@
// Web acceptance for current sandbox-policy context. A real Chromium drives
// the shipped /permission command through all three presets; record mode uses
// the real provider, while replay keeps the same provider-authored behavior
// keyless. Assertions read the exact durable request headers and tool calls,
// so assistant prose alone cannot satisfy the scenario.
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
assertFixtureInventory, fixtureUserPrompts, launchWebScaffold, recordFixture,
watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/permission-policy-context', import.meta.url))
const FIXTURE = fileURLToPath(new URL('./snapshots/permission-policy-context/session.jsonl', import.meta.url))
const MODE = webSnapshotMode()
const PROMPTS = [
'Can you create or edit a normal file right now under the current policy? Answer directly in one sentence. Do not call a tool just to discover the policy.',
'Does the DSH file sandbox currently restrict file operations? Answer directly in one sentence. Do not call tools.',
'Under the current DSH file policy, where are writes allowed? Answer directly in one sentence. Do not call tools.',
] as const
const PRESET_LABELS = ['Read Only', 'Danger Full Access', 'Workspace Write'] as const
function requestSystems(events: readonly SessionEvent[]): string[] {
return events.flatMap((event) => {
if (event.type !== 'request/header') return []
return typeof event.data.header.system === 'string' ? [event.data.header.system] : []
})
}
function assistantTexts(events: readonly SessionEvent[]): string[] {
return events.flatMap((event) => {
if (event.type !== 'assistant/message') return []
return [event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('').replaceAll('**', '')]
})
}
describe('web e2e: current sandbox policy reaches the model before tools', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
let sessionWorkspace: string | undefined
const sessionEvents: SessionEvent[] = []
beforeAll(async () => {
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE })
scaffold.ctx.on('session/event', (session, event: SessionEvent) => {
sessionWorkspace = session.header.cwd
sessionEvents.push(event)
})
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await connectFreshWorkspace(page)
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('switches read-only, danger-full-access, and workspace-write through the real GUI command path', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-permission-policy-context'))
if (MODE !== 'record') {
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual(PROMPTS)
}
const input = page.locator('textarea').first()
let sessionId: Awaited<ReturnType<WebScaffold['whenTurnSettled']>> | undefined
for (const [index, preset] of ['read-only', 'danger-full-access', 'workspace-write'].entries()) {
await input.fill(`/permission ${preset}`)
await input.press('Enter')
await page.getByRole('button', { name: `Access mode, current: ${PRESET_LABELS[index]}` })
.waitFor({ timeout: 10_000 })
const settled = scaffold.whenTurnSettled()
await input.fill(PROMPTS[index] as string)
await input.press('Enter')
sessionId = await settled
await expect.poll(() => input.isEnabled(), { timeout: 10_000 }).toBe(true)
}
if (sessionId === undefined) throw new Error('permission-policy scenario completed no model turn')
if (MODE === 'record') await recordFixture(scaffold, sessionId, FIXTURE)
}, 240_000)
it.skipIf(MODE === 'record')('records each effective policy before the corresponding model behavior', () => {
const systems = requestSystems(sessionEvents)
expect(systems).toHaveLength(3)
expect(systems[0]).toContain('Current DSH file sandbox policy: read-only. Ordinary file writes, edits, and file-mutating shell effects are denied')
expect(systems[1]).toContain('Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations.')
expect(systems[1]).toContain('Approval prompts are disabled in this session')
if (sessionWorkspace === undefined) throw new Error('permission-policy scenario observed no session workspace')
const policy = {
mode: 'workspace-write' as const,
workspaceRoot: canonicalPath(sessionWorkspace),
}
const roots = writableRoots(policy)
expect(systems[2]).toContain(`Current DSH file sandbox policy: workspace-write. File writes, edits, and file-mutating shell effects are limited to these canonical writable roots: ${roots.map(root => JSON.stringify(root)).join(', ')}.`)
expect(systems[2]).not.toContain('Approval prompts are disabled in this session')
const answers = assistantTexts(sessionEvents)
expect(answers).toHaveLength(3)
expect(answers[0]).toMatch(/cannot create or edit normal files|writes?.*denied/i)
expect(answers[1]).toMatch(/does not.*restrict file operations|not restrict.*file operations/i)
expect(answers[2]).toMatch(/workspace.*(temporary|temp)|writable roots/i)
expect(sessionEvents.filter(event => event.type === 'tool/call')).toHaveLength(0)
})
it.skipIf(MODE === 'record')('stays clean and keeps the fixture inventory closed', async () => {
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
})
})

File diff suppressed because one or more lines are too long

View File

@@ -38,7 +38,8 @@
"tests/cordis-tool-round.e2e.ts",
"tests/message-actions.e2e.ts",
"tests/queue-actions.e2e.ts",
"tests/skill-invocation-policy.e2e.ts"
"tests/skill-invocation-policy.e2e.ts",
"tests/permission-policy-context.e2e.ts"
],
"references": [
{

View File

@@ -1007,7 +1007,7 @@ export interface Config {
Depends on: [`SandboxMode`](core-data-structures/sandbox.md)
Source: [`packages/sandbox/sandbox-policy/src/index.ts:44`](../packages/sandbox/sandbox-policy/src/index.ts)
Source: [`packages/sandbox/sandbox-policy/src/index.ts:66`](../packages/sandbox/sandbox-policy/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-jsonl`

View File

@@ -1017,7 +1017,7 @@ Source: [`packages/sandbox/sandbox/src/index.ts:131`](../../packages/sandbox/san
## `ctx.sandboxPolicy` — `SandboxPolicyService`
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode and fallback workspace root. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
```ts cordis-catalog
/**
@@ -1041,7 +1041,7 @@ overrideOf(session: Session): SandboxMode | undefined
Types: [SandboxExecutionPolicy](../core-data-structures/sandbox.md) · [SandboxMode](../core-data-structures/sandbox.md) · [SandboxPolicyRequest](../core-data-structures/sandbox.md) · [Session](../core-data-structures/session.md)
Source: [`packages/sandbox/sandbox-policy/src/index.ts:68`](../../packages/sandbox/sandbox-policy/src/index.ts)
Source: [`packages/sandbox/sandbox-policy/src/index.ts:90`](../../packages/sandbox/sandbox-policy/src/index.ts)
## `ctx.sessionPersistence` — `SessionPersistence` (abstract seam)

View File

@@ -36,7 +36,7 @@
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: danger-full-access
mode: workspace-write
workspaceRoot: /tmp
- insert:
- id: llm-replay

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: workspace-write. File writes, edits, and file-mutating shell effects are limited to these canonical writable roots: "{{cwd}}", "/private/tmp", "/private/var/folders/8k/kj35k1fd6t90n0czg7k3hv140000gn/T". Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
<!-- dsh-user-approval-policy:ask -->

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use search/read for ordinary navigation. Use lsp when textual matches are ambiguous or before a change requires precise definitions, implementations, or references. Positions are one-based line and character (UTF-16) at the cursor; an off-symbol position may return no results. findReferences always includes the declaration.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.

View File

@@ -17,6 +17,8 @@ Use a terminal session only when work needs persistent terminal state or interac
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use session_search to find relevant work from prior sessions, or session_event_search to search earlier events in one session. Search results are cursor-free and workspace-scoped. Follow a useful hit with session_trace, session_event_trace, or session_event_read when you need lineage, relationships, or exact data.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use the web_fetch tool to retrieve the content of a specific HTTP(S) URL (for example a result from web_search). It returns the page content decoded to text. Cite the URL as a markdown link when you use its content.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.

View File

@@ -15,6 +15,8 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).

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 packages/sandbox/sandbox-policy/README.md
README.md: dca54330bc888af9ecac21aa92019d8a2b0140bd
README.zh.md: a201d48c81f563fc3d85495e964bb67432517a3c
README.md: 3258492ba80102ec96d37baa5e4989b1c396cf15
README.zh.md: 4bb7e3b1321620413b81414d60f7b2d02df588c6

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The single owner of sandbox-policy resolution: the deployment's default [`SandboxMode`](../sandbox/README.md) and fallback root, plus each session's durable mode override and immutable workspace root. Every enforcing capability family receives one resolved mode-and-root policy per call.
The single owner of sandbox-policy resolution: the deployment's default [`SandboxMode`](../sandbox/README.md) and fallback root, plus each session's durable mode override and immutable workspace root. Every enforcing capability family receives one resolved mode-and-root policy per call, and the model receives that same effective policy before each request.
## Why a shared home
@@ -17,23 +17,50 @@ Two families enforce the same mode vocabulary: the sandboxed bash executor (`@de
- `ctx.sandboxPolicy.resolve({ session?, mode? })` — resolves one complete per-call policy. An explicit approved mode outranks the session's last `sandbox/mode` event, which outranks `defaultMode`; the session's immutable `cwd` is canonicalized with filesystem semantics before becoming `workspaceRoot`, otherwise the configured fallback applies. Canonicalization precedes lexical normalization so `symlink/..` agrees with process working-directory resolution.
- `ctx.sandboxPolicy.defaultMode` / `ctx.sandboxPolicy.workspaceRoot` — the deployment default and fallback root used by `resolve()`.
- `sandbox:policy` — a request-time system-prompt section derived from `resolve({ session })`. It states the current file-effect mode, its consequences, and every canonical writable root under `workspace-write`; it does not claim host permissions, sandbox-backend readiness, or network/process restrictions.
- `effectiveSandboxMode(events)` — the pure fold of a session's `sandbox/mode` events (the last switch wins, or `undefined`), used inside `resolve()`.
- `setSandboxMode(session, mode)` — THE write path for a per-session override: appends exactly one `sandbox/mode` event. The switch IS its event; nothing mutates the mode out of band.
- `SANDBOX_MODES` — every mode, for option advertisement and runtime validation.
The optional `./invariant` companion rejects a forged durable `sandbox/mode` event whose value falls outside that closed vocabulary; Session and its companion own the surrounding storage and core execution-enclosure rules.
The optional `./invariant` companion rejects a forged durable `sandbox/mode` event whose value falls outside that closed vocabulary; Session and its companion own the surrounding storage and core execution-enclosure rules. The rendered section is logged inside `request/header`, so the exact effective policy remains reconstructable without another event or an in-memory “last told” mirror.
## The per-session store
A runtime switch is one log-only `sandbox/mode` event on the session it applies to. `effective = explicit grant ?? fold(events) ?? deployment default`, so an override survives restart by replay and two sessions never see each other's state. Workspace identity does not need another event: the immutable `SessionHeader.cwd` recorded at creation is the root for every call in that session. The event is log-only (the `approval/*` precedent): the model learns the mode from the enforcing tools' denial markers, never from the event.
A runtime switch is one log-only `sandbox/mode` event on the session it applies to. `effective = explicit grant ?? fold(events) ?? deployment default`, so an override survives restart by replay and two sessions never see each other's state. Workspace identity does not need another event: the immutable `SessionHeader.cwd` recorded at creation is the root for every call in that session. The event stays log-only; the next request assembles the current section from the fold before any tool call.
## Model Experience
Indirectly, through `dsh-tool-bash` and `dsh-tool-fs`, which render the effective mode this service holds in their `[sandbox: …]` denial markers and escalation prompts; the `sandbox/mode` event itself never reaches the model.
### Current file sandbox policy
#### What the model sees
One `sandbox:policy` system section on every agent request. The section states only DSH file-effect policy; tool schemas remain their owners' surfaces, approval policy remains `dsh-user-approval`'s section, and plan guidance remains `dsh-plan-mode`'s section.
##### Read-only
```markdown
Current DSH file sandbox policy: read-only. Ordinary file writes, edits, and file-mutating shell effects are denied; required sinks such as `/dev/null` may remain writable. Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.
```
##### Workspace-write
```markdown
Current DSH file sandbox policy: workspace-write. File writes, edits, and file-mutating shell effects are limited to these canonical writable roots: "<workspace root>", "<temporary root>". Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.
```
##### Danger-full-access
```markdown
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
```
#### Token effect
One concise system section per request. `workspace-write` additionally lists the canonical session workspace root plus the canonical `/tmp` and platform temporary roots, deduplicated when they identify the same directory.
#### KV Cache effect
No direct invalidation; the named consumers own any request-prefix changes, and the mode is deliberately absent from the prompt.
The request prefix is byte-stable while the session mode and immutable workspace root stay unchanged. A mode switch changes the section on the next request; the resulting `request/header` records the new prefix.
## Known Limitations and Deferred Work

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每个强制执行策略的能力家族在每次调用时都会收到一项解析完成的模式与根目录策略。
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每个强制执行策略的能力家族在每次调用时都会收到一项解析完成的模式与根目录策略,模型也会在每次请求前收到同一项有效策略
## 为何需要共享归属位置
@@ -17,23 +17,50 @@
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话最后一条 `sandbox/mode` 事件,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
- `ctx.sandboxPolicy.defaultMode``ctx.sandboxPolicy.workspaceRoot``resolve()` 使用的部署默认值与回退根目录。
- `sandbox:policy`:由 `resolve({ session })` 派生的请求时系统提示词段落。它说明当前文件操作模式及其后果,并列出 `workspace-write` 下所有规范化的可写根目录;不会声称主机权限、沙箱后端就绪状态或网络/进程限制。
- `effectiveSandboxMode(events)`:会话 `sandbox/mode` 事件的纯 fold最后一次切换胜出没有则为 `undefined`),在 `resolve()` 内使用。
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
可选的 `./invariant` 配套组件会拒绝伪造的持久 `sandbox/mode` 事件只要其值不在该封闭词汇中Session 与其配套组件负责相关存储与核心执行封闭规则。
可选的 `./invariant` 配套组件会拒绝伪造的持久 `sandbox/mode` 事件只要其值不在该封闭词汇中Session 与其配套组件负责相关存储与核心执行封闭规则。渲染后的段落记录在 `request/header` 中,因此无需另一条事件或内存中的「上次告知」镜像,也能重建确切的有效策略。
## 逐会话存储
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`,因此覆盖会通过回放跨重启保留,两个会话也绝不会看到彼此状态。工作区标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件只进入日志(沿用 `approval/*` 先例):模型通过强制执行工具的拒绝标记获知模式,绝不会从事件获知
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`,因此覆盖会通过回放跨重启保留,两个会话也绝不会看到彼此状态。工作区标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件只进入日志;下一次请求会在任何工具调用发生前,根据 fold 组装当前段落
## 模型体验
通过 `dsh-tool-bash``dsh-tool-fs` 间接影响;它们会在 `[sandbox: …]` 拒绝标记和升权提示词中渲染该服务持有的有效模式,`sandbox/mode` 事件本身绝不会到达模型。
### 当前文件沙箱策略
#### 模型看到的内容
每次 agent 请求都有一个 `sandbox:policy` 系统段落。该段落只说明 DSH 文件操作策略;工具 schema 仍由各自归属方管理,批准策略仍由 `dsh-user-approval` 的段落管理,计划引导仍由 `dsh-plan-mode` 的段落管理。
##### 只读
```markdown
Current DSH file sandbox policy: read-only. Ordinary file writes, edits, and file-mutating shell effects are denied; required sinks such as `/dev/null` may remain writable. Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.
```
##### 工作区写入
```markdown
Current DSH file sandbox policy: workspace-write. File writes, edits, and file-mutating shell effects are limited to these canonical writable roots: "<workspace root>", "<temporary root>". Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.
```
##### 完全访问
```markdown
Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.
```
#### Token 影响
每个请求增加一个简洁的系统段落。`workspace-write` 还会列出规范化的会话工作区根目录,以及规范化的 `/tmp` 与平台临时根目录;如果它们指向同一目录则去重。
#### KV Cache 影响
不会直接使 KV Cache 失效;请求前缀变更由上述消费方负责,且提示词有意不包含模式
只要会话模式与不可变工作区根目录不变,请求前缀就在字节层面保持稳定。模式切换会在下一次请求中改变该段落;生成的 `request/header` 会记录新的前缀
## 已知限制与暂缓事项

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-sandbox-policy",
"description": "Per-call sandbox policy resolver (ctx.sandboxPolicy): deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family",
"description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -27,18 +27,22 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -3,6 +3,9 @@
* deployment's sandbox fallbacks plus per-session resolution: the file-effect
* {@link SandboxMode}, the `workspace-write` root, and the override kit (the
* `sandbox/mode` event, its fold, and its write path, from `./session-mode.ts`).
* Before each agent request, the owner also renders the resolved policy as the
* `sandbox:policy` system section; request headers therefore reconstruct the
* same mode and roots the enforcing consumers resolve.
*
* Both enforcing capability families read the SAME policy here: the sandboxed
* bash executor (`@deepseek-ai/dsh-bash-sandbox`) and the sandboxed filesystem
@@ -17,8 +20,10 @@
import { resolve as resolvePath } from 'node:path'
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { canonicalPath, type SandboxExecutionPolicy, type SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type {} from '@deepseek-ai/dsh-agent'
import { canonicalPath, writableRoots, type SandboxExecutionPolicy, type SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type { Session } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-system-prompt'
import { effectiveSandboxMode } from './session-mode.ts'
export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts'
@@ -28,6 +33,23 @@ function resolveWorkspaceRoot(path: string): string {
return resolvePath(canonicalPath(path))
}
/** Render the current file-effect policy without claiming host or backend capabilities. */
function renderPolicyContext(policy: SandboxExecutionPolicy): string {
switch (policy.mode) {
case 'read-only':
return 'Current DSH file sandbox policy: read-only. Ordinary file writes, edits, and file-mutating shell effects are denied; required sinks such as `/dev/null` may remain writable. Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.'
case 'workspace-write':
return `Current DSH file sandbox policy: workspace-write. File writes, edits, and file-mutating shell effects are limited to these canonical writable roots: ${writableRoots(policy).map(root => JSON.stringify(root)).join(', ')}. Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.`
case 'danger-full-access':
return 'Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.'
/* v8 ignore next 4 -- SandboxMode is a typed same-process closed union; this branch is only the static exhaustiveness guard. */
default: {
const mode: never = policy.mode
throw new Error(`unreachable sandbox mode: ${String(mode)}`)
}
}
}
declare module 'cordis' {
interface Context {
sandboxPolicy: SandboxPolicyService
@@ -61,9 +83,9 @@ export interface SandboxPolicyRequest {
/**
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
* default mode and fallback workspace root. Tool layers call {@link resolve}
* for each execution so a session's mode log and immutable cwd travel together
* to every enforcing capability.
* default mode, fallback workspace root, and current request-time policy
* section. Tool layers call {@link resolve} for each execution so a session's
* mode log and immutable cwd travel together to every enforcing capability.
*/
export class SandboxPolicyService extends Service {
// Inline schema call: the config catalog walks `static Config` statically.
@@ -86,6 +108,17 @@ export class SandboxPolicyService extends Service {
// the process cwd is real branching, resolved absolute either way.
this.defaultMode = config.mode as SandboxMode
this.workspaceRoot = resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd())
ctx.inject(['systemPrompt'], (scope: Context) => {
scope.systemPrompt.section({
name: 'sandbox:policy',
order: 110,
text: (context) => {
const session = context.agent?.session
return session === undefined ? '' : renderPolicyContext(this.resolve({ session }))
},
})
})
}
/**

View File

@@ -9,8 +9,11 @@ import { tmpdir } from 'node:os'
import { join, resolve, sep } from 'node:path'
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import SandboxPolicyService, { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
async function mounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}) {
const ctx = new Context()
@@ -28,6 +31,15 @@ function session(id: string, cwd?: string): Session {
})
}
function agentFor(activeSession: Session): Agent {
return { session: activeSession } as unknown as Agent
}
async function policySection(ctx: Context, activeSession: Session): Promise<string | undefined> {
return (await ctx.systemPrompt.assemble({ agent: agentFor(activeSession) }))
.sections.find(section => section.name === 'sandbox:policy')?.text
}
describe('SandboxPolicyService', () => {
it('defaults to read-only under the process cwd', async () => {
const ctx = await mounted()
@@ -116,10 +128,70 @@ describe('SandboxPolicyService', () => {
it('unregisters cleanly from a child fiber (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
const fiber = await ctx.plugin(SandboxPolicyService, {})
expect(ctx.sandboxPolicy).toBeDefined()
expect(await policySection(ctx, session('sess-hmr'))).toContain('read-only')
await fiber.dispose()
expect(ctx.get('sandboxPolicy')).toBeUndefined()
expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'sandbox:policy')).toBeUndefined()
})
})
describe('sandbox:policy request context', () => {
async function promptMounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(SandboxPolicyService, config)
return ctx
}
it('states the fresh read-only consequences before a tool attempt', async () => {
const ctx = await promptMounted()
const text = await policySection(ctx, session('sess-read-only', '/projects/read-only'))
expect(text).toBe('Current DSH file sandbox policy: read-only. Ordinary file writes, edits, and file-mutating shell effects are denied; required sinks such as `/dev/null` may remain writable. Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.')
})
it('states canonical workspace and temporary roots under workspace-write', async () => {
const ctx = await promptMounted({ mode: 'workspace-write', workspaceRoot: '/fallback' })
const active = session('sess-workspace-write', '/projects/../projects/current')
const policy = ctx.sandboxPolicy.resolve({ session: active })
const roots = writableRoots(policy)
const text = await policySection(ctx, active)
expect(text).toBe(`Current DSH file sandbox policy: workspace-write. File writes, edits, and file-mutating shell effects are limited to these canonical writable roots: ${roots.map(root => JSON.stringify(root)).join(', ')}. Host OS permissions and sandbox-backend availability may restrict operations further. This policy does not govern network or process access.`)
expect(roots[0]).toBe(resolve('/projects/current'))
expect(roots).toContain(canonicalPath('/tmp'))
})
it('states that danger-full-access adds no DSH file restriction without claiming wider authority', async () => {
const ctx = await promptMounted({ mode: 'danger-full-access' })
const text = await policySection(ctx, session('sess-danger', '/projects/current'))
expect(text).toBe('Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does not restrict file operations. Host OS permissions and other policies still apply. This policy does not govern network or process access.')
})
it('reflects the latest durable switch on the next assembly and stays byte-stable otherwise', async () => {
const ctx = await promptMounted()
const active = session('sess-switch', '/projects/current')
const first = await policySection(ctx, active)
expect(await policySection(ctx, active)).toBe(first)
setSandboxMode(active, 'danger-full-access')
const danger = await policySection(ctx, active)
expect(danger).toContain('does not restrict file operations')
expect(await policySection(ctx, active)).toBe(danger)
setSandboxMode(active, 'workspace-write')
expect(await policySection(ctx, active)).toContain(JSON.stringify(resolve('/projects/current')))
})
it('reconstructs resumed policy from the session log and omits diagnostics without an agent', async () => {
const active = session('sess-resume', '/projects/current')
setSandboxMode(active, 'workspace-write')
const resumed = new Session(active.id, active.events, active.header)
const ctx = await promptMounted({ mode: 'read-only' })
expect(await policySection(ctx, resumed)).toContain('workspace-write')
expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'sandbox:policy')?.text).toBe('')
})
})

View File

@@ -17,12 +17,18 @@
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../core/agent"
},
{
"path": "../sandbox"
},
{
"path": "../../core/session"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../support/invariants"
}

6
pnpm-lock.yaml generated
View File

@@ -3557,6 +3557,9 @@ importers:
specifier: ^3.18.0
version: 3.18.0
devDependencies:
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../support/invariants
@@ -3566,6 +3569,9 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:^
version: link:../../core/system-prompt
cordis:
specifier: ^4.0.0-rc.7
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)

View File

@@ -90,7 +90,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/subprocess/subprocess': { kind: 'indirect', reason: 'The seam delegates all model rendering to consumer seams such as the bash executor family.' },
'packages/subprocess/subprocess-local': { kind: 'indirect', reason: 'The spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
'packages/sandbox/sandbox-policy': { kind: 'indirect', reason: 'The policy service holds the mode dsh-tool-bash and dsh-tool-fs render in their denial markers.' },
'packages/sdk/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
'packages/sdk/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' },
'packages/sdk/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' },

View File

@@ -26,6 +26,7 @@
"apps/web/tests/message-actions.e2e.ts",
"apps/web/tests/queue-actions.e2e.ts",
"apps/web/tests/skill-invocation-policy.e2e.ts",
"apps/web/tests/permission-policy-context.e2e.ts",
"apps/cli/tests/**/*.ts",
"examples/*/src/**/*.ts",
"examples/*/start.ts",