feat(sandbox-policy): describe enforced file families
This commit is contained in:
@@ -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/bash/bash-sandbox/README.md
|
||||
README.md: ca77a9c626784b29145712535d69de4afbd3a697
|
||||
README.zh.md: 4ecc8d533f7af373bdacd133d44a8def6d265868
|
||||
README.md: 8012dbcbd656130b3d7b6701723c880dcf3d9d71
|
||||
README.zh.md: de1077174ad802c0a18e5910792e61acd33c16ab
|
||||
|
||||
@@ -18,7 +18,7 @@ Semantics:
|
||||
|
||||
- **Denials are result facts.** A failed run whose stderr carries the selected backend's own denial dialect — the signatures the provider stamps on every wrap (EROFS text under bwrap, EACCES under Landlock, EPERM under Seatbelt) — is reported as `BashRunResult.sandbox.denied: true` (conservative classification, read from the collected stderr tail); every CONFINED run also carries the mode it executed under (`result.sandbox.mode`) and the provider's enforcement completeness (`result.sandbox.enforcement`: `full`, or `partial` on an older Landlock ABI).
|
||||
- **Runner failures are sandbox failures, never command failures.** Foreground execution throws `SANDBOX_UNAVAILABLE`; a settled background process stamps `process.sandbox.runnerFailed`, which the bash producer renders through generic `task_output`. Spawn failures also pass through settlement, so confined background handles retain their mode/enforcement facts and release per-process accounting.
|
||||
- **Deployment fallback, per-call policy.** [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) resolves a complete `SandboxExecutionPolicy` for every tool call: the calling session supplies its mode override and immutable cwd root, while deployment config supplies the fallbacks for agentless calls. An approved escalation changes only that policy's mode; its session root stays attached. `resolve()` carries the policy onto the spec, so overlapping commands from different projects run, classify, and report under their own roots and modes. The capability fact `ctx.bash.sandboxMode` reports the configured default so the tool layer advertises escalation only when this executor is mounted. The model learns of the sandbox only through result facts — the static bash tool description explains the denial marker; there is no current-mode statement in the system prompt.
|
||||
- **Deployment fallback, per-call policy.** [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) resolves a complete `SandboxExecutionPolicy` for every tool call: the calling session supplies its mode override and immutable cwd root, while deployment config supplies the fallbacks for agentless calls. An approved escalation changes only that policy's mode; its session root stays attached. `resolve()` carries the policy onto the spec, so overlapping commands from different projects run, classify, and report under their own roots and modes. The capability fact `ctx.bash.sandboxMode` reports the configured default so the tool layer advertises escalation only when this executor is mounted. This backend also contributes the one-shot bash family to the owner-rendered current-policy section; the static bash tool description separately owns denial and escalation guidance.
|
||||
- **File effects only.** Network and process visibility are deliberately not restricted — the mode vocabulary does not pretend to cover what the backend does not enforce.
|
||||
- Process mechanics (spawn, process-group kills, output collection/spill, background handles, credential scrub) are inherited from [`dsh-bash-local`](../bash-local/); runner selection lives in [`dsh-sandbox-local`](../../sandbox/sandbox-local/).
|
||||
|
||||
@@ -44,15 +44,15 @@ The keyless consumer-integration proofs are `tests/bwrap.e2e.ts`, `tests/landloc
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The generated [`dsh-tool-bash` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash) are the baseline. By advertising a confining `sandboxMode`, this backend augments `bash` with `sandbox_permissions` using enum `workspace-write` | `danger-full-access` and with `justification`. The backend adds no prompt prose, and the session's effective mode remains unstated.
|
||||
The generated [`dsh-tool-bash` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash) are the baseline. By advertising a confining `sandboxMode`, this backend augments `bash` with `sandbox_permissions` using enum `workspace-write` | `danger-full-access` and with `justification`. Its family contribution also makes one-shot bash commands appear in the owner-rendered `sandbox:policy` section.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Small fixed schema increment on requests where `bash` is visible; mode switches add no context tokens.
|
||||
Small fixed schema increment on requests where `bash` is visible, plus the current-policy clause owned by `dsh-sandbox-policy`.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Prefix-stable while the executor advertises the same sandbox capabilities. Changing those capabilities alters the `bash` schema and may invalidate reuse from that definition; per-session mode switches do not.
|
||||
Prefix-stable while the executor and standing policy are unchanged. Changing the policy updates the owner-rendered section; changing executor capabilities also alters the `bash` schema.
|
||||
|
||||
### Bash tool result, indirectly
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
- **拒绝是结果事实。** 如果一次失败运行的 stderr 包含所选后端自身的拒绝方言,即提供方在每次包装时加上的特征(bwrap 下的 EROFS 文本、Landlock 下的 EACCES、Seatbelt 下的 EPERM),则结果报告 `BashRunResult.sandbox.denied: true`(从已收集的 stderr 尾部进行保守分类)。每次受限制运行还会携带执行时模式(`result.sandbox.mode`)与提供方强制执行完整性(`result.sandbox.enforcement`:`full`,或在较旧 Landlock ABI 上为 `partial`)。
|
||||
- **Runner 失败是沙箱失败,绝不是命令失败。** 前台执行会抛出 `SANDBOX_UNAVAILABLE`;已结算的后台进程会标记 `process.sandbox.runnerFailed`,Bash 结果生成方通过通用 `task_output` 渲染它。spawn 失败也会经过结算,因此受限制的后台句柄会保留自身的模式/强制执行事实,并释放每进程计数。
|
||||
- **部署回退,每次调用策略。** [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) 为每次工具调用解析完整的 `SandboxExecutionPolicy`:调用会话提供自身的模式覆盖与不可变 cwd 根目录,部署配置则为无 agent(智能体)调用提供回退。已批准的升权只更改该策略的模式,会话根目录仍然附着其上。`resolve()` 把策略带入 spec,因此来自不同项目的重叠命令会在各自的根目录与模式下运行、分类和报告。能力事实 `ctx.bash.sandboxMode` 报告已配置的默认值,因此工具层只在装载该执行器时才公布升权。模型只能通过结果事实了解沙箱:静态 bash 工具描述会解释拒绝标记,系统提示词中不会声明当前模式。
|
||||
- **部署回退,每次调用策略。** [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) 为每次工具调用解析完整的 `SandboxExecutionPolicy`:调用会话提供自身的模式覆盖与不可变 cwd 根目录,部署配置则为无 agent(智能体)调用提供回退。已批准的升权只更改该策略的模式,会话根目录仍然附着其上。`resolve()` 把策略带入 spec,因此来自不同项目的重叠命令会在各自的根目录与模式下运行、分类和报告。能力事实 `ctx.bash.sandboxMode` 报告已配置的默认值,因此工具层只在装载该执行器时才公布升权。该后端还会向归属方渲染的当前策略段落贡献一次性 bash 家族;静态 bash 工具描述则单独负责拒绝与升级引导。
|
||||
- **只限制文件影响。** 设计上不限制网络与进程可见性:模式词汇不会声称覆盖后端未强制执行的范围。
|
||||
- 进程机制(spawn、进程组终止、输出收集/spill、后台句柄、凭证清理)继承自 [`dsh-bash-local`](../bash-local/);runner 选择位于 [`dsh-sandbox-local`](../../sandbox/sandbox-local/)。
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
基线是生成的 [`dsh-tool-bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash)。通过公布表明启用隔离的 `sandboxMode` 能力,此后端会为 `bash` 增加 `sandbox_permissions`,其 enum 为 `workspace-write` | `danger-full-access`,并增加 `justification`。后端不添加提示词文本,会话的有效模式仍不会声明。
|
||||
基线是生成的 [`dsh-tool-bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash)。通过公布表明启用隔离的 `sandboxMode` 能力,此后端会为 `bash` 增加 `sandbox_permissions`,其 enum 为 `workspace-write` | `danger-full-access`,并增加 `justification`。其家族贡献还会让一次性 bash 命令出现在归属方渲染的 `sandbox:policy` 段落中。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
在 `bash` 可见的请求上,schema 固定增加少量内容;模式切换不增加上下文 token。
|
||||
在 `bash` 可见的请求上,schema 固定增加少量内容,另有一条由 `dsh-sandbox-policy` 负责的当前策略子句。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
执行器持续公布相同沙箱能力时,前缀保持稳定。更改这些能力会改变 `bash` schema,可能使从该定义起的复用失效;每会话模式切换不会导致失效。
|
||||
执行器与常驻策略不变时,前缀保持稳定。更改策略会更新归属方渲染的段落;更改执行器能力也会改变 `bash` schema。
|
||||
|
||||
### 间接的 Bash 工具结果
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@ export type Config = LocalConfig
|
||||
* Registers as `ctx.bash` in place of the local executor and requires a
|
||||
* `ctx.sandbox` provider plus `ctx.sandboxPolicy`; the tool layer is
|
||||
* unchanged. Tool calls pass the calling session's resolved policy; direct
|
||||
* calls fall back to deployment policy. The prompt does not state the standing
|
||||
* mode; `result.sandbox` reports the mode and enforcement actually used.
|
||||
* calls fall back to deployment policy. Its family contribution lets the
|
||||
* policy owner state which one-shot bash effects the standing mode governs;
|
||||
* `result.sandbox` reports the mode and enforcement actually used.
|
||||
*/
|
||||
export class SandboxBashExecutor extends LocalBashExecutor {
|
||||
static override inject = ['subprocess', 'sandbox', 'sandboxPolicy']
|
||||
@@ -59,6 +60,7 @@ export class SandboxBashExecutor extends LocalBashExecutor {
|
||||
// The default mode is the capability fact used for schema advertisement;
|
||||
// actual tool executions carry their resolved per-call policy.
|
||||
this.mode = ctx.sandboxPolicy.defaultMode
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('bash')
|
||||
}
|
||||
|
||||
/** The configured default mode — the capability fact the tool layer reads. */
|
||||
|
||||
@@ -512,6 +512,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
key: 'sandboxPolicy',
|
||||
summary: 'The sandbox-policy service (`ctx.sandboxPolicy`).',
|
||||
methods: [
|
||||
{
|
||||
signature: 'registerEnforcedFamily(family: \'filesystem\' | \'bash\' | \'terminal\'): () => void',
|
||||
jsDoc: '/**\n * Register one runtime contribution that enforces the shared file policy for\n * a model-facing operation family. Equal families remain independently\n * disposable; registration and removal invalidate assembled prompt caches\n * when a system-prompt service is active.\n * @param family - operation family whose file effects this contribution enforces.\n * @returns the exact Cordis effect disposer for this contribution.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy',
|
||||
jsDoc: '/**\n * Resolve the complete policy for one capability call. An approved explicit\n * mode outranks the session\'s last `sandbox/mode` event, which outranks the\n * deployment default. A session cwd is its workspace-write boundary; the\n * configured root is the fallback for agentless calls and sessions without a\n * cwd.\n * @param request - optional session and approved mode override.\n * @returns the fully resolved per-call mode and absolute workspace root.\n */',
|
||||
|
||||
@@ -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/fs/fs-sandbox/README.md
|
||||
README.md: 790444a4184b9bcccd3a0798cf0c09cb6f1b166e
|
||||
README.zh.md: d54bdcbe65673b6892ebd1d539dd066f66d68cb6
|
||||
README.md: a376f2c23dca9f0895525fa274a1ae0545823f63
|
||||
README.zh.md: 4fa2deecb7d10124da8b7920997196b816457fcc
|
||||
|
||||
@@ -22,11 +22,19 @@ A denial is a structured `FsError` (`FS_SANDBOX_DENIED`, carrying the effective
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `dsh-tool-fs`, which renders this backend's `FS_SANDBOX_DENIED` refusals as the `[sandbox: file access denied under <mode> mode]` marker plus the same-turn escalation hint.
|
||||
### Filesystem policy and refusals
|
||||
|
||||
#### What the model sees
|
||||
|
||||
This backend contributes the write/edit family to the owner-rendered `sandbox:policy` section. Indirectly, `dsh-tool-fs` renders its `FS_SANDBOX_DENIED` refusals as the `[sandbox: file access denied under <mode> mode]` marker plus the same-turn escalation hint.
|
||||
|
||||
#### Token effect
|
||||
|
||||
The current-policy clause adds a small fixed prefix while this backend is mounted; a denial adds the bounded marker and escalation hint to conversation history.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct invalidation; the named consumer owns any request-prefix changes.
|
||||
A standing-policy or family-composition change updates the owner-rendered request prefix; operation results remain append-only.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -22,11 +22,19 @@
|
||||
|
||||
## 模型体验
|
||||
|
||||
通过 `dsh-tool-fs` 间接产生影响;该消费方把本后端的 `FS_SANDBOX_DENIED` 拒绝渲染为 `[sandbox: file access denied under <mode> mode]` 标记和同轮次升级提示。
|
||||
### 文件系统策略与拒绝
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
该后端会向归属方渲染的 `sandbox:policy` 段落贡献 write/edit 家族。作为间接影响,`dsh-tool-fs` 会把本后端的 `FS_SANDBOX_DENIED` 拒绝渲染为 `[sandbox: file access denied under <mode> mode]` 标记和同轮次升级提示。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
该后端挂载期间,当前策略条款会在前缀中增加少量固定内容;拒绝则会把有界标记和升级提示追加到对话历史。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接使缓存失效;上述消费方负责请求前缀的任何变化。
|
||||
常驻策略或家族组合发生变化时,归属方渲染的请求前缀会更新;操作结果保持仅追加。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ export class SandboxedFileSystem extends LocalFileSystem {
|
||||
constructor(ctx: Context, config: Config) {
|
||||
super(ctx, config)
|
||||
this.defaultMode = ctx.sandboxPolicy.defaultMode
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
}
|
||||
|
||||
/** The deployment default mode — the capability fact the tool layer reads to advertise escalation. */
|
||||
|
||||
@@ -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/pty/pty-local/README.md
|
||||
README.md: de17c1e56108726daf4009492012caaa79155eca
|
||||
README.zh.md: 95d8350716da685381333187d534d696873db605
|
||||
README.md: 5ad2c94f7c3e8e8bda5b7b432b3d0529a30f2b4e
|
||||
README.zh.md: 0ab3f4a485315e6037d862f423f9e25b6200a63e
|
||||
|
||||
@@ -14,19 +14,19 @@ Send cancellation resolves the current foreground process group and delivers a r
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Indirect consumer
|
||||
### Current file policy and indirect consumer
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Nothing directly. Through `@deepseek-ai/dsh-tool-pty`, the model may receive bounded MOTD, send deltas, scrollback pages, readiness reasons, and cleanup errors.
|
||||
This backend contributes the terminal family to the owner-rendered `sandbox:policy` section. Through `@deepseek-ai/dsh-tool-pty` or another PTY consumer, the model may also receive bounded MOTD, send deltas, scrollback pages, readiness reasons, and cleanup errors.
|
||||
|
||||
#### Token effect
|
||||
|
||||
None until a consumer returns bounded backend output. Retained PTY scrollback is not placed in model history by this package.
|
||||
The current-policy clause is present while this backend is mounted. Retained PTY scrollback is not placed in model history until a consumer returns bounded output.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct invalidation; the consumer owns prompts, schemas, and appended results.
|
||||
A standing-policy or terminal-family change updates the owner-rendered request prefix; consumer results remain append-only.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -14,19 +14,19 @@ Linux 的就绪检测结合以下机制:由前台状态验证的私有 bash
|
||||
|
||||
## 模型体验
|
||||
|
||||
### 间接消费方
|
||||
### 当前文件策略与间接消费方
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
没有直接可见内容。模型通过 `@deepseek-ai/dsh-tool-pty` 可能收到有界的 MOTD、发送增量、scrollback 页、就绪原因和清理错误。
|
||||
该后端会向归属方渲染的 `sandbox:policy` 段落贡献终端家族。模型通过 `@deepseek-ai/dsh-tool-pty` 或其他 PTY 消费方还可能收到有界的 MOTD、发送增量、scrollback 页、就绪原因和清理错误。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
消费方返回有界的后端输出前没有影响。此包(package)不会把保留的 PTY scrollback 放入模型历史。
|
||||
装载该后端期间,当前策略子句会一直存在。消费方返回有界输出前,保留的 PTY scrollback 不会进入模型历史。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接使 KV Cache 失效;提示词、schema 与追加结果由消费方负责。
|
||||
常驻策略或终端家族发生变化时,归属方渲染的请求前缀会更新;消费方结果保持仅追加。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
@@ -129,5 +129,6 @@ export class LocalPtyBackend implements PtyBackend {
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
validateConfig(config)
|
||||
const inspector = createProcessInspector()
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
|
||||
ctx.pty.registerBackend(new LocalPtyBackend(ctx, config, inspector))
|
||||
}
|
||||
|
||||
@@ -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: 3258492ba80102ec96d37baa5e4989b1c396cf15
|
||||
README.zh.md: 4bb7e3b1321620413b81414d60f7b2d02df588c6
|
||||
README.md: 45349f7b0bbb6e035dd2aa6f4695735124dd9f2d
|
||||
README.zh.md: 9393d2a22aa3df310287ccb9c5e486880453a838
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
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, and the model receives that same effective policy before each request.
|
||||
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 family receives one resolved mode-and-root policy per call and registers whether the current runtime fences filesystem tools, one-shot bash commands, or terminal sessions; the model receives only those current facts before each request.
|
||||
|
||||
## Why a shared home
|
||||
|
||||
Two families enforce the same mode vocabulary: the sandboxed bash executor (`@deepseek-ai/dsh-bash-sandbox`) and the sandboxed filesystem provider (`@deepseek-ai/dsh-fs-sandbox`). If each resolved its own `mode` + `workspaceRoot`, the two could drift into a split world — bash confined to one root while fs fences another, exactly what [the sandbox RFC](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) warns against. Both tool layers resolve policy through `ctx.sandboxPolicy`, and both enforcing backends consume that complete per-call result. The [cross-family fs sandbox RFC](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) records the shared-policy decision.
|
||||
Filesystem tools, one-shot bash commands, and terminal sessions may enforce the same mode vocabulary in different combinations. If each resolved its own `mode` + `workspaceRoot`, they could drift into a split world, exactly what [the sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) warns against. Each enforcing backend consumes the complete owner-resolved policy and contributes its model-facing family; the current section therefore does not claim that an unfenced family shares another family's restrictions. The [cross-family fs sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) records the shared-policy decision.
|
||||
|
||||
## Config
|
||||
|
||||
@@ -17,7 +17,8 @@ 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.
|
||||
- `ctx.sandboxPolicy.registerEnforcedFamily(family)` — independently registers `filesystem`, `bash`, or `terminal` and returns the exact effect disposer. Equal families remain separate contributions; the section uses canonical family order and removes a family only after its final contribution leaves.
|
||||
- `sandbox:policy` — a request-time system-prompt section derived from `resolve({ session })` and the active family contributions. It is empty without an enforcing family and states only the mode, the affected model-facing operations, and the canonical session workspace under `workspace-write`.
|
||||
- `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.
|
||||
@@ -34,29 +35,29 @@ A runtime switch is one log-only `sandbox/mode` event on the session it applies
|
||||
|
||||
#### 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.
|
||||
One `sandbox:policy` system section on each agent request when at least one enforcing family is registered. The examples below show all three families; absent families are omitted. Tool plugins retain operation and escalation guidance, 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.
|
||||
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files under this policy.
|
||||
```
|
||||
|
||||
##### 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.
|
||||
Current DSH file policy: workspace-write. The write and edit tools, one-shot bash commands, and terminal sessions may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
|
||||
```
|
||||
|
||||
##### 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.
|
||||
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict the write and edit tools, one-shot bash commands, or terminal sessions.
|
||||
```
|
||||
|
||||
#### 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.
|
||||
One concise system section per request. `workspace-write` carries only the canonical session workspace path; platform-specific temporary paths are summarized without adding host-dependent bytes.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
@@ -66,3 +67,4 @@ The request prefix is byte-stable while the session mode and immutable workspace
|
||||
|
||||
- **One primary workspace root per session** — policy resolves `SessionHeader.cwd`; extra writable roots are not part of `SandboxExecutionPolicy`.
|
||||
- **File-effect modes only** — `SandboxMode` governs file effects; network and process policy are outside its vocabulary, so no knob here restricts them.
|
||||
- **Temporary areas are deliberately summarized** — enforcing backends grant different platform temporary areas, which are selected after policy resolution and therefore cannot be enumerated truthfully in the standing section.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每个强制执行策略的能力家族在每次调用时都会收到一项解析完成的模式与根目录策略,模型也会在每次请求前收到同一项有效策略。
|
||||
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每个强制执行家族在每次调用时都会收到一项解析完成的模式与根目录策略,并登记当前运行时对文件系统工具、一次性 bash 命令和终端会话中的哪些家族施加围栏;模型在每次请求前只会收到这些当前事实。
|
||||
|
||||
## 为何需要共享归属位置
|
||||
|
||||
两个家族强制执行同一套模式词汇:沙箱化 bash 执行器(`@deepseek-ai/dsh-bash-sandbox`)与沙箱化文件系统提供方(`@deepseek-ai/dsh-fs-sandbox`)。如果两者各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界:bash 限制在一个根目录,fs 却隔离另一个根目录,正是[沙箱 RFC](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。两个工具层都通过 `ctx.sandboxPolicy` 解析策略,两个强制执行后端也都消费完整的逐调用结果。[跨家族 fs 沙箱 RFC](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了共享策略决策。
|
||||
文件系统工具、一次性 bash 命令和终端会话可以用不同组合强制执行同一套模式词汇。如果各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界,正是[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。每个强制执行后端都会消费归属方解析出的完整策略,并贡献其面向模型的家族;因此,当前段落不会声称不受围栏约束的家族也受另一家族的限制。[跨家族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了共享策略决策。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
|
||||
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话最后一条 `sandbox/mode` 事件,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
|
||||
- `ctx.sandboxPolicy.defaultMode`/`ctx.sandboxPolicy.workspaceRoot`:`resolve()` 使用的部署默认值与回退根目录。
|
||||
- `sandbox:policy`:由 `resolve({ session })` 派生的请求时系统提示词段落。它说明当前文件操作模式及其后果,并列出 `workspace-write` 下所有规范化的可写根目录;不会声称主机权限、沙箱后端就绪状态或网络/进程限制。
|
||||
- `ctx.sandboxPolicy.registerEnforcedFamily(family)`:独立注册 `filesystem`、`bash` 或 `terminal`,并返回对应的精确 effect disposer。相同家族仍是彼此独立的贡献;该段落使用规范的家族顺序,并且只有最后一项贡献离开后才移除对应家族。
|
||||
- `sandbox:policy`:由 `resolve({ session })` 和当前家族贡献派生的请求时系统提示词段落。没有强制执行家族时为空,只说明模式、受影响的面向模型操作,以及 `workspace-write` 下规范化的会话工作区。
|
||||
- `effectiveSandboxMode(events)`:会话 `sandbox/mode` 事件的纯 fold(最后一次切换胜出,没有则为 `undefined`),在 `resolve()` 内使用。
|
||||
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
|
||||
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
|
||||
@@ -34,29 +35,29 @@
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
每次 agent 请求都有一个 `sandbox:policy` 系统段落。该段落只说明 DSH 文件操作策略;工具 schema 仍由各自归属方管理,批准策略仍由 `dsh-user-approval` 的段落管理,计划引导仍由 `dsh-plan-mode` 的段落管理。
|
||||
只要至少注册了一个强制执行家族,每次 agent 请求就会有一个 `sandbox:policy` 系统段落。以下示例展示全部三个家族;缺失的家族会被省略。工具插件继续负责操作与升级引导,批准策略仍由 `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.
|
||||
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files under this policy.
|
||||
```
|
||||
|
||||
##### 工作区写入
|
||||
|
||||
```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.
|
||||
Current DSH file policy: workspace-write. The write and edit tools, one-shot bash commands, and terminal sessions may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
|
||||
```
|
||||
|
||||
##### 完全访问
|
||||
|
||||
```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.
|
||||
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict the write and edit tools, one-shot bash commands, or terminal sessions.
|
||||
```
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每个请求增加一个简洁的系统段落。`workspace-write` 还会列出规范化的会话工作区根目录,以及规范化的 `/tmp` 与平台临时根目录;如果它们指向同一目录则去重。
|
||||
每个请求增加一个简洁的系统段落。`workspace-write` 只携带规范化的会话工作区路径;平台特定的临时路径会以摘要表述,不会加入依赖主机的字节。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
@@ -66,3 +67,4 @@ Current DSH file sandbox policy: danger-full-access. The DSH file sandbox does n
|
||||
|
||||
- **每个会话只有一个主要工作区根目录**:策略解析 `SessionHeader.cwd`;额外可写根目录不属于 `SandboxExecutionPolicy`。
|
||||
- **仅限文件操作模式**:`SandboxMode` 管控文件操作;网络和进程策略不在其词汇中,因此这里没有限制它们的旋钮。
|
||||
- **有意概述临时区域**:强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在常驻段落中如实枚举。
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
* `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
|
||||
* provider (`@deepseek-ai/dsh-fs-sandbox`) consume the SAME resolved per-call
|
||||
* policy, so bash and fs can never confine to different roots — the split
|
||||
* world the sandbox RFC warns about. The service reads session state once at
|
||||
* the tool boundary; executors and providers remain session-free.
|
||||
* Enforcing filesystem, one-shot bash, and terminal backends read the SAME
|
||||
* resolved policy here and register their independently disposable model-facing
|
||||
* families. The request section therefore describes only operations this
|
||||
* runtime actually fences, while each backend retains its own enforcement
|
||||
* dialect. The service reads session state once at each operation boundary;
|
||||
* executors and providers remain session-free.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-sandbox-policy
|
||||
*/
|
||||
@@ -21,7 +21,7 @@ import { resolve as resolvePath } from 'node:path'
|
||||
import { Context, Service } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import { canonicalPath, writableRoots, type SandboxExecutionPolicy, type SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import { canonicalPath, 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'
|
||||
@@ -33,15 +33,40 @@ 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 {
|
||||
/** Model-facing operation family whose current file policy is enforced by a runtime contribution. */
|
||||
type FilePolicyFamily = 'filesystem' | 'bash' | 'terminal'
|
||||
|
||||
/** Canonical model-facing order, independent of plugin load order. */
|
||||
const FILE_POLICY_FAMILIES: readonly FilePolicyFamily[] = ['filesystem', 'bash', 'terminal']
|
||||
|
||||
const FAMILY_LABELS: Readonly<Record<FilePolicyFamily, string>> = {
|
||||
filesystem: 'the write and edit tools',
|
||||
bash: 'one-shot bash commands',
|
||||
terminal: 'terminal sessions',
|
||||
}
|
||||
|
||||
/** Join model-facing family names with stable English punctuation. */
|
||||
function familyList(families: readonly FilePolicyFamily[], conjunction: 'and' | 'or'): string {
|
||||
const labels = families.map(family => FAMILY_LABELS[family])
|
||||
if (labels.length === 1) return labels[0] as string
|
||||
if (labels.length === 2) return `${labels[0]} ${conjunction} ${labels[1]}`
|
||||
return `${labels.slice(0, -1).join(', ')}, ${conjunction} ${labels.at(-1)}`
|
||||
}
|
||||
|
||||
/** Render only policy facts shared by every backend enforcing each registered family. */
|
||||
function renderPolicyContext(policy: SandboxExecutionPolicy, families: readonly FilePolicyFamily[]): string {
|
||||
if (families.length === 0) return ''
|
||||
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 'read-only': {
|
||||
const subjects = familyList(families, 'and')
|
||||
return `Current DSH file policy: read-only. ${subjects[0]?.toUpperCase()}${subjects.slice(1)} cannot modify files under this policy.`
|
||||
}
|
||||
case 'workspace-write': {
|
||||
const subjects = familyList(families, 'and')
|
||||
return `Current DSH file policy: workspace-write. ${subjects[0]?.toUpperCase()}${subjects.slice(1)} may modify files under the session workspace: ${JSON.stringify(policy.workspaceRoot)}. Some platform temporary areas may also be writable.`
|
||||
}
|
||||
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.'
|
||||
return `Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict ${familyList(families, 'or')}.`
|
||||
/* 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
|
||||
@@ -83,9 +108,10 @@ export interface SandboxPolicyRequest {
|
||||
|
||||
/**
|
||||
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
|
||||
* 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.
|
||||
* default mode, fallback workspace root, enforcing-family contributions, 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.
|
||||
@@ -100,6 +126,8 @@ export class SandboxPolicyService extends Service {
|
||||
readonly defaultMode: SandboxMode
|
||||
/** The absolute `workspace-write` fallback root for calls without a session cwd. */
|
||||
readonly workspaceRoot: string
|
||||
/** Independently disposable enforcement-family contributions. */
|
||||
private readonly enforcedFamilies = new Map<FilePolicyFamily, Set<symbol>>()
|
||||
|
||||
constructor(ctx: Context, config: Config) {
|
||||
super(ctx, 'sandboxPolicy')
|
||||
@@ -115,12 +143,38 @@ export class SandboxPolicyService extends Service {
|
||||
order: 110,
|
||||
text: (context) => {
|
||||
const session = context.agent?.session
|
||||
return session === undefined ? '' : renderPolicyContext(this.resolve({ session }))
|
||||
return session === undefined ? '' : renderPolicyContext(this.resolve({ session }), this.activeFamilies())
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register one runtime contribution that enforces the shared file policy for
|
||||
* a model-facing operation family. Equal families remain independently
|
||||
* disposable; registration and removal invalidate assembled prompt caches
|
||||
* when a system-prompt service is active.
|
||||
* @param family - operation family whose file effects this contribution enforces.
|
||||
* @returns the exact Cordis effect disposer for this contribution.
|
||||
*/
|
||||
registerEnforcedFamily(family: 'filesystem' | 'bash' | 'terminal'): () => void {
|
||||
const token = Symbol(family)
|
||||
const dispose = this.ctx.effect(() => {
|
||||
const contributions = this.enforcedFamilies.get(family) ?? new Set<symbol>()
|
||||
contributions.add(token)
|
||||
this.enforcedFamilies.set(family, contributions)
|
||||
this.emitPromptChange()
|
||||
return () => {
|
||||
contributions.delete(token)
|
||||
if (contributions.size === 0 && this.enforcedFamilies.get(family) === contributions) {
|
||||
this.enforcedFamilies.delete(family)
|
||||
}
|
||||
this.emitPromptChange()
|
||||
}
|
||||
}, 'sandboxPolicy.registerEnforcedFamily()')
|
||||
return () => void dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the complete policy for one capability call. An approved explicit
|
||||
* mode outranks the session's last `sandbox/mode` event, which outranks the
|
||||
@@ -146,6 +200,16 @@ export class SandboxPolicyService extends Service {
|
||||
overrideOf(session: Session): SandboxMode | undefined {
|
||||
return effectiveSandboxMode(session.events)
|
||||
}
|
||||
|
||||
/** Active families in canonical model-facing order. */
|
||||
private activeFamilies(): FilePolicyFamily[] {
|
||||
return FILE_POLICY_FAMILIES.filter(family => (this.enforcedFamilies.get(family)?.size ?? 0) > 0)
|
||||
}
|
||||
|
||||
/** Notify prompt consumers only after their registry exists. */
|
||||
private emitPromptChange(): void {
|
||||
if (this.ctx.get('systemPrompt') !== undefined) this.ctx.emit('system-prompt/change')
|
||||
}
|
||||
}
|
||||
|
||||
export default SandboxPolicyService
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
* `effective = fold(events) ?? the deployment default`, so an override
|
||||
* survives restart by replay, two sessions can never see each other's state,
|
||||
* and there is no external config store. The event is log-only (the
|
||||
* `approval/*` precedent): the model learns the mode from the boundary
|
||||
* markers in the enforcing tools, never from the event itself. EXECUTION
|
||||
* honors the fold through `ctx.sandboxPolicy.resolve()` — it stamps the mode
|
||||
* together with the calling session's workspace root onto each capability
|
||||
* call, weakest-precedence beneath an escalation grant.
|
||||
* `approval/*` precedent): the policy owner projects the fold into each model
|
||||
* request, while enforcing tools report operation-specific boundary markers.
|
||||
* EXECUTION honors the same fold through `ctx.sandboxPolicy.resolve()` — it
|
||||
* stamps the mode together with the calling session's workspace root onto each
|
||||
* capability call, weakest-precedence beneath an escalation grant.
|
||||
*
|
||||
* The override is policy state shared by every enforcing family (bash and
|
||||
* filesystem alike), so it lives here in the policy package rather than in any
|
||||
|
||||
@@ -10,10 +10,9 @@ 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'
|
||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
|
||||
async function mounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}) {
|
||||
const ctx = new Context()
|
||||
@@ -130,6 +129,7 @@ describe('SandboxPolicyService', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
const fiber = await ctx.plugin(SandboxPolicyService, {})
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
expect(ctx.sandboxPolicy).toBeDefined()
|
||||
expect(await policySection(ctx, session('sess-hmr'))).toContain('read-only')
|
||||
await fiber.dispose()
|
||||
@@ -146,38 +146,95 @@ describe('sandbox:policy request context', () => {
|
||||
return ctx
|
||||
}
|
||||
|
||||
it('states the fresh read-only consequences before a tool attempt', async () => {
|
||||
it('omits policy prose when no enforcing family is registered', 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.')
|
||||
expect(await policySection(ctx, session('sess-no-family'))).toBe('')
|
||||
})
|
||||
|
||||
it('states canonical workspace and temporary roots under workspace-write', async () => {
|
||||
it.each([
|
||||
[['filesystem'], 'Current DSH file policy: read-only. The write and edit tools cannot modify files under this policy.'],
|
||||
[['bash'], 'Current DSH file policy: read-only. One-shot bash commands cannot modify files under this policy.'],
|
||||
[['terminal'], 'Current DSH file policy: read-only. Terminal sessions cannot modify files under this policy.'],
|
||||
[['filesystem', 'bash'], 'Current DSH file policy: read-only. The write and edit tools and one-shot bash commands cannot modify files under this policy.'],
|
||||
[['filesystem', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools and terminal sessions cannot modify files under this policy.'],
|
||||
[['bash', 'terminal'], 'Current DSH file policy: read-only. One-shot bash commands and terminal sessions cannot modify files under this policy.'],
|
||||
[['filesystem', 'bash', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files under this policy.'],
|
||||
] as const)('states read-only consequences for %j', async (families, expected) => {
|
||||
const ctx = await promptMounted()
|
||||
for (const family of [...families].reverse()) ctx.sandboxPolicy.registerEnforcedFamily(family)
|
||||
expect(await policySection(ctx, session(`sess-read-only-${families.join('-')}`))).toBe(expected)
|
||||
})
|
||||
|
||||
it('states the portable workspace guarantee without enumerating host temp paths', async () => {
|
||||
const ctx = await promptMounted({ mode: 'workspace-write', workspaceRoot: '/fallback' })
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('bash')
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
|
||||
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'))
|
||||
expect(await policySection(ctx, active)).toBe('Current DSH file policy: workspace-write. The write and edit tools, one-shot bash commands, and terminal sessions may modify files under the session workspace: "/projects/current". Some platform temporary areas may also be writable.')
|
||||
})
|
||||
|
||||
it('states that danger-full-access adds no DSH file restriction without claiming wider authority', async () => {
|
||||
it('states the exact families bypassed by danger-full-access', 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.')
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
|
||||
expect(await policySection(ctx, session('sess-danger', '/projects/current'))).toBe('Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict the write and edit tools or terminal sessions.')
|
||||
})
|
||||
|
||||
it('renders family contributions independently across mount and repeated disposal', async () => {
|
||||
const ctx = await promptMounted()
|
||||
const active = session('sess-family-lifecycle')
|
||||
const filesystemFiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
inner.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
}, { inject: ['sandboxPolicy'] }))
|
||||
expect(await policySection(ctx, active)).toContain('The write and edit tools cannot modify files')
|
||||
|
||||
let disposeBashFirst!: () => void
|
||||
const bashFirstFiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
disposeBashFirst = inner.sandboxPolicy.registerEnforcedFamily('bash')
|
||||
}, { inject: ['sandboxPolicy'] }))
|
||||
const bashSecondFiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
inner.sandboxPolicy.registerEnforcedFamily('bash')
|
||||
}, { inject: ['sandboxPolicy'] }))
|
||||
expect(await policySection(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
|
||||
disposeBashFirst()
|
||||
disposeBashFirst()
|
||||
expect(await policySection(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
|
||||
await bashSecondFiber.dispose()
|
||||
expect(await policySection(ctx, active)).toContain('The write and edit tools cannot modify files')
|
||||
await bashFirstFiber.dispose()
|
||||
await filesystemFiber.dispose()
|
||||
expect(await policySection(ctx, active)).toBe('')
|
||||
})
|
||||
|
||||
it('keeps the complete rendered prompt byte-stable across TMPDIR changes', async () => {
|
||||
const ctx = await promptMounted({ mode: 'workspace-write' })
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
const active = session('sess-tmpdir-stability', '/projects/current')
|
||||
const previous = process.env.TMPDIR
|
||||
try {
|
||||
process.env.TMPDIR = '/tmp/first-host-temp'
|
||||
const first = renderPrompt(await ctx.systemPrompt.assemble({ agent: agentFor(active) }))
|
||||
process.env.TMPDIR = '/tmp/second-host-temp'
|
||||
const second = renderPrompt(await ctx.systemPrompt.assemble({ agent: agentFor(active) }))
|
||||
expect(second).toBe(first)
|
||||
expect(second).not.toContain('host-temp')
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.TMPDIR
|
||||
else process.env.TMPDIR = previous
|
||||
}
|
||||
})
|
||||
|
||||
it('reflects the latest durable switch on the next assembly and stays byte-stable otherwise', async () => {
|
||||
const ctx = await promptMounted()
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
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(danger).toContain('does not restrict the write and edit tools')
|
||||
expect(await policySection(ctx, active)).toBe(danger)
|
||||
|
||||
setSandboxMode(active, 'workspace-write')
|
||||
@@ -189,6 +246,7 @@ describe('sandbox:policy request context', () => {
|
||||
setSandboxMode(active, 'workspace-write')
|
||||
const resumed = new Session(active.id, active.events, active.header)
|
||||
const ctx = await promptMounted({ mode: 'read-only' })
|
||||
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
|
||||
|
||||
expect(await policySection(ctx, resumed)).toContain('workspace-write')
|
||||
expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'sandbox:policy')?.text).toBe('')
|
||||
|
||||
Reference in New Issue
Block a user