refactor(sandbox-policy): remove capability family registries

This commit is contained in:
Tianyi Cui
2026-07-31 13:28:02 +08:00
parent e6407477a7
commit e088cfa59d
112 changed files with 260 additions and 403 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 packages/bash/bash-sandbox/README.md
README.md: 8012dbcbd656130b3d7b6701723c880dcf3d9d71
README.zh.md: de1077174ad802c0a18e5910792e61acd33c16ab
README.md: e9b040e7af55192e8afb9abf8bb4b99358ef8342
README.zh.md: f3956111f31ad85331caa47b1755667b096d2969

View File

@@ -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. 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.
- **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 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,7 +44,7 @@ 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`. Its family contribution also makes one-shot bash commands appear in the owner-rendered `sandbox:policy` section.
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 policy owner separately contributes the current capability-neutral `sandbox:policy` context.
#### Token effect

View File

@@ -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 家族;静态 bash 工具描述则单独负责拒绝与升级引导。
- **部署回退,每次调用策略。** [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) 为每次工具调用解析完整的 `SandboxExecutionPolicy`:调用会话提供自身的模式覆盖与不可变 cwd 根目录,部署配置则为无 agent智能体调用提供回退。已批准的升权只更改该策略的模式会话根目录仍然附着其上。`resolve()` 把策略带入 spec因此来自不同项目的重叠命令会在各自的根目录与模式下运行、分类和报告。能力事实 `ctx.bash.sandboxMode` 报告已配置的默认值,因此工具层只在装载该执行器时才公布升权;静态 bash 工具描述则单独负责拒绝与升级引导。
- **只限制文件影响。** 设计上不限制网络与进程可见性:模式词汇不会声称覆盖后端未强制执行的范围。
- 进程机制spawn、进程组终止、输出收集spill、后台句柄、凭证清理继承自 [`dsh-bash-local`](../bash-local/)runner 选择位于 [`dsh-sandbox-local`](../../sandbox/sandbox-local/)。
@@ -44,7 +44,7 @@
#### 模型看到的内容
基线是生成的 [`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` 段落中
基线是生成的 [`dsh-tool-bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash)。通过公布表明启用隔离的 `sandboxMode` 能力,此后端会为 `bash` 增加 `sandbox_permissions`,其 enum 为 `workspace-write` | `danger-full-access`,并增加 `justification`策略归属方会另行贡献当前且不区分具体能力`sandbox:policy` 上下文
#### Token 影响

View File

@@ -21,7 +21,7 @@ import { classifyDenial, classifyRunnerFailure, matchesSignature, shellQuote } f
* Plugin config: the local executor's knobs, verbatim. The sandbox policy —
* the default mode and fallback `workspace-write` root — is NOT here: it lives
* on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), which resolves
* each calling session's mode and cwd for both enforcing families. The runner
* each calling session's mode and cwd for every enforcing capability. The runner
* choice is likewise the `ctx.sandbox` provider's config, not this executor's.
*/
export type Config = LocalConfig
@@ -30,9 +30,8 @@ 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. 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.
* calls fall back to deployment policy. `result.sandbox` reports the mode and
* enforcement actually used.
*/
export class SandboxBashExecutor extends LocalBashExecutor {
static override inject = ['subprocess', 'sandbox', 'sandboxPolicy']
@@ -60,7 +59,6 @@ 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. */

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/bash/tool-bash/README.md
README.md: 7af756ef5198b0b459085f28f9f5920b905e7ddb
README.zh.md: cc2e824cdc6cf6da743537cef70997020285b74e
README.md: 29b9fba369e1fc6a4b8bb7bdd6543b7678df627d
README.zh.md: 31f691f7bfb8d2cb905751663151c3f6a6bc6c57

View File

@@ -69,11 +69,9 @@ Commands run with the executor's full authority unless a sandboxing executor ([`
Escalating bash calls resolve `ctx.approval` before execution. `allowed-once` applies the requested mode only to that call; rejection, cancellation, unavailability, or missing approval context executes nothing and returns a distinct error. On a real denial, the model may retry the same command once in the same turn with the narrowest sufficient mode and justification; the approval prompt itself is the consent step. Escalation is never speculative, and a disabled or rejected approval is final. The [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale.
When the executor confines and the schema exposes escalation, this plugin registers the `bash` family as escalatable with `ctx.sandboxPolicy`. The policy owner can therefore add anti-refusal context only when the actual bash tool offers the denial-and-approved-retry path; the contribution disposes with the plugin.
## Per-session mode switching
For sandboxing executors, each call resolves mode as one-shot escalation, then session override, then executor default. Non-sandboxing and agent-less calls carry no session override. The policy owner contributes current standing mode and composition-conditioned anti-refusal context; denial results still own the operation-specific effective mode and retry guidance. See the [`dsh-bash` fold](../bash/README.md) and [sandbox switching contract](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md).
For sandboxing executors, each call resolves mode as one-shot escalation, then session override, then executor default. Non-sandboxing and agent-less calls carry no session override. The policy owner contributes the current capability-neutral standing mode; denial results still own the operation-specific effective mode and retry guidance. See the [`dsh-bash` fold](../bash/README.md) and [sandbox switching contract](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md).
## Model Experience
@@ -81,7 +79,7 @@ For sandboxing executors, each call resolves mode as one-shot escalation, then s
#### What the model sees
Every request in this plugin's registration scope contains the bash guidance below. A sandboxing executor contributes capability facts through the policy owner's cache-safe runtime context rather than changing this section. Scoped tool restrictions can hide the schemas without removing this independently registered section.
Every request in this plugin's registration scope contains the bash guidance below. The policy owner contributes current sandbox state through its cache-safe runtime context rather than changing this section. Scoped tool restrictions can hide the schemas without removing this independently registered section.
##### Bash guidance

View File

@@ -69,11 +69,9 @@ overlay 根据当前 `ToolExecution` 计算,并通过专用的 `BashExecReques
需要升权的 bash 调用会在执行前解析 `ctx.approval``allowed-once` 只对该次调用应用请求模式;审批被拒、取消、不可用或缺少审批上下文时,命令完全不会执行,并返回不同的错误。发生真实拒绝后,模型可以在同一轮次中使用满足需要的最窄模式和理由重试同一命令一次;审批提示本身就是征求同意的步骤。升权绝不能预先推测,禁用或拒绝审批即为最终结果。其理由由 [沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) 持有。
执行器施加沙箱限制且 schema 公开升权能力时,此插件会将 `bash` 家族注册到 `ctx.sandboxPolicy`,标记为可升权。策略归属方因而只会在实际 bash 工具具备「拒绝后经批准重试」路径时添加反预防性拒绝上下文;该贡献随插件 dispose。
## 逐会话模式切换
对于启用沙箱的执行器,每次调用依次按单次升权、会话覆盖、执行器默认值解析模式。未启用沙箱以及没有 agent 的调用不携带会话覆盖。策略归属方通过缓存安全的运行时上下文贡献当前常驻模式与按组合条件化的反预防性拒绝上下文;拒绝结果仍负责操作特定的有效模式与重试引导。参见 [`dsh-bash` 整合](../bash/README.md)和[沙箱切换契约](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
对于启用沙箱的执行器,每次调用依次按单次升权、会话覆盖、执行器默认值解析模式。未启用沙箱以及没有 agent 的调用不携带会话覆盖。策略归属方贡献当前且不区分具体能力的常驻模式;拒绝结果仍负责操作特定的有效模式与重试引导。参见 [`dsh-bash` 整合](../bash/README.md)和[沙箱切换契约](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
## 模型体验
@@ -81,7 +79,7 @@ overlay 根据当前 `ToolExecution` 计算,并通过专用的 `BashExecReques
#### 模型看到的内容
此插件注册作用域内的每个请求都包含下方 bash 指引。启用沙箱的执行器会通过策略归属方的缓存安全运行时上下文贡献能力事实,而不改变此段落。作用域工具限制可以隐藏 schema但不会移除这个独立注册的段落。
此插件注册作用域内的每个请求都包含下方 bash 指引。策略归属方通过自身的缓存安全运行时上下文贡献当前沙箱状态,而不改变此段落。作用域工具限制可以隐藏 schema但不会移除这个独立注册的段落。
##### Bash 指引

View File

@@ -376,8 +376,6 @@ export function apply(ctx: Context, config: Config = {}): void {
if (defaultMode !== undefined && sandboxPolicy === undefined) {
throw new Error('tool-bash: the mounted bash executor confines but ctx.sandboxPolicy is missing')
}
if (escalationModes.length > 0) sandboxPolicy?.registerEscalatableFamily('bash')
/** Resolve the complete standing policy for this call when a confining executor is mounted. */
const resolveSandboxPolicy = (exec: ToolExecution): SandboxExecutionPolicy | undefined =>
sandboxPolicy?.resolve(exec.agent === undefined ? {} : { session: exec.agent.session })

View File

@@ -542,14 +542,6 @@ 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 request-input assemblies\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: 'registerEscalatableFamily(family: \'filesystem\' | \'bash\' | \'terminal\'): () => void',
jsDoc: '/**\n * Register one model-facing family whose tool schema and execution path offer\n * an approved wider retry after a real denial. Equal contributions remain\n * independently disposable; a family is narrated as escalatable only while\n * it is also enforced.\n * @param family - operation family whose tools expose escalation.\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 */',

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/fs/fs-sandbox/README.md
README.md: a376f2c23dca9f0895525fa274a1ae0545823f63
README.zh.md: 4fa2deecb7d10124da8b7920997196b816457fcc
README.md: bce22feac8b6b4a31abc0cbfddd39681ad437062
README.zh.md: 0bb0190f5a0506f2d10f82a5f03370b977472aee

View File

@@ -26,7 +26,7 @@ A denial is a structured `FsError` (`FS_SANDBOX_DENIED`, carrying the effective
#### 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.
The policy owner contributes capability-neutral `sandbox:policy` context. Indirectly, `dsh-tool-fs` renders this backend's `FS_SANDBOX_DENIED` refusals as the `[sandbox: file access denied under <mode> mode]` marker plus the same-turn escalation hint.
#### Token effect
@@ -34,7 +34,7 @@ The current-policy clause adds a small fixed prefix while this backend is mounte
#### KV Cache effect
A standing-policy or family-composition change updates the owner-rendered request prefix; operation results remain append-only.
A standing-policy change updates the owner-rendered request prefix; operation results remain append-only.
## Known Limitations and Deferred Work

View File

@@ -26,7 +26,7 @@
#### 模型看到的内容
该后端会向归属方渲染`sandbox:policy` 段落贡献 write/edit 家族。作为间接影响,`dsh-tool-fs` 会把本后端的 `FS_SANDBOX_DENIED` 拒绝渲染为 `[sandbox: file access denied under <mode> mode]` 标记和同轮次升级提示。
策略归属方会贡献与具体能力无关`sandbox:policy` 上下文。作为间接影响,`dsh-tool-fs` 会把本后端的 `FS_SANDBOX_DENIED` 拒绝渲染为 `[sandbox: file access denied under <mode> mode]` 标记和同轮次升级提示。
#### Token 影响
@@ -34,7 +34,7 @@
#### KV Cache 影响
常驻策略或家族组合发生变化时,归属方渲染的请求前缀会更新;操作结果保持仅追加。
常驻策略发生变化时,归属方渲染的请求前缀会更新;操作结果保持仅追加。
## 已知限制与暂缓事项

View File

@@ -44,7 +44,7 @@ import { isPathUnder } from './containment.ts'
* Plugin config: the local backend's knobs, verbatim (only `cwd`, the resolve
* base for relative paths). The sandbox default (mode + `workspace-write`
* fallback root) is NOT here — `ctx.sandboxPolicy` resolves each calling
* session for both enforcing families.
* session for every enforcing capability.
*/
export type Config = LocalConfig
@@ -63,7 +63,6 @@ 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. */

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/fs/tool-fs/README.md
README.md: eae4fca7fcb7416a139e17a2ec356b4445ba4fc3
README.zh.md: 8005fc63851ea2373ecd225358694d02cbe9a6ec
README.md: d8d75e251d6b565b402d97eed7bc8b135708e7c5
README.zh.md: 515320ee85219a55904702dfde1777f53f047cd3

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The **model-facing filesystem tools**`read`, `write`, `edit` — and their **executor**. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, **read windowing**, and result formatting. It reads/writes/edits through the `ctx.fs` provider seam ([`@deepseek-ai/dsh-fs`](../fs)) **directly**. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-policy`](../fs-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. Under a confining provider, the shared sandbox-policy service is required for per-session execution and the tool registers that filesystem mutations expose escalation.
The **model-facing filesystem tools**`read`, `write`, `edit` — and their **executor**. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, **read windowing**, and result formatting. It reads/writes/edits through the `ctx.fs` provider seam ([`@deepseek-ai/dsh-fs`](../fs)) **directly**. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-policy`](../fs-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. Under a confining provider, the shared sandbox-policy service is required for per-session execution and the tool exposes escalation for filesystem mutations.
```ts ignore-check
// Default deployment: a ctx.fs provider, the policy plugin, then the tools.
@@ -46,7 +46,7 @@ The tools do **not** inject a policy service or inspect any cache. Each tool res
The tool passes `exec` (the tool-execution context) as the opaque `actor` on every dispatch. The default thunks return `undefined` (the unconstrained bare provider). When `@deepseek-ai/dsh-fs-policy` is loaded it occupies the single decision slot — returning `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED` — and records on `fs/observed`. Backend errors (`FsError`) and a thrown `FS_NOT_OBSERVED` flow through `ToolRegistry.execute()` and become `isError` tool results with their `{ name, code }` attached.
When `ctx.fs.sandboxMode` reports confinement, write/edit advertise `sandbox_permissions` and `justification`, resolve approved retries through `ctx.approval`, and register `filesystem` as escalatable with `ctx.sandboxPolicy`. The policy owner therefore adds anti-refusal context only while the real mutation tools offer that path.
When `ctx.fs.sandboxMode` reports confinement, write/edit advertise `sandbox_permissions` and `justification` and resolve approved retries through `ctx.approval`. The policy owner contributes capability-neutral standing policy; the tool results retain operation-specific denial and retry guidance.
## `fs/observed` is fire-and-forget

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
**面向模型的文件系统工具**`read``write``edit`)及其**执行器**。这是文件系统栈的消费方层拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方 seam[`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会登记文件系统变更具备升权路径。
**面向模型的文件系统工具**`read``write``edit`)及其**执行器**。这是文件系统栈的消费方层拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方 seam[`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会文件系统变更提供升权路径。
```ts ignore-check
// Default deployment: a ctx.fs provider, the policy plugin, then the tools.
@@ -46,7 +46,7 @@ await ctx.plugin(ToolFs) // this package — re
工具在每次分派中把 `exec`(工具执行上下文)作为不透明 `actor` 传入。默认 thunk 返回 `undefined`(不受约束的裸提供方)。加载 `@deepseek-ai/dsh-fs-policy` 后,它会占用单个决策槽:返回 `createIfAbsent`/`replaceIfVersion`/`{ version }` 或抛出 `FS_NOT_OBSERVED`,并在 `fs/observed` 时记录。后端错误(`FsError`)和抛出的 `FS_NOT_OBSERVED` 会流经 `ToolRegistry.execute()`,变成 `isError` 工具结果,并附带 `{ name, code }`。
当 `ctx.fs.sandboxMode` 表明提供方施加沙箱限制时write/edit 会公开 `sandbox_permissions` 与 `justification`,通过 `ctx.approval` 解析经批准的重试,并将 `filesystem` 家族注册到 `ctx.sandboxPolicy`,标记为可升权。因此,策略归属方只会在实际变更工具提供该路径时添加反预防性拒绝上下文
当 `ctx.fs.sandboxMode` 表明提供方施加沙箱限制时write/edit 会公开 `sandbox_permissions` 与 `justification`通过 `ctx.approval` 解析经批准的重试。策略归属方会贡献与具体能力无关的常驻策略;工具结果仍保留操作特定的拒绝与重试引导
## `fs/observed` 发后即忘

View File

@@ -67,7 +67,6 @@ export function apply(ctx: Context, config: Config): void {
// per-call policy resolution, and denial-marker mapping, all keyed off whether
// the mounted ctx.fs confines (ctx.fs.sandboxMode).
const sandbox = new FsSandboxSurface(ctx)
if (sandbox.escalationModes.length > 0) ctx.get('sandboxPolicy')?.registerEscalatableFamily('filesystem')
applyWriteTool(ctx, sandbox)
applyEditTool(ctx, sandbox)
}

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/pty/pty-local/README.md
README.md: 5ad2c94f7c3e8e8bda5b7b432b3d0529a30f2b4e
README.zh.md: 0ab3f4a485315e6037d862f423f9e25b6200a63e
README.md: 4c200743732e62d8e76afd1a055943dac5f141b0
README.zh.md: 0fbcc0eb8c0b5ddb89e62b4e748d1bd280b50b36

View File

@@ -18,7 +18,7 @@ Send cancellation resolves the current foreground process group and delivers a r
#### What the model sees
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.
The policy owner contributes capability-neutral `sandbox:policy` context. 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
@@ -26,7 +26,7 @@ The current-policy clause is present while this backend is mounted. Retained PTY
#### KV Cache effect
A standing-policy or terminal-family change updates the owner-rendered request prefix; consumer results remain append-only.
A standing-policy change updates the owner-rendered request prefix; consumer results remain append-only.
## Known Limitations and Deferred Work

View File

@@ -18,7 +18,7 @@ Linux 的就绪检测结合以下机制:由前台状态验证的私有 bash
#### 模型看到的内容
该后端会向归属方渲染`sandbox:policy` 段落贡献终端家族。模型通过 `@deepseek-ai/dsh-tool-pty` 或其他 PTY 消费方还可能收到有界的 MOTD、发送增量、scrollback 页、就绪原因和清理错误。
策略归属方会贡献与具体能力无关`sandbox:policy` 上下文。模型通过 `@deepseek-ai/dsh-tool-pty` 或其他 PTY 消费方还可能收到有界的 MOTD、发送增量、scrollback 页、就绪原因和清理错误。
#### Token 影响
@@ -26,7 +26,7 @@ Linux 的就绪检测结合以下机制:由前台状态验证的私有 bash
#### KV Cache 影响
常驻策略或终端家族发生变化时,归属方渲染的请求前缀会更新;消费方结果保持仅追加。
常驻策略发生变化时,归属方渲染的请求前缀会更新;消费方结果保持仅追加。
## 已知限制与暂缓事项

View File

@@ -129,6 +129,5 @@ 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))
}

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: 0f5c9b7c21acddd789b21edf6c5a7e1316fe4053
README.zh.md: 10a4af24d82e8472803d96af1d9b7046731a7833
README.md: b2512790c5cf5b3a06523cf91b50dd5d288b1522
README.zh.md: dda98b7aa4775ac2453704295df87c3c92025093

View File

@@ -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 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.
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 receives one resolved mode-and-root policy per call; before each request, the model receives the current policy without a separate capability inventory.
## Why a shared home
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.
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, while the current context describes only what that policy means for any available operation the DSH file sandbox enforces. 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,9 +17,7 @@ Filesystem tools, one-shot bash commands, and terminal sessions may enforce the
- `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()`.
- `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.
- `ctx.sandboxPolicy.registerEscalatableFamily(family)` — independently registers a family whose actual tool schema and execution path offer an approved wider retry. Anti-refusal guidance names only families that are both enforced and escalatable; contributions dispose independently.
- `sandbox:policy` — a request-time cache-safe context contribution 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`.
- `sandbox:policy` — a request-time cache-safe context contribution derived directly from `resolve({ session })`. It states the mode's capability-neutral file-effect contract and the canonical session workspace under `workspace-write`; tool owners retain operation-specific denial and escalation guidance.
- `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.
@@ -36,24 +34,24 @@ A runtime switch is one log-only `sandbox/mode` event on the session it applies
#### What the model sees
One `sandbox:policy` contribution in the current runtime-context snapshot 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 contributes separately to the same snapshot, and plan guidance remains `dsh-plan-mode`'s system section.
One `sandbox:policy` contribution in the current runtime-context snapshot for every agent session. It does not enumerate mounted capabilities. Tool plugins retain operation and escalation guidance, approval policy contributes separately to the same snapshot, and plan guidance remains `dsh-plan-mode`'s system section.
##### Read-only
```markdown
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files in the standing mode. For the write and edit tools and one-shot bash commands, do not refuse a required modification from this standing mode alone: attempt it normally and follow the tool's denial and escalation guidance.
Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.
```
##### Workspace-write
```markdown
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.
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
```
##### Danger-full-access
```markdown
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.
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
```
#### Token effect

View File

@@ -2,11 +2,11 @@
[English](README.md) | 中文
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每强制执行家族在每次调用时都会收到一项解析完成的模式与根目录策略,并登记当前运行时对文件系统工具、一次性 bash 命令和终端会话中的哪些家族施加围栏;模型在每次请求前会收到这些当前事实
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.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)记录了共享策略决策。
文件系统工具、一次性 bash 命令和终端会话可以用不同组合强制执行同一套模式词汇。如果各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界,正是[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。每个强制执行后端都会消费归属方解析出的完整策略,而当前上下文只说明该策略对于任何受 DSH 文件沙箱强制执行的可用操作有何含义。[跨家族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了共享策略决策。
## 配置
@@ -17,9 +17,7 @@
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话最后一条 `sandbox/mode` 事件,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
- `ctx.sandboxPolicy.defaultMode``ctx.sandboxPolicy.workspaceRoot``resolve()` 使用的部署默认值与回退根目录。
- `ctx.sandboxPolicy.registerEnforcedFamily(family)`:独立注册 `filesystem``bash``terminal`,并返回对应的精确 effect disposer。相同家族仍是彼此独立的贡献该段落使用规范的家族顺序并且只有最后一项贡献离开后才移除对应家族
- `ctx.sandboxPolicy.registerEscalatableFamily(family)`:独立注册实际工具 schema 与执行路径可提供经批准的更宽松模式重试的家族。反预防性拒绝引导只会列出既受强制执行又可升权的家族;各项贡献独立释放。
- `sandbox:policy`:由 `resolve({ session })` 和当前家族贡献派生的请求时缓存安全上下文贡献。没有强制执行家族时为空,只说明模式、受影响的面向模型操作,以及 `workspace-write` 下规范化的会话工作区。
- `sandbox:policy`:直接派生自 `resolve({ session })` 的请求时缓存安全上下文贡献。它说明该模式中与具体能力无关的文件效果契约,以及 `workspace-write` 下规范化的会话工作区;工具归属方仍负责操作特定的拒绝与升权引导
- `effectiveSandboxMode(events)`:会话 `sandbox/mode` 事件的纯 fold最后一次切换胜出没有则为 `undefined`),在 `resolve()` 内使用。
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
@@ -36,24 +34,24 @@
#### 模型看到的内容
只要至少注册了一个强制执行家族,当前运行时上下文快照中就会有一项 `sandbox:policy` 贡献。以下示例展示全部三个家族;缺失的家族会被省略。工具插件继续负责操作与升级引导,批准策略单独贡献给同一份快照,计划引导仍由 `dsh-plan-mode` 的系统段落管理。
每个 agent 会话的当前运行时上下文快照中有一项 `sandbox:policy` 贡献。它不枚举已装载的能力。工具插件继续负责操作与升级引导,批准策略单独贡献给同一份快照,计划引导仍由 `dsh-plan-mode` 的系统段落管理。
##### 只读
```markdown
Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files in the standing mode. For the write and edit tools and one-shot bash commands, do not refuse a required modification from this standing mode alone: attempt it normally and follow the tool's denial and escalation guidance.
Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.
```
##### 工作区写入
```markdown
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.
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
```
##### 完全访问
```markdown
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.
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
```
#### Token 影响

View File

@@ -9,11 +9,9 @@
* consumers resolve without rewriting the stable system prompt.
*
* Enforcing filesystem, one-shot bash, and terminal backends read the SAME
* resolved policy here and register their independently disposable model-facing
* families. Tool owners separately register families whose schemas expose an
* approved wider retry. The context therefore describes only operations this
* runtime actually fences and adds anti-refusal guidance only where escalation
* exists, while each backend retains its own enforcement dialect. The service
* resolved policy here. The context describes that policy without inventorying
* capabilities, while each backend retains its own enforcement dialect and each
* tool owns its operation-specific denial and escalation guidance. The service
* reads session state once at each operation boundary; executors and providers
* remain session-free.
*
@@ -36,47 +34,15 @@ function resolveWorkspaceRoot(path: string): string {
return resolvePath(canonicalPath(path))
}
/** 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[],
escalatableFamilies: readonly FilePolicyFamily[],
): string {
if (families.length === 0) return ''
/** Render the policy without claiming which capabilities are mounted. */
function renderPolicyContext(policy: SandboxExecutionPolicy): string {
switch (policy.mode) {
case 'read-only': {
const subjects = familyList(families, 'and')
const standing = `Current DSH file policy: read-only. ${subjects[0]?.toUpperCase()}${subjects.slice(1)} cannot modify files in the standing mode.`
if (escalatableFamilies.length === 0) return standing
const escalatable = familyList(escalatableFamilies, 'and')
return `${standing} For ${escalatable}, do not refuse a required modification from this standing mode alone: attempt it normally and follow the tool's denial and escalation guidance.`
}
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 'read-only':
return 'Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.'
case 'workspace-write':
return `Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox 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 policy: danger-full-access. The DSH file sandbox does not restrict ${familyList(families, 'or')}.`
return 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.'
/* 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
@@ -118,10 +84,9 @@ export interface SandboxPolicyRequest {
/**
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
* 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.
* 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.
@@ -136,11 +101,6 @@ 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>>()
/** Independently disposable tool families that expose an approved wider retry. */
private readonly escalatableFamilies = new Map<FilePolicyFamily, Set<symbol>>()
constructor(ctx: Context, config: Config) {
super(ctx, 'sandboxPolicy')
// schemastery (static Config) already filled `mode`; the cast records that
@@ -157,59 +117,12 @@ export class SandboxPolicyService extends Service {
const session = context.agent?.session
return session === undefined
? ''
: renderPolicyContext(this.resolve({ session }), this.activeFamilies(), this.activeEscalatableFamilies())
: renderPolicyContext(this.resolve({ session }))
},
})
})
}
/**
* 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 request-input assemblies
* 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 {
return this.registerFamily(this.enforcedFamilies, family, 'sandboxPolicy.registerEnforcedFamily()')
}
/**
* Register one model-facing family whose tool schema and execution path offer
* an approved wider retry after a real denial. Equal contributions remain
* independently disposable; a family is narrated as escalatable only while
* it is also enforced.
* @param family - operation family whose tools expose escalation.
* @returns the exact Cordis effect disposer for this contribution.
*/
registerEscalatableFamily(family: 'filesystem' | 'bash' | 'terminal'): () => void {
return this.registerFamily(this.escalatableFamilies, family, 'sandboxPolicy.registerEscalatableFamily()')
}
/** Register one independently disposable family contribution in an owned map. */
private registerFamily(
registry: Map<FilePolicyFamily, Set<symbol>>,
family: FilePolicyFamily,
label: string,
): () => void {
const token = Symbol(family)
const dispose = this.ctx.effect(() => {
const contributions = registry.get(family) ?? new Set<symbol>()
contributions.add(token)
registry.set(family, contributions)
this.emitPromptChange()
return () => {
contributions.delete(token)
if (contributions.size === 0 && registry.get(family) === contributions) {
registry.delete(family)
}
this.emitPromptChange()
}
}, label)
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
@@ -235,21 +148,6 @@ 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)
}
/** Escalatable families that are also currently enforced, in canonical order. */
private activeEscalatableFamilies(): FilePolicyFamily[] {
return this.activeFamilies().filter(family => (this.escalatableFamilies.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

View File

@@ -1,7 +1,7 @@
/**
* Tests for the sandbox-policy home: the deployment default (mode +
* workspaceRoot) the service exposes, and the per-session `sandbox/mode`
* override kit (fold + write path) both enforcing families read.
* override kit (fold + write path) every enforcing capability reads.
*/
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
@@ -44,8 +44,6 @@ describe('SandboxPolicyService', () => {
const ctx = await mounted()
expect(ctx.sandboxPolicy.defaultMode).toBe('read-only')
expect(ctx.sandboxPolicy.workspaceRoot).toBe(resolve(process.cwd()))
const dispose = ctx.sandboxPolicy.registerEscalatableFamily('bash')
expect(() =>{ dispose() }).not.toThrow()
})
it('carries a configured mode and resolves the workspace root absolute', async () => {
@@ -127,11 +125,10 @@ describe('SandboxPolicyService', () => {
await expect(ctx.plugin(SandboxPolicyService, { mode: 'yolo' as never })).rejects.toThrow()
})
it('unregisters cleanly from a child fiber (HMR safety)', async () => {
it('disposes the service and context contribution from a child fiber (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
const fiber = await ctx.plugin(SandboxPolicyService, {})
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
expect(ctx.sandboxPolicy).toBeDefined()
expect(await policyContext(ctx, session('sess-hmr'))).toContain('read-only')
await fiber.dispose()
@@ -148,89 +145,20 @@ describe('sandbox:policy request context', () => {
return ctx
}
it('omits policy prose when no enforcing family is registered', async () => {
const ctx = await promptMounted()
expect(await policyContext(ctx, session('sess-no-family'))).toBe('')
})
it.each(['read-only', 'workspace-write', 'danger-full-access'] as const)('renders the exact %s policy without a capability inventory', async (mode) => {
const ctx = await promptMounted({ mode, workspaceRoot: '/fallback' })
const workspaceRoot = resolve('/projects/current')
const expected = {
'read-only': 'Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.',
'workspace-write': `Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(workspaceRoot)}. Some platform temporary areas may also be writable.`,
'danger-full-access': 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.',
} as const
it.each([
[['filesystem'], 'Current DSH file policy: read-only. The write and edit tools cannot modify files in the standing mode.'],
[['bash'], 'Current DSH file policy: read-only. One-shot bash commands cannot modify files in the standing mode.'],
[['terminal'], 'Current DSH file policy: read-only. Terminal sessions cannot modify files in the standing mode.'],
[['filesystem', 'bash'], 'Current DSH file policy: read-only. The write and edit tools and one-shot bash commands cannot modify files in the standing mode.'],
[['filesystem', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools and terminal sessions cannot modify files in the standing mode.'],
[['bash', 'terminal'], 'Current DSH file policy: read-only. One-shot bash commands and terminal sessions cannot modify files in the standing mode.'],
[['filesystem', 'bash', 'terminal'], 'Current DSH file policy: read-only. The write and edit tools, one-shot bash commands, and terminal sessions cannot modify files in the standing mode.'],
] 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 policyContext(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')
expect(await policyContext(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('adds anti-refusal guidance only for enforced families with a real escalation path', async () => {
const ctx = await promptMounted()
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
ctx.sandboxPolicy.registerEnforcedFamily('bash')
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
ctx.sandboxPolicy.registerEscalatableFamily('filesystem')
const disposeBash = ctx.sandboxPolicy.registerEscalatableFamily('bash')
ctx.sandboxPolicy.registerEscalatableFamily('terminal')
const isolated = await promptMounted()
isolated.sandboxPolicy.registerEnforcedFamily('filesystem')
isolated.sandboxPolicy.registerEscalatableFamily('terminal')
expect(await policyContext(isolated, session('sess-unenforced-escalation'))).not.toContain('do not refuse')
const active = session('sess-escalatable-families')
expect(await policyContext(ctx, active)).toContain('For the write and edit tools, one-shot bash commands, and terminal sessions, do not refuse')
disposeBash()
expect(await policyContext(ctx, active)).toContain('For the write and edit tools and terminal sessions, do not refuse')
})
it('states the exact families bypassed by danger-full-access', async () => {
const ctx = await promptMounted({ mode: 'danger-full-access' })
ctx.sandboxPolicy.registerEnforcedFamily('filesystem')
ctx.sandboxPolicy.registerEnforcedFamily('terminal')
expect(await policyContext(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 policyContext(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 policyContext(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
disposeBashFirst()
disposeBashFirst()
expect(await policyContext(ctx, active)).toContain('The write and edit tools and one-shot bash commands')
await bashSecondFiber.dispose()
expect(await policyContext(ctx, active)).toContain('The write and edit tools cannot modify files')
await bashFirstFiber.dispose()
await filesystemFiber.dispose()
expect(await policyContext(ctx, active)).toBe('')
expect(await policyContext(ctx, session(`sess-${mode}`, '/projects/../projects/current'))).toBe(expected[mode])
})
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 {
@@ -251,18 +179,17 @@ describe('sandbox:policy request context', () => {
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 policyContext(ctx, active)
expect(await policyContext(ctx, active)).toBe(first)
setSandboxMode(active, 'danger-full-access')
const danger = await policyContext(ctx, active)
expect(danger).toContain('does not restrict the write and edit tools')
expect(danger).toBe('Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.')
expect(await policyContext(ctx, active)).toBe(danger)
setSandboxMode(active, 'workspace-write')
expect(await policyContext(ctx, active)).toContain(JSON.stringify(resolve('/projects/current')))
expect(await policyContext(ctx, active)).toBe(`Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(resolve('/projects/current'))}. Some platform temporary areas may also be writable.`)
})
it('reconstructs resumed policy from the session log and omits diagnostics without an agent', async () => {
@@ -270,7 +197,6 @@ 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 policyContext(ctx, resumed)).toContain('workspace-write')
expect((await ctx.systemPrompt.assemble()).contexts.find(context => context.name === 'sandbox:policy')?.text).toBe('')