Merge pull request #7415 from deepseek-harness/feat/windows-writable-directory-sets
fix(sandbox): keep Windows extra directory grants in file tools
This commit is contained in:
commit
ae24f608a6
57 files changed
+683
-169
No files matched your search
@@ -15,5 +15,5 @@
|
||||
en: 10bebc7f982cf7f4
|
||||
zh: 9247a2ba57921a11
|
||||
/agent-note-retained-ptc-processes-follow-file-policy/consequences:
|
||||
en: b3ddf273158d3754
|
||||
zh: 916356f4074b3622
|
||||
en: 1f94ca28e0cb7564
|
||||
zh: d8a918c0c5dfc6d8
|
||||
@@ -28,4 +28,4 @@ An approved one-time escalation omits the state owner and receives a fresh tempo
|
||||
|
||||
## Consequences
|
||||
|
||||
Normal calls retain variables without changing file permissions. Changing sandbox mode discards variables. Escalated programs must be self-contained. Runtime consumers must release a retained owner before supplying a different policy without a Session mode change; the provider rejects inconsistent reuse. The sandbox inherits the existing backends’ file-effect coverage and platform limitations.
|
||||
Normal calls retain variables without changing file permissions. Changing sandbox mode discards variables. Escalated programs must be self-contained. Runtime consumers must release a retained owner before changing its mode, workspace root or Session identity; the provider rejects inconsistent reuse. [Directory-effect lifetimes](2026-10-26-session-writable-directory-effects.md) permit extra-directory changes while retained processes keep their launch grants. The sandbox inherits the existing backends’ file-effect coverage and platform limitations.
|
||||
@@ -28,4 +28,4 @@ Status: implemented
|
||||
|
||||
## 影响
|
||||
|
||||
普通调用保留变量,同时维持原文件权限。切换沙箱模式会丢弃变量。提权程序必须包含执行所需的完整代码。未发生 Session 模式变化时,运行时消费者在传入不同策略前必须释放保留状态的所有者;提供方拒绝策略不一致的复用。沙箱继承现有后端的文件约束范围与平台限制。
|
||||
普通调用保留变量,同时维持原文件权限。切换沙箱模式会丢弃变量。提权程序必须包含执行所需的完整代码。运行时消费者在更改模式、工作区根目录或 Session 身份前必须释放保留状态的所有者;提供方拒绝策略不一致的复用。[目录 effect 生命周期](2026-10-26-session-writable-directory-effects.zh.md)允许额外目录发生变化,同时保留进程继续使用启动时的授权。沙箱继承现有后端的文件约束范围与平台限制。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-26-session-writable-directory-effects.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-26-session-writable-directory-effects.md
|
||||
/agent-note-session-writable-directory-effects:
|
||||
en: b5a98da65d44e212
|
||||
zh: ba6842a9b4a890f7
|
||||
/agent-note-session-writable-directory-effects/problem:
|
||||
en: 2a32db1f27371403
|
||||
zh: 89cf04705ef66769
|
||||
/agent-note-session-writable-directory-effects/decision:
|
||||
en: af67969841bedb67
|
||||
zh: edabdfaa30bcadd1
|
||||
/agent-note-session-writable-directory-effects/alternatives-considered:
|
||||
en: dad77744dea17bde
|
||||
zh: bb3838f9f9099944
|
||||
/agent-note-session-writable-directory-effects/consequences:
|
||||
en: 1c2468df41bfbb46
|
||||
zh: 2118230c0091b20e
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Session writable-directory effects
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-26-session-writable-directory-effects.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Memory plugins need agent writes outside the workspace without granting every Session the same directory. Deployment-wide roots cannot express a plugin enabled for only one Session or a grant that ends when that plugin unloads.
|
||||
|
||||
## Decision
|
||||
|
||||
The sandbox-policy service owns effect-scoped directory registrations keyed by live Session identity. Configured grants remain shared; runtime grants apply only to their named Session. Each resolution captures the active union, and existing request-context logging records the directories advertised to the model. Resume and fork require new registrations rather than restoring authority from historical text.
|
||||
|
||||
Registration disposal affects subsequent operations, not already-resolved calls or running processes. This deliberately permits persistent terminals and PTC environments to retain their launch grants until they stop. The requirement is to update ongoing Sessions' future executions without coordinating process cancellation or losing retained state. This narrows the different-policy reuse restriction in the [retained PTC sandbox decision](2026-10-08-persistent-ptc-sandbox.md) for additional directories only; mode-change invalidation and one-time escalation cleanup remain unchanged.
|
||||
|
||||
Missing, non-directory, and symlinked extra paths stop execution with `SandboxInvalidRootError`, which identifies the path and its problem. Silently skipping a path would hide an invalid grant and let execution proceed with permissions different from those requested. Registration checks path spelling; enforcement validates the physical directory on each call without retaining an earlier resolution. Symlinks remain disallowed, keeping configured and plugin grants under the same validation rules.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Shared mutable extra roots.** Enabling memory in one Session would also authorize unrelated Sessions using the same service.
|
||||
|
||||
**Immediate process revocation.** Existing sandboxes apply grants at launch. Stopping and recreating processes would discard terminal or PTC state and require cleanup coordination beyond the accepted future-operation requirement.
|
||||
|
||||
**Persist runtime grants as Session permissions.** Replaying such grants could authorize directories after the plugin that supplied them has been removed. The active plugin composition owns these grants.
|
||||
|
||||
## Consequences
|
||||
|
||||
Independent registrations for the same directory survive another owner's disposal. Removing a registration cannot revoke overlapping configured or workspace access. A running process may retain a removed grant, or lack a newly added grant; a new process uses the current policy. Windows subprocesses ignore extra roots while file tools enforce them; the [Windows scope decision](2026-10-29-windows-file-tool-directory-grants.md) records this limitation. Focused coverage checks Session isolation, duplicate ownership, plugin unload, filesystem denial and mutation, request context, and retained PTC launch permissions.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Agent Note: 会话可写目录 effect
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-26-session-writable-directory-effects.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
记忆插件需要允许 agent 在工作区之外写入,同时不向所有 Session 授予相同目录。部署级根目录无法表达只为一个 Session 启用的插件,或随插件卸载而结束的授权。
|
||||
|
||||
## 决策
|
||||
|
||||
sandbox-policy 服务拥有以活跃 Session 身份为键、受 effect 生命周期管理的目录注册。配置授权仍然共享;运行时授权只适用于指定 Session。每次解析捕获当前授权的并集,现有请求上下文日志记录向模型展示的目录。恢复与派生需要重新注册,而非从历史文本恢复权限。
|
||||
|
||||
注册清理影响后续操作,不影响已经解析策略的调用或运行中的进程。这明确允许持久终端与 PTC 环境在停止前保留启动时的授权。需求是在不协调进程取消、不丢失保留状态的情况下更新运行中 Session 的后续执行。这仅针对额外目录收窄了[保留 PTC 沙箱决策](2026-10-08-persistent-ptc-sandbox.zh.md)中不同策略禁止复用的限制;模式变化失效与单次提权清理保持不变。
|
||||
|
||||
不存在、不是目录或经过符号链接的额外路径会以 `SandboxInvalidRootError` 阻止执行,错误指出路径及其问题。静默跳过路径会隐藏无效授权,使执行时的权限与请求的权限不同。注册时检查路径写法;执行限制时每次验证物理目录,不保留先前的解析结果。符号链接仍被禁止,使配置授权和插件授权遵循相同的验证规则。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**共享的可变额外根目录。** 在一个 Session 中启用记忆,也会授权使用同一服务的无关 Session。
|
||||
|
||||
**立即撤销进程权限。** 现有沙箱在启动时应用授权。停止并重新创建进程会丢失终端或 PTC 状态,并需要超出已接受的后续操作需求的清理协调。
|
||||
|
||||
**将运行时授权持久化为 Session 权限。** 回放此类授权可能在提供授权的插件已被移除后继续授权目录。当前插件组合拥有这些授权。
|
||||
|
||||
## 影响
|
||||
|
||||
同一目录的独立注册不会随另一个所有者的清理而移除。移除注册无法撤销重叠的配置授权或工作区访问权限。运行中的进程可能保留已移除的授权,或缺少新添加的授权;新进程使用当前策略。Windows 子进程忽略额外根目录,文件工具则执行这些授权;[Windows 范围决策](2026-10-29-windows-file-tool-directory-grants.zh.md)记录此限制。针对性覆盖检查 Session 隔离、重复注册的所有权、插件卸载、文件系统拒绝与修改、请求上下文,以及保留 PTC 环境的启动权限。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-29-windows-file-tool-directory-grants.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-29-windows-file-tool-directory-grants.md
|
||||
/agent-note-windows-additional-directory-grants-stay-in-file-tools:
|
||||
en: aea9858c74dfd36e
|
||||
zh: 4064573838d6000f
|
||||
/agent-note-windows-additional-directory-grants-stay-in-file-tools/problem:
|
||||
en: da14bde7ed3eae49
|
||||
zh: 6aad2181b2756b50
|
||||
/agent-note-windows-additional-directory-grants-stay-in-file-tools/decision:
|
||||
en: acec40cda6036f2c
|
||||
zh: 149c9eecb9863643
|
||||
/agent-note-windows-additional-directory-grants-stay-in-file-tools/alternatives-considered:
|
||||
en: 09d6cceef8004a65
|
||||
zh: e9009eb5bbae1d87
|
||||
/agent-note-windows-additional-directory-grants-stay-in-file-tools/consequences:
|
||||
en: a37925f508bb5644
|
||||
zh: f5a6cb4e099f94c6
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Windows additional directory grants stay in file tools
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-29-windows-file-tool-directory-grants.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Memory plugins need session-specific writes to directories such as `~/.dsh/memory`. Extending Windows process access through ACLs requires managing persistent filesystem grants and token identities for changing directory combinations, increasing reliance on a backend with existing enforcement limitations.
|
||||
|
||||
## Decision
|
||||
|
||||
On Windows, the [session directory effects](2026-10-26-session-writable-directory-effects.md) authorize extra directories only through the harness filesystem checks used by `write` and `edit`. The process provider ignores extra roots and retains its workspace and private-temp permissions. The ACL package, runner arguments, and workspace SID remain unchanged. POSIX process backends continue to honor extra roots.
|
||||
|
||||
Windows process support for extra roots is deferred until a better sandbox backend can enforce it without extending the ACL customization. Model context explicitly distinguishes file-tool grants from Windows process permissions.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One SID per directory or directory set.** These designs could select process authority for each execution, but require more ACL mutation, identity management, and lifecycle handling. The capability does not justify expanding this mechanism.
|
||||
|
||||
**Reject processes whenever extra grants exist.** This prevents unrelated PowerShell and workspace operations from running when a memory plugin registers a directory, even if the command never accesses it.
|
||||
|
||||
## Consequences
|
||||
|
||||
A Windows memory plugin can use `write` and `edit` for its registered directory while ordinary workspace commands continue to work. A shell command or PTC program cannot use that extra grant. This deliberately accepts different capabilities across file tools and process tools; registration and disposal remain session-scoped, and existing processes retain their launch-time permissions.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Windows 额外目录授权仅用于文件工具
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-29-windows-file-tool-directory-grants.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
记忆插件需要针对会话向 `~/.dsh/memory` 等目录写入。通过 ACL 扩展 Windows 进程访问权限,需要为不断变化的目录组合管理持久文件系统授权与令牌身份,从而加深对已有执行限制的后端的依赖。
|
||||
|
||||
## 决策
|
||||
|
||||
在 Windows 上,[会话目录 effect](2026-10-26-session-writable-directory-effects.zh.md) 仅通过 `write` 与 `edit` 使用的 Harness 文件系统检查授权额外目录。进程提供方忽略额外根目录,保留工作区与私有临时目录权限。ACL 包、runner 参数与工作区 SID 保持不变。POSIX 进程后端继续执行额外根目录授权。
|
||||
|
||||
Windows 进程的额外根目录支持推迟到更好的沙箱后端能够实现该功能、无需扩展 ACL 定制时。模型上下文明示文件工具授权与 Windows 进程权限的区别。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**为每个目录或目录集合分配 SID。** 这些设计可以逐次执行选择进程权限,但需要更多 ACL 修改、身份管理与生命周期处理。该能力不足以证明扩展此机制的必要性。
|
||||
|
||||
**存在额外授权时拒绝进程。** 当记忆插件注册目录时,这会阻止无关的 PowerShell 和工作区操作运行,即使命令从不访问该目录。
|
||||
|
||||
## 影响
|
||||
|
||||
Windows 记忆插件可以通过 `write` 与 `edit` 访问注册的目录,同时普通工作区命令继续运行。shell 命令或 PTC 程序无法使用该额外授权。这明确接受文件工具与进程工具的能力差异;注册和清理仍限定于会话,已有进程保留启动时的权限。
|
||||
@@ -2968,7 +2968,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `sessionProjections`
|
||||
- `refs`: [`SandboxMode`](subsystems/sandbox.md)
|
||||
- `source`: [`packages/sandbox/sandbox-policy/src/index.ts:94`](../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
- `source`: [`packages/sandbox/sandbox-policy/src/index.ts:95`](../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/**
|
||||
@@ -2992,7 +2992,7 @@ export interface Config {
|
||||
* automatic repository or gitfile discovery. Entries are execution-world
|
||||
* paths: this service validates absoluteness and detaches the list, while
|
||||
* the enforcing host rejects missing, non-directory, or symlinked paths. They grant nothing under
|
||||
* `read-only`.
|
||||
* `read-only`. Windows process confinement ignores them; harness file tools honor them.
|
||||
*/
|
||||
additionalWritableRoots?: string[]
|
||||
}
|
||||
|
||||
@@ -2970,7 +2970,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `sessionProjections`
|
||||
- `refs`: [`SandboxMode`](subsystems/sandbox.zh.md)
|
||||
- `source`: [`packages/sandbox/sandbox-policy/src/index.ts:94`](../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
- `source`: [`packages/sandbox/sandbox-policy/src/index.ts:95`](../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/**
|
||||
@@ -2994,7 +2994,7 @@ export interface Config {
|
||||
* automatic repository or gitfile discovery. Entries are execution-world
|
||||
* paths: this service validates absoluteness and detaches the list, while
|
||||
* the enforcing host rejects missing, non-directory, or symlinked paths. They grant nothing under
|
||||
* `read-only`.
|
||||
* `read-only`. Windows process confinement ignores them; harness file tools honor them.
|
||||
*/
|
||||
additionalWritableRoots?: string[]
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
en: e0d4876306b192c3
|
||||
zh: 63baaa0c79e87deb
|
||||
/process-sandbox/modes-and-enforcement:
|
||||
en: 6777e1b1270ec3e1
|
||||
zh: 22533fb3304c1dd8
|
||||
en: 3e518b62c9bbcd6e
|
||||
zh: d59d4a3830ce7cf6
|
||||
/process-sandbox/per-call-policy:
|
||||
en: fac6f5d32327dfb6
|
||||
zh: 1b8e4a1af48f33e8
|
||||
en: 53850780ce70e3d8
|
||||
zh: 60769c319d778e0e
|
||||
/process-sandbox/wrapped-argv-and-classification-dialects:
|
||||
en: 2ba8930762e92e40
|
||||
zh: 220f64da1a9e5d57
|
||||
|
||||
@@ -8,13 +8,13 @@ Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox
|
||||
|
||||
## Modes and enforcement
|
||||
|
||||
`SandboxMode` governs filesystem effects only. `read-only` asks the backend to deny writes — the POSIX runners additionally grant the `/dev/null` sink their shells require, while the Windows ACL runner grants no explicit writable root and reports partial enforcement for its ambient ACL gaps; `workspace-write` permits writes under the workspace root, explicit extra directories, and the backend's promised temp area; `danger-full-access` bypasses confinement. Network and process visibility are outside this vocabulary.
|
||||
`SandboxMode` governs filesystem effects only. `read-only` asks the backend to deny writes — the POSIX runners additionally grant the `/dev/null` sink their shells require, while the Windows ACL runner grants no explicit writable root and reports partial enforcement for its ambient ACL gaps; `workspace-write` permits writes under the workspace root, extra directories where supported, and the backend's promised temp area; `danger-full-access` bypasses confinement. Windows subprocesses ignore extra directory grants; the harness file tools enforce them independently. Network and process visibility are outside this vocabulary.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* File-effect policy for confined processes. `read-only` permits only required
|
||||
* sinks such as `/dev/null`; `workspace-write` also permits the workspace,
|
||||
* explicit extra roots, and backend-defined temp areas; `danger-full-access` bypasses confinement. Network
|
||||
* explicit extra roots where supported, and backend-defined temp areas; `danger-full-access` bypasses confinement. Network
|
||||
* and process visibility are outside this vocabulary.
|
||||
*/
|
||||
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
@@ -40,7 +40,7 @@ type SandboxEnforcement = 'full' | 'partial'
|
||||
|
||||
## Per-call policy
|
||||
|
||||
The complete execution policy is resolved and carried per capability call. It includes `danger-full-access` so a consumer can resolve policy once before deciding whether to bypass confinement. Normal tool calls derive `workspaceRoot` from the calling session's immutable cwd; deployment configuration supplies the agentless fallback and explicit additional write grants. The resolver preserves absolute execution-world spelling. Enforcing providers canonicalize the root where the files exist, so a cwd containing `symlink/..` identifies the directory where the paired subprocess provider actually runs.
|
||||
The complete execution policy is resolved and carried per capability call. It includes `danger-full-access` so a consumer can resolve policy once before deciding whether to bypass confinement. Normal tool calls derive `workspaceRoot` from the calling session's immutable cwd; deployment configuration supplies the agentless fallback and shared additional write grants. Live Session registrations add effect-scoped directories to subsequent resolutions. The resolver preserves absolute execution-world spelling. Enforcing providers canonicalize the root where the files exist, so a cwd containing `symlink/..` identifies the directory where the paired subprocess provider actually runs.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -57,6 +57,7 @@ interface SandboxExecutionPolicy {
|
||||
* Extra physical absolute directories `workspace-write` may also write under.
|
||||
* Absence means none. The enforcing host rejects missing, non-directory, and
|
||||
* symlinked entries on each call. Remote paths are checked on the remote host.
|
||||
* Windows process confinement ignores these; harness filesystem operations honor them.
|
||||
* These grant nothing under `read-only` or `danger-full-access`.
|
||||
*/
|
||||
additionalWritableRoots?: readonly string[]
|
||||
@@ -200,7 +201,7 @@ Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox
|
||||
|
||||
### `ctx.sandboxPolicy` — `SandboxPolicyService`
|
||||
|
||||
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
|
||||
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, session-scoped directory grants, and request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -208,12 +209,27 @@ The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mo
|
||||
* mode outranks the session's last `sandbox/mode` event, which outranks the
|
||||
* deployment default. A session cwd is its workspace-write boundary; the
|
||||
* configured root is the fallback for agentless calls and sessions without a
|
||||
* cwd.
|
||||
* cwd. Configured extra roots and this Session's active registrations are
|
||||
* snapshotted together; existing operations and processes keep their policy.
|
||||
* @param request - optional session and approved mode override.
|
||||
* @returns the fully resolved per-call mode and absolute workspace root.
|
||||
*/
|
||||
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy
|
||||
|
||||
/**
|
||||
* Permit writes under one directory in this Session's future workspace-write
|
||||
* operations. The calling context owns the effect; disposal removes only this
|
||||
* registration. Other grants to the directory remain effective. Existing
|
||||
* operations and persistent processes retain their resolved permissions.
|
||||
* Grants are runtime-only and must be registered again after resume or fork.
|
||||
* On Windows, only harness file tools honor extra grants; process permissions stay unchanged.
|
||||
* @param session - exact live Session receiving the grant; other Sessions and agentless calls are unaffected.
|
||||
* @param directory - absolute execution-world path; execution rejects missing, non-directory, or symlinked paths.
|
||||
* @returns the exact Cordis effect disposer for this registration.
|
||||
* @throws when the directory is relative or contains a NUL byte.
|
||||
*/
|
||||
registerWritableRoot(session: Session, directory: string): () => void
|
||||
|
||||
/**
|
||||
* Read the session override without applying the deployment default.
|
||||
* @param session - session whose log supplies the override.
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
## 模式与强制执行
|
||||
|
||||
`SandboxMode` 仅管控文件系统效果。`read-only` 要求后端拒绝写入——POSIX runner 还会授予其 shell 所需的 `/dev/null` 接收器,而 Windows ACL runner 不授予任何显式可写根目录,并因环境 ACL 缺口报告部分强制执行;`workspace-write` 允许在工作区根目录、显式额外目录及后端承诺的临时区域下写入;`danger-full-access` 绕过隔离。网络与进程可见性不在此处的定义范围内。
|
||||
`SandboxMode` 仅管控文件系统效果。`read-only` 要求后端拒绝写入——POSIX runner 还会授予其 shell 所需的 `/dev/null` 接收器,而 Windows ACL runner 不授予任何显式可写根目录,并因环境 ACL 缺口报告部分强制执行;`workspace-write` 允许在工作区根目录、受支持的额外目录及后端承诺的临时区域下写入;`danger-full-access` 绕过隔离。Windows 子进程忽略额外目录授权;Harness 文件工具独立执行这些授权。网络与进程可见性不在此处的定义范围内。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* File-effect policy for confined processes. `read-only` permits only required
|
||||
* sinks such as `/dev/null`; `workspace-write` also permits the workspace,
|
||||
* explicit extra roots, and backend-defined temp areas; `danger-full-access` bypasses confinement. Network
|
||||
* explicit extra roots where supported, and backend-defined temp areas; `danger-full-access` bypasses confinement. Network
|
||||
* and process visibility are outside this vocabulary.
|
||||
*/
|
||||
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
@@ -40,7 +40,7 @@ type SandboxEnforcement = 'full' | 'partial'
|
||||
|
||||
## 逐调用策略
|
||||
|
||||
完整执行策略会按每次能力调用解析并携带。它包括 `danger-full-access`,因此消费方可以只解析一次策略,再决定是否绕过约束。普通工具调用从调用会话的不可变 cwd 派生 `workspaceRoot`;部署配置提供无 agent 调用的回退值和显式额外写入授权。解析器保留执行环境中的绝对路径写法。执行限制的提供方在文件实际存在的位置规范化根目录,因此包含 `symlink/..` 的 cwd 会标识配套子进程提供方实际运行的目录。
|
||||
完整执行策略会按每次能力调用解析并携带。它包括 `danger-full-access`,因此消费方可以只解析一次策略,再决定是否绕过约束。普通工具调用从调用会话的不可变 cwd 派生 `workspaceRoot`;部署配置提供无 agent 调用的回退值和共享的额外写入授权。存活 Session 的注册为后续解析添加 effect 生命周期内的目录授权。解析器保留执行环境中的绝对路径写法。执行限制的提供方在文件实际存在的位置规范化根目录,因此包含 `symlink/..` 的 cwd 会标识配套子进程提供方实际运行的目录。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -57,6 +57,7 @@ interface SandboxExecutionPolicy {
|
||||
* Extra physical absolute directories `workspace-write` may also write under.
|
||||
* Absence means none. The enforcing host rejects missing, non-directory, and
|
||||
* symlinked entries on each call. Remote paths are checked on the remote host.
|
||||
* Windows process confinement ignores these; harness filesystem operations honor them.
|
||||
* These grant nothing under `read-only` or `danger-full-access`.
|
||||
*/
|
||||
additionalWritableRoots?: readonly string[]
|
||||
@@ -200,7 +201,7 @@ Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox
|
||||
|
||||
### `ctx.sandboxPolicy` — `SandboxPolicyService`
|
||||
|
||||
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
|
||||
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, session-scoped directory grants, and request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -208,12 +209,27 @@ The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mo
|
||||
* mode outranks the session's last `sandbox/mode` event, which outranks the
|
||||
* deployment default. A session cwd is its workspace-write boundary; the
|
||||
* configured root is the fallback for agentless calls and sessions without a
|
||||
* cwd.
|
||||
* cwd. Configured extra roots and this Session's active registrations are
|
||||
* snapshotted together; existing operations and processes keep their policy.
|
||||
* @param request - optional session and approved mode override.
|
||||
* @returns the fully resolved per-call mode and absolute workspace root.
|
||||
*/
|
||||
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy
|
||||
|
||||
/**
|
||||
* Permit writes under one directory in this Session's future workspace-write
|
||||
* operations. The calling context owns the effect; disposal removes only this
|
||||
* registration. Other grants to the directory remain effective. Existing
|
||||
* operations and persistent processes retain their resolved permissions.
|
||||
* Grants are runtime-only and must be registered again after resume or fork.
|
||||
* On Windows, only harness file tools honor extra grants; process permissions stay unchanged.
|
||||
* @param session - exact live Session receiving the grant; other Sessions and agentless calls are unaffected.
|
||||
* @param directory - absolute execution-world path; execution rejects missing, non-directory, or symlinked paths.
|
||||
* @returns the exact Cordis effect disposer for this registration.
|
||||
* @throws when the directory is relative or contains a NUL byte.
|
||||
*/
|
||||
registerWritableRoot(session: Session, directory: string): () => void
|
||||
|
||||
/**
|
||||
* Read the session override without applying the deployment default.
|
||||
* @param session - session whose log supplies the override.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.4/sandbox-unsupported-policy-error/guide.md
|
||||
/:
|
||||
en: d7c1fc703991b92a
|
||||
zh: f5cddebaa6b311a0
|
||||
/remove-sandboxunsupportedpolicyerror-imports:
|
||||
en: fc8fa3704fa036ce
|
||||
zh: e96b09c9882f760b
|
||||
/remove-sandboxunsupportedpolicyerror-imports/change:
|
||||
en: 3ca1bb8d19b7781e
|
||||
zh: 9f3800c847791344
|
||||
/remove-sandboxunsupportedpolicyerror-imports/migration:
|
||||
en: 29b8d3231a83e606
|
||||
zh: 24416c92a4dc9b71
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "The sandbox package removes SandboxUnsupportedPolicyError; Windows processes ignore additional writable roots."
|
||||
---
|
||||
|
||||
# Remove SandboxUnsupportedPolicyError imports
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`@deepseek-ai/dsh-sandbox` no longer exports `SandboxUnsupportedPolicyError`. Windows subprocess confinement ignores additional writable directories instead of rejecting execution with `SANDBOX_UNSUPPORTED_POLICY`. The harness `write` and `edit` tools still honor those directories; Windows processes retain workspace and private-temp access under `workspace-write`.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Remove imports and error handlers specific to `SandboxUnsupportedPolicyError` or `SANDBOX_UNSUPPORTED_POLICY`.
|
||||
2. Use the harness file tools for writes to additional directories on Windows. Do not assume a successful process launch grants those directories.
|
||||
3. Rebuild the consumer and verify that workspace commands run while extra directories remain accessible only through the file tools.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "sandbox 包移除 SandboxUnsupportedPolicyError;Windows 进程忽略额外可写根目录。"
|
||||
---
|
||||
|
||||
# 移除 SandboxUnsupportedPolicyError 导入
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`@deepseek-ai/dsh-sandbox` 不再导出 `SandboxUnsupportedPolicyError`。Windows 子进程隔离忽略额外可写目录,不再以 `SANDBOX_UNSUPPORTED_POLICY` 拒绝执行。Harness 的 `write` 与 `edit` 工具仍执行这些目录的授权;Windows 进程在 `workspace-write` 下保留工作区与私有临时目录访问权限。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 移除针对 `SandboxUnsupportedPolicyError` 或 `SANDBOX_UNSUPPORTED_POLICY` 的导入与错误处理。
|
||||
2. 在 Windows 上使用 Harness 文件工具写入额外目录。不要假定进程启动成功就意味着这些目录已获授权。
|
||||
3. 重新构建消费方,验证工作区命令可以运行,额外目录仍仅能通过文件工具访问。
|
||||
@@ -2586,7 +2586,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
{
|
||||
key: 'sandboxPolicy',
|
||||
summary: 'The sandbox-policy service (`ctx.sandboxPolicy`).',
|
||||
description: 'The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session\'s mode log and immutable cwd travel together to every enforcing capability.',
|
||||
description: 'The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, session-scoped directory grants, and request-time policy section. Tool layers call resolve for each execution so a session\'s mode log and immutable cwd travel together to every enforcing capability.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'readonly defaultMode: SandboxMode',
|
||||
@@ -2605,10 +2605,17 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
signature: 'resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy',
|
||||
description: 'Resolve the complete policy for one capability call. An approved explicit mode outranks the session\'s last `sandbox/mode` event, which outranks the deployment default. A session cwd is its workspace-write boundary; the configured root is the fallback for agentless calls and sessions without a cwd.',
|
||||
description: 'Resolve the complete policy for one capability call. An approved explicit mode outranks the session\'s last `sandbox/mode` event, which outranks the deployment default. A session cwd is its workspace-write boundary; the configured root is the fallback for agentless calls and sessions without a cwd. Configured extra roots and this Session\'s active registrations are snapshotted together; existing operations and processes keep their policy.',
|
||||
parameters: [{ name: 'request', description: 'optional session and approved mode override.' }],
|
||||
returns: 'the fully resolved per-call mode and absolute workspace root.',
|
||||
},
|
||||
{
|
||||
signature: 'registerWritableRoot(session: Session, directory: string): () => void',
|
||||
description: 'Permit writes under one directory in this Session\'s future workspace-write operations. The calling context owns the effect; disposal removes only this registration. Other grants to the directory remain effective. Existing operations and persistent processes retain their resolved permissions. Grants are runtime-only and must be registered again after resume or fork. On Windows, only harness file tools honor extra grants; process permissions stay unchanged.',
|
||||
parameters: [{ name: 'session', description: 'exact live Session receiving the grant; other Sessions and agentless calls are unaffected.' }, { name: 'directory', description: 'absolute execution-world path; execution rejects missing, non-directory, or symlinked paths.' }],
|
||||
returns: 'the exact Cordis effect disposer for this registration.',
|
||||
throws: ['when the directory is relative or contains a NUL byte.'],
|
||||
},
|
||||
{
|
||||
signature: 'overrideOf(session: Session): SandboxMode | undefined',
|
||||
description: 'Read the session override without applying the deployment default.',
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:*",
|
||||
|
||||
@@ -10,13 +10,14 @@
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { existsSync, realpathSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, parse } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { FsError, FsTargetKey } from '@deepseek-ai/dsh-fs'
|
||||
import type { FsTarget } from '@deepseek-ai/dsh-fs'
|
||||
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox'
|
||||
@@ -41,7 +42,7 @@ beforeEach(async ({ onTestFinished }) => {
|
||||
// Both siblings must be outside automatic temp grants for containment denials to be meaningful.
|
||||
const directory = await mkdtemp(join(outsideTempWorkspaceParent(), '.dsh-fssbx-'))
|
||||
onTestFinished(async () => { await rm(directory, { recursive: true, force: true }) })
|
||||
base = directory
|
||||
base = realpathSync.native(directory)
|
||||
assertWorkspaceOutsideTemp(base)
|
||||
workspace = join(base, 'ws')
|
||||
outside = join(base, 'out')
|
||||
@@ -91,6 +92,53 @@ describe('read-only', () => {
|
||||
describe('workspace-write containment', () => {
|
||||
beforeEach(() => boot('workspace-write'))
|
||||
|
||||
it('applies session grants to writes and edits until their plugin unloads', async ({ onTestFinished }) => {
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
const first = Session.create(SessionId('memory-first'))
|
||||
const second = Session.create(SessionId('memory-second'))
|
||||
const path = join(outside, 'memory.txt')
|
||||
const write = async (session: Session, content: string) => fs.writeText(
|
||||
await target(path), content, undefined, undefined, ctx.sandboxPolicy.resolve({ session }),
|
||||
)
|
||||
const edit = async (session: Session) => fs.editText(
|
||||
await target(path), { oldString: 'memory', newString: 'edited', replaceAll: false },
|
||||
undefined, undefined, ctx.sandboxPolicy.resolve({ session }),
|
||||
)
|
||||
await expect(write(first, 'denied')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
|
||||
const plugin = await ctx.plugin({
|
||||
inject: ['sandboxPolicy'],
|
||||
apply(scope: Context) { scope.sandboxPolicy.registerWritableRoot(first, outside) },
|
||||
})
|
||||
await write(first, 'memory')
|
||||
expect(await readFile(path, 'utf8')).toBe('memory')
|
||||
await expect(write(second, 'other session')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
|
||||
await expect(edit(second)).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
|
||||
await edit(first)
|
||||
expect(await readFile(path, 'utf8')).toBe('edited')
|
||||
setSandboxMode(first, 'read-only')
|
||||
await expect(write(first, 'read-only')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
|
||||
await expect(edit(first)).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
|
||||
setSandboxMode(first, 'workspace-write')
|
||||
await write(first, 'updated')
|
||||
await plugin.dispose()
|
||||
await expect(write(first, 'revoked')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
|
||||
await expect(edit(first)).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
|
||||
expect(await readFile(path, 'utf8')).toBe('updated')
|
||||
})
|
||||
|
||||
it('rejects invalid grants before workspace writes', async () => {
|
||||
const file = join(outside, 'file.txt')
|
||||
await writeFile(file, 'file')
|
||||
const path = join(workspace, 'blocked.txt')
|
||||
for (const invalid of [join(base, 'missing'), file, join(file, 'child')]) {
|
||||
const policy = { mode: 'workspace-write' as const, workspaceRoot: workspace,
|
||||
additionalWritableRoots: [outside, invalid] }
|
||||
await expect(fs.writeText(await target(path), 'blocked', undefined, undefined, policy))
|
||||
.rejects.toMatchObject({ code: 'SANDBOX_INVALID_ROOT' })
|
||||
expect(existsSync(path)).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an extra directory replaced by a symlink to an ungranted sibling', async () => {
|
||||
const extra = join(workspace, 'extra')
|
||||
await mkdir(extra)
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 7e1a8657a212c70b
|
||||
zh: afb7a8c6a3afd718
|
||||
/deepseek-ai-dsh-ptc-runtime-node/summary:
|
||||
en: fd81f3e969a3d8cc
|
||||
zh: a8211ce7c308dfd2
|
||||
en: 7f80d74a7f87eb05
|
||||
zh: 6aa31b97ac920ca6
|
||||
/deepseek-ai-dsh-ptc-runtime-node/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 725b47765ee8f30b
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 2a9fa3292bf07355
|
||||
zh: db0b5684820915eb
|
||||
/deepseek-ai-dsh-ptc-runtime-node/use-this-package/in-memory-modes:
|
||||
en: 0204fbdcb55c9c0d
|
||||
zh: 8e6b6f3974993ca0
|
||||
en: c82e89a3221bfa3f
|
||||
zh: 5b37a48d50291a06
|
||||
/deepseek-ai-dsh-ptc-runtime-node/use-this-package/deadlines-and-cancellation:
|
||||
en: d383ec7aeab8afec
|
||||
zh: 7c9c49e205fbbcd8
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Execute model-written TypeScript with host-provided async functions. The default `per-call` mode runs async function bodies in fresh Node processes; `vm-script` supports top-level await and optionally retains a context per Agent with `scriptPersistence`. Opt-in `persistent-in-memory` retains a REPL per Agent. All modes apply the selected file sandbox policy and return captured logs, lossless JSON, or structured failures. Elapsed deadlines, output bounds and a V8 heap limit constrain execution. Unacknowledged stops and one-shot completion clean up the managed process range; unavailable confinement fails execution.
|
||||
Execute model-written TypeScript with host-provided async functions. The default `per-call` mode runs async function bodies in fresh Node processes; `vm-script` supports top-level await and optionally retains a context per Agent with `scriptPersistence`. Opt-in `persistent-in-memory` retains a REPL per Agent. All modes apply the file sandbox policy at process launch and return captured logs, lossless JSON, or structured failures. Elapsed deadlines, output bounds and a V8 heap limit constrain execution. Unacknowledged stops and one-shot completion clean up the managed process range; unavailable confinement fails execution.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -83,7 +83,7 @@ Set `executionMode: vm-script` to evaluate erasable TypeScript through `node:vm`
|
||||
<a id="persistent-repl"></a>
|
||||
### In-memory modes
|
||||
|
||||
The `persistent-in-memory` mode and Script mode with `scriptPersistence: true` retain state per owner. To select the REPL evaluator, set `executionMode: persistent-in-memory` on the existing `@deepseek-ai/dsh-ptc-runtime-node` plugin. All modes require `fs`, `subprocess`, `sandbox` and `sandboxPolicy` and apply the selected file sandbox policy. Enable it deliberately with a profile patch:
|
||||
The `persistent-in-memory` mode and Script mode with `scriptPersistence: true` retain state per owner. To select the REPL evaluator, set `executionMode: persistent-in-memory` on the existing `@deepseek-ai/dsh-ptc-runtime-node` plugin. All modes require `fs`, `subprocess`, `sandbox` and `sandboxPolicy` and apply the file sandbox policy at process launch. Enable it deliberately with a profile patch:
|
||||
|
||||
```yaml
|
||||
- id: ptc-runtime
|
||||
@@ -95,7 +95,7 @@ Save the patch and pass its path to `dsh headless --patch <path>` in a compositi
|
||||
|
||||
In REPL mode, each live Agent keeps its own process and top-level variables, functions, and imported modules across `run_code` calls and turns. Use `await import(...)`, `console.log(...)`, and a final JSON expression; top-level `return` is not supported. Assignments to undeclared names create retained globals under Node REPL’s non-strict evaluation. `_` and `_error` are ordinary retained globals, without interactive REPL result setters. Node REPL declaration rules apply, including errors on redeclaring existing `let`, `const`, or `class` names; assign to a mutable binding or choose a new name. A leading `{ ... }` is a statement block and can contain top-level await and temporary declarations; parenthesize an object expression to return it. Retained functions and saved tool methods call the current evaluation's bindings. All tool calls must be awaited; callbacks from a completed evaluation cannot invoke tools in a later evaluation.
|
||||
|
||||
Memory is best-effort and never saved to disk. Restart, Agent disposal, provider unload, protocol failure, output overflow, or process exit clears it. Missing variables raise normal reference errors; reacquire handles and recreate declarations. Exceptions in the active evaluation settle once and preserve earlier mutations. In REPL mode, exceptions from callbacks belonging to a completed evaluation are not reported in a later result. In Script mode, uncaught callback exceptions can terminate the retained process; await asynchronous work before completing an evaluation. The Session still records programs, tool dispatches and visible results; restoring a Session or forking an Agent starts with empty memory. Each evaluation resets the process working directory to its resolved cwd; program calls to `process.chdir()` do not persist across evaluations. Direct Node access follows the resolved file policy. A retained owner cannot change mode, workspace root, additional writable roots, or Session identity until its environment is released.
|
||||
Memory is best-effort and never saved to disk. Restart, Agent disposal, provider unload, protocol failure, output overflow, or process exit clears it. Missing variables raise normal reference errors; reacquire handles and recreate declarations. Exceptions in the active evaluation settle once and preserve earlier mutations. In REPL mode, exceptions from callbacks belonging to a completed evaluation are not reported in a later result. In Script mode, uncaught callback exceptions can terminate the retained process; await asynchronous work before completing an evaluation. The Session still records programs, tool dispatches and visible results; restoring a Session or forking an Agent starts with empty memory. Each evaluation resets the process working directory to its resolved cwd; program calls to `process.chdir()` do not persist across evaluations. Direct Node access follows the process launch policy. A retained owner cannot change mode, workspace root, or Session identity until its environment is released. Additional writable directories are fixed when the process starts: later grants and revocations apply only after release and recreation.
|
||||
|
||||
Service consumers pass an object as `stateOwner` to reuse its environment and await `release(owner)` when finished. Requests without an owner run in temporary processes. Their failures leave other owners' retained variables intact and do not report clearing them. Permission setters record the new mode synchronously. The runtime immediately invalidates affected processes and begins cancellation when the Session mode changes, including idle environments. The next program waits for their cleanup before starting; cleanup failure blocks that program and remains retryable. Permission-setting success does not mean old processes have finished stopping. A one-time approved escalation uses a separate environment and awaits its cleanup before returning; it cannot read the standing environment's variables or retain new declarations. Concurrent runs for one owner reject until execution and required cleanup complete; distinct owners may run concurrently. An idle environment has no expiration timer and keeps its process until release or failure. Raw stdout/stderr have no evaluation identity: bytes received during an evaluation share its output budget, even when written by an earlier child process; idle stdout is discarded and idle stderr retains only the bounded diagnostic tail. Await child-process completion before ending an evaluation. Process-exit diagnostics include the exit code and retained stderr tail; idle failures are written to the host logger. If process observation fails during cleanup, releasing the owner retries termination; new runs remain blocked until cleanup succeeds. Pipes that fail to drain after confirmed process exit produce a diagnostic but do not prevent releasing the environment.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
执行模型编写的 TypeScript,并提供宿主异步函数。默认的 `per-call` 模式在全新 Node 进程中运行异步函数体;`vm-script` 支持顶层 await,并可通过 `scriptPersistence` 为每个 Agent 保留 context。显式启用的 `persistent-in-memory` 为每个 Agent 保留 REPL。所有模式都应用所选文件沙箱策略,返回捕获日志、无损 JSON 或结构化失败。执行时限、输出上限和 V8 堆上限约束运行。未确认的停止请求与单次执行完成后清理受管进程范围;约束不可用时执行失败。
|
||||
执行模型编写的 TypeScript,并提供宿主异步函数。默认的 `per-call` 模式在全新 Node 进程中运行异步函数体;`vm-script` 支持顶层 await,并可通过 `scriptPersistence` 为每个 Agent 保留 context。显式启用的 `persistent-in-memory` 为每个 Agent 保留 REPL。所有模式都在进程启动时应用文件沙箱策略,返回捕获日志、无损 JSON 或结构化失败。执行时限、输出上限和 V8 堆上限约束运行。未确认的停止请求与单次执行完成后清理受管进程范围;约束不可用时执行失败。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -83,7 +83,7 @@ profile 编辑器可从 `@deepseek-ai/dsh-ptc-runtime-node/configuration` 导入
|
||||
<a id="persistent-repl"></a>
|
||||
### 内存保留模式
|
||||
|
||||
`persistent-in-memory` 模式和启用 `scriptPersistence: true` 的 Script 模式都按所有者保留状态。要选择 REPL 求值器,在现有 `@deepseek-ai/dsh-ptc-runtime-node` 插件上设置 `executionMode: persistent-in-memory`。所有模式都需要 `fs`、`subprocess`、`sandbox` 和 `sandboxPolicy`,并应用所选文件沙箱策略。通过配置档补丁显式启用:
|
||||
`persistent-in-memory` 模式和启用 `scriptPersistence: true` 的 Script 模式都按所有者保留状态。要选择 REPL 求值器,在现有 `@deepseek-ai/dsh-ptc-runtime-node` 插件上设置 `executionMode: persistent-in-memory`。所有模式都需要 `fs`、`subprocess`、`sandbox` 和 `sandboxPolicy`,并在进程启动时应用文件沙箱策略。通过配置档补丁显式启用:
|
||||
|
||||
```yaml
|
||||
- id: ptc-runtime
|
||||
@@ -95,7 +95,7 @@ profile 编辑器可从 `@deepseek-ai/dsh-ptc-runtime-node/configuration` 导入
|
||||
|
||||
在 REPL 模式下,每个存活 Agent 保留独立进程,跨 `run_code` 调用和轮次保留顶层变量、函数及导入模块。使用 `await import(...)`、`console.log(...)` 和末尾 JSON 表达式;不支持顶层 `return`。Node REPL 使用非严格模式,给未声明的名称赋值会创建保留的全局变量。`_` 和 `_error` 是普通的保留全局变量,不使用交互式 REPL 的结果设置器。声明遵循 Node REPL 规则,包括重复声明已有 `let`、`const` 或 `class` 名称时报错;应给可变绑定赋值或选择新名称。开头的 `{ ... }` 按语句块执行,可以包含顶层 await 和临时声明;需要返回对象表达式时,用圆括号包裹。保留的函数和工具方法引用调用当前执行的绑定。必须等待所有工具调用;已完成执行的回调不能在后续执行中调用工具。
|
||||
|
||||
内存按尽力保留处理,不保存到磁盘。重启、Agent 销毁、提供方卸载、协议失败、输出超限或进程退出都会清除内存。缺失变量产生普通引用错误,模型需要重新获取句柄和创建声明。当前执行中的异常只结算一次,并保留此前发生的修改。在 REPL 模式下,已完成执行的回调抛出的异常不会在后续结果中报告。在 Script 模式下,未捕获的回调异常可能终止保留进程;结束执行前应等待异步工作完成。Session 仍记录程序、工具分派和可见结果;恢复 Session 或派生 Agent 时从空内存开始。每次执行都会将进程工作目录重置为本次解析的 cwd;程序中的 `process.chdir()` 不跨执行保留。直接 Node 访问遵循已解析的文件策略。保留状态的所有者必须先释放环境,才能更改权限模式、工作区根目录、额外可写根目录或 Session 身份。
|
||||
内存按尽力保留处理,不保存到磁盘。重启、Agent 销毁、提供方卸载、协议失败、输出超限或进程退出都会清除内存。缺失变量产生普通引用错误,模型需要重新获取句柄和创建声明。当前执行中的异常只结算一次,并保留此前发生的修改。在 REPL 模式下,已完成执行的回调抛出的异常不会在后续结果中报告。在 Script 模式下,未捕获的回调异常可能终止保留进程;结束执行前应等待异步工作完成。Session 仍记录程序、工具分派和可见结果;恢复 Session 或派生 Agent 时从空内存开始。每次执行都会将进程工作目录重置为本次解析的 cwd;程序中的 `process.chdir()` 不跨执行保留。直接 Node 访问遵循进程启动时的文件策略。保留状态的所有者必须先释放环境,才能更改权限模式、工作区根目录或 Session 身份。额外可写目录在进程启动时固定:之后的授权和撤销仅在释放并重新创建环境后生效。
|
||||
|
||||
服务消费者通过 `stateOwner` 对象复用环境,并在结束时等待 `release(owner)`。没有所有者的请求在临时进程中执行。此类执行失败不影响其他所有者保留的变量,也不报告清空了这些变量。权限设置同步记录新模式。Session 模式变化时,运行时立即禁止复用受影响的进程并开始取消,包括空闲环境。下一次程序执行前等待这些进程清理;清理失败会阻止本次程序启动,并允许重试。权限设置成功时,旧进程可能仍在停止过程中。单次获准的提权使用独立环境,返回前等待清理;该次执行无法访问原环境的变量,也不会保留新声明。执行及必要清理完成之前,同一所有者的并发调用会被拒绝;不同所有者可以并发。空闲环境没有过期计时器,进程保留到释放或失败为止。原始 stdout/stderr 不携带执行标识:执行期间收到的字节计入本次输出预算,包括较早启动的子进程此时写出的内容;空闲期间丢弃 stdout,只保留限额内的 stderr 诊断尾部。结束本次执行前,应等待子进程完成。进程退出诊断包含退出码和保留的 stderr 尾部;空闲期间的失败写入宿主日志。清理期间无法观测进程范围时,释放所有者会重试终止;清理成功前拒绝新执行。确认进程退出后仍未排空的管道会产生诊断,但不阻止释放环境。
|
||||
|
||||
|
||||
@@ -92,10 +92,10 @@ class NodeExecution {
|
||||
worker?.assertIdle()
|
||||
if (worker && !worker.closed && (worker.policy.mode !== spec.sandboxPolicy.mode
|
||||
|| worker.policy.workspaceRoot !== spec.sandboxPolicy.workspaceRoot
|
||||
|| worker.policy.sessionId !== spec.sandboxPolicy.sessionId
|
||||
|| !sameAdditionalRoots(worker.policy.additionalWritableRoots, spec.sandboxPolicy.additionalWritableRoots))) {
|
||||
|| worker.policy.sessionId !== spec.sandboxPolicy.sessionId)) {
|
||||
throw new Error('persistent PTC: release the retained environment before changing its sandbox policy')
|
||||
}
|
||||
// Retained processes keep their launch-time directory grants until release.
|
||||
if (!worker || worker.closed) {
|
||||
if (worker) this.workers.delete(worker)
|
||||
// Direct callers can own mutable policy arrays; the retained process owns its grant snapshot.
|
||||
@@ -127,19 +127,6 @@ class NodeExecution {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare requested extra grants without resolving paths in another execution world.
|
||||
* @param left - one policy's additional roots.
|
||||
* @param right - the other policy's additional roots.
|
||||
* @returns whether both contain the same paths, ignoring order and duplicates.
|
||||
*/
|
||||
function sameAdditionalRoots(left: readonly string[] | undefined, right: readonly string[] | undefined): boolean {
|
||||
const effective = (roots: readonly string[] | undefined): string[] => [...new Set(roots ?? [])].sort()
|
||||
const a = effective(left)
|
||||
const b = effective(right)
|
||||
return a.length === b.length && a.every((root, index) => root === b[index])
|
||||
}
|
||||
|
||||
const STRIP_PREFIX = 'async function __dsh_program__() {\n'
|
||||
const STRIP_SUFFIX = '\n}'
|
||||
const RETAINED_STATE_INSTRUCTIONS = 'Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation.'
|
||||
@@ -158,13 +145,16 @@ function resolveMode(config: ResolvedConfig): ExecutionMode {
|
||||
case 'persistent-in-memory': return {
|
||||
format: 'repl',
|
||||
retainsState: true,
|
||||
executionInstructions: 'The program contains TypeScript REPL statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Direct filesystem access follows this execution\'s sandbox policy; process.env starts empty. ' + RETAINED_STATE_INSTRUCTIONS,
|
||||
executionInstructions: 'The program contains TypeScript REPL statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Direct filesystem access follows the environment\'s launch-time sandbox policy; process.env starts empty. ' + RETAINED_STATE_INSTRUCTIONS,
|
||||
stripTypes: stripTypeScriptTypes,
|
||||
}
|
||||
case 'vm-script': {
|
||||
const lifetime = config.scriptPersistence
|
||||
? 'Variables, functions, and imported modules remain in memory for this Agent across calls.'
|
||||
: 'Each call runs in a fresh Node process.'
|
||||
const filePolicy = config.scriptPersistence
|
||||
? 'Direct filesystem access follows the environment\'s launch-time sandbox policy; process.env starts empty.'
|
||||
: 'Direct filesystem access follows this execution\'s sandbox policy; process.env starts empty.'
|
||||
const asynchronousFailure = 'Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process'
|
||||
+ (config.scriptPersistence ? ' and clears its variables.' : '.')
|
||||
const recovery = config.scriptPersistence
|
||||
@@ -173,7 +163,7 @@ function resolveMode(config: ResolvedConfig): ExecutionMode {
|
||||
return {
|
||||
format: 'script',
|
||||
retainsState: config.scriptPersistence,
|
||||
executionInstructions: 'The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. ' + lifetime + ' Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution\'s sandbox policy; process.env starts empty. ' + asynchronousFailure + ' Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false.' + recovery,
|
||||
executionInstructions: 'The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. ' + lifetime + ' Use await import(...) for modules. Relative paths use the supplied working directory. ' + filePolicy + ' ' + asynchronousFailure + ' Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false.' + recovery,
|
||||
stripTypes: stripTypeScriptTypes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,8 @@ export class ProgramWorker {
|
||||
if (spec.signal?.aborted) onAbort()
|
||||
else {
|
||||
try { evaluation.data.code = this.mode.stripTypes(spec.program) } catch (error: unknown) {
|
||||
evaluation.finish({ kind: 'exception', message: messageOf(error) })
|
||||
// A parse failure before startup has no process state or authority to retain.
|
||||
evaluation.finish({ kind: 'exception', message: messageOf(error) }, undefined, !this.ready)
|
||||
return await result.promise
|
||||
}
|
||||
if (this.ready) this.dispatch(evaluation)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { mkdtemp, mkdir, readFile, rm } from 'node:fs/promises'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -8,14 +9,15 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { mountRuntime } from './setup.ts'
|
||||
import type { Config } from '../src/index.ts'
|
||||
|
||||
async function setup(mode: 'read-only' | 'workspace-write' = 'read-only', executionMode: 'per-call' | 'persistent-in-memory' = 'persistent-in-memory') {
|
||||
const root = await mkdtemp(join(homedir(), '.dsh-persistent-sandbox-'))
|
||||
async function setup(mode: 'read-only' | 'workspace-write' = 'read-only', executionMode: NonNullable<Config['executionMode']> = 'persistent-in-memory') {
|
||||
const root = realpathSync.native(await mkdtemp(join(homedir(), '.dsh-persistent-sandbox-')))
|
||||
const cwd = join(root, 'workspace')
|
||||
await mkdir(cwd)
|
||||
const ctx = new Context()
|
||||
onTestFinished(async () => { await ctx.fiber.dispose(); await rm(root, { recursive: true, force: true }) })
|
||||
const { runtime } = await mountRuntime(ctx, { executionMode }, { mode, workspaceRoot: cwd })
|
||||
const { runtime } = await mountRuntime(ctx, { executionMode, scriptPersistence: executionMode === 'vm-script' }, { mode, workspaceRoot: cwd })
|
||||
const owner = {}
|
||||
const run = (program: string) => runtime.run(runtime.resolve({ program, bindings: [], stateOwner: owner }))
|
||||
return { root, cwd, ctx, runtime, owner, run }
|
||||
@@ -50,25 +52,70 @@ it('refuses reuse under another policy until the owner has been released', async
|
||||
})
|
||||
|
||||
// Windows ACL rejects additional roots; its refusal is covered by sandbox-local's runner tests.
|
||||
it.skipIf(process.platform === 'win32')('requires release before changing a retained environment\'s additional writable roots', async () => {
|
||||
const { root, cwd, runtime, owner, run } = await setup('workspace-write')
|
||||
it.skipIf(process.platform === 'win32').each(['persistent-in-memory', 'vm-script'] as const)('keeps %s directory grants until the retained process is released', async (executionMode) => {
|
||||
const { root, cwd, runtime, owner } = await setup('workspace-write', executionMode)
|
||||
const extra = join(root, 'extra')
|
||||
await mkdir(extra)
|
||||
expect((await run('var retained = 7; retained')).value).toBe(7)
|
||||
const spec = runtime.resolve({
|
||||
program: 'typeof retained',
|
||||
const target = join(extra, 'memory.txt')
|
||||
const roots = [extra]
|
||||
const policy = { mode: 'workspace-write' as const, workspaceRoot: cwd, additionalWritableRoots: roots }
|
||||
const write = (content: string, additionalWritableRoots: readonly string[] = []) => runtime.run(runtime.resolve({
|
||||
program: `await (await import("node:fs/promises")).writeFile(${JSON.stringify(target)}, ${JSON.stringify(content)})`,
|
||||
bindings: [],
|
||||
stateOwner: owner,
|
||||
sandboxPolicy: { mode: 'workspace-write', workspaceRoot: cwd, additionalWritableRoots: [extra] },
|
||||
})
|
||||
await expect(runtime.run(spec)).rejects.toThrow('release the retained environment')
|
||||
expect((await run('retained')).value).toBe(7)
|
||||
sandboxPolicy: { ...policy, additionalWritableRoots },
|
||||
}))
|
||||
|
||||
expect((await write('initial', roots)).error).toBeUndefined()
|
||||
roots[0] = cwd
|
||||
expect((await write('retained')).error).toBeUndefined()
|
||||
expect(await readFile(target, 'utf8')).toBe('retained')
|
||||
|
||||
await runtime.release(owner)
|
||||
expect((await runtime.run(spec)).value).toBe('undefined')
|
||||
const sameRoots = runtime.resolve({ ...spec, program: 'var second = 8; second', sandboxPolicy: { mode: 'workspace-write', workspaceRoot: cwd, additionalWritableRoots: [extra, extra] } })
|
||||
expect((await runtime.run(sameRoots)).value).toBe(8)
|
||||
const changed = runtime.resolve({ ...spec, sandboxPolicy: { mode: 'workspace-write', workspaceRoot: cwd, additionalWritableRoots: [cwd] } })
|
||||
await expect(runtime.run(changed)).rejects.toThrow('release the retained environment')
|
||||
expect((await write('denied')).sandbox).toMatchObject({ mode: 'workspace-write', denied: true })
|
||||
expect((await write('still denied', [extra])).sandbox).toMatchObject({ mode: 'workspace-write', denied: true })
|
||||
expect(await readFile(target, 'utf8')).toBe('retained')
|
||||
|
||||
await runtime.release(owner)
|
||||
expect((await write('renewed', [extra])).error).toBeUndefined()
|
||||
expect(await readFile(target, 'utf8')).toBe('renewed')
|
||||
})
|
||||
|
||||
// Windows ACL rejects additional roots before launching a process.
|
||||
it.skipIf(process.platform === 'win32').each([
|
||||
{ executionMode: 'persistent-in-memory', change: 'add' },
|
||||
{ executionMode: 'persistent-in-memory', change: 'remove' },
|
||||
{ executionMode: 'vm-script', change: 'add' },
|
||||
{ executionMode: 'vm-script', change: 'remove' },
|
||||
] as const)('uses current grants for the first $executionMode process after a syntax error and grant $change', async ({ executionMode, change }) => {
|
||||
const { root, cwd, ctx, runtime, owner } = await setup('workspace-write', executionMode)
|
||||
const extra = join(root, 'extra')
|
||||
await mkdir(extra)
|
||||
const target = join(extra, 'memory.txt')
|
||||
const session = ctx.sessions.create(SessionId('first-launch-grants'), { meta: { cwd } })
|
||||
const request = { bindings: [], stateOwner: owner }
|
||||
const remove = change === 'remove' ? ctx.sandboxPolicy.registerWritableRoot(session, extra) : undefined
|
||||
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
|
||||
onTestFinished(() => { spawn.mockRestore() })
|
||||
expect((await runtime.run(runtime.resolve({
|
||||
...request, program: 'const =', sandboxPolicy: ctx.sandboxPolicy.resolve({ session }),
|
||||
}))).error?.kind).toBe('exception')
|
||||
expect(spawn).not.toHaveBeenCalled()
|
||||
if (change === 'add') ctx.sandboxPolicy.registerWritableRoot(session, extra)
|
||||
else remove?.()
|
||||
|
||||
const result = await runtime.run(runtime.resolve({
|
||||
...request, sandboxPolicy: ctx.sandboxPolicy.resolve({ session }),
|
||||
program: `await (await import("node:fs/promises")).writeFile(${JSON.stringify(target)}, "memory")`,
|
||||
}))
|
||||
expect(spawn).toHaveBeenCalledTimes(1)
|
||||
if (change === 'add') {
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(await readFile(target, 'utf8')).toBe('memory')
|
||||
} else {
|
||||
expect(result.sandbox).toMatchObject({ mode: 'workspace-write', denied: true })
|
||||
await expect(readFile(target)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
}
|
||||
})
|
||||
|
||||
it('fails closed when confinement is unavailable without spawning Node', async () => {
|
||||
|
||||
@@ -39,14 +39,14 @@ describe.each([
|
||||
expect((await run('typeof retained')).value).toBe('undefined')
|
||||
} finally { await runtime.release(owner); await pending }
|
||||
})
|
||||
it('keeps a snapshot of caller-owned root arrays and rejects changed grants', async () => {
|
||||
it('retains variables across additional-root changes until release', async () => {
|
||||
const { run, runtime, owner, cwd } = await setup()
|
||||
const roots = [cwd]
|
||||
const policy = { mode: 'danger-full-access' as const, workspaceRoot: cwd, additionalWritableRoots: roots }
|
||||
expect((await run('var retained = 7; retained', { sandboxPolicy: policy })).value).toBe(7)
|
||||
expect((await run('retained', { sandboxPolicy: { ...policy, additionalWritableRoots: [cwd, cwd] } })).value).toBe(7)
|
||||
roots[0] = tmpdir()
|
||||
await expect(run('retained', { sandboxPolicy: policy })).rejects.toThrow('release the retained environment')
|
||||
expect((await run('retained', { sandboxPolicy: policy })).value).toBe(7)
|
||||
await runtime.release(owner)
|
||||
expect((await run('typeof retained', { sandboxPolicy: policy })).value).toBe('undefined')
|
||||
})
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
en: 7282c9ec77fd6f01
|
||||
zh: 10e385a9489105e3
|
||||
/deepseek-ai-dsh-sandbox-local/use-this-package/failures-and-recovery:
|
||||
en: 9b648f1bfdd55e76
|
||||
zh: d9265bdf2e272581
|
||||
en: b1f9aeca3baf9561
|
||||
zh: a5681f4f49259468
|
||||
/deepseek-ai-dsh-sandbox-local/understand-the-implementation:
|
||||
en: 15db6ec303151ccc
|
||||
zh: 91f62c1f64192f92
|
||||
@@ -54,8 +54,8 @@
|
||||
en: e290ccc9b3c92c08
|
||||
zh: 469589ac12cdadab
|
||||
/deepseek-ai-dsh-sandbox-local/known-limitations-and-deferred-work:
|
||||
en: 7cd5c6a4cc971803
|
||||
zh: 4e4f495b654d95c5
|
||||
en: 08b89c59db310961
|
||||
zh: b505db4148754f94
|
||||
/deepseek-ai-dsh-sandbox-local/known-limitations-and-deferred-work/dev-note:
|
||||
en: cc273320f6a1beaf
|
||||
zh: 5c57a312ddc50651
|
||||
|
||||
@@ -56,7 +56,7 @@ With the provider mounted, a command runs under the mode you resolve per call. E
|
||||
|
||||
An unsupported platform or an unusable runner fails closed: `confine()` rejects with `SANDBOX_UNAVAILABLE` and names the runner options for the platform, and the consumer surfaces that error rather than running the command unconfined. A runner that starts but refuses its profile is identified by its fatal stderr signature and exit code, so a broken sandbox is not mistaken for a denied command. The `runnerCommand` override is an operator assertion: it skips functional probes and assumes the configured runner implements the bwrap-compatible profile honestly.
|
||||
|
||||
Seatbelt, bwrap, and Landlock include the policy's explicit `additionalWritableRoots` under `workspace-write`. Read-only ignores those grants. Invalid extra directories raise `SANDBOX_INVALID_ROOT` before the command starts.
|
||||
Seatbelt, bwrap, and Landlock include the policy's explicit `additionalWritableRoots` under `workspace-write`. Read-only ignores those grants. Missing paths, non-directories, symlinked directories, and other inspection failures raise `SANDBOX_INVALID_ROOT` before the command starts. The Windows ACL backend does not inspect these extra roots and keeps workspace and private-temp access only.
|
||||
|
||||
-----
|
||||
|
||||
@@ -130,7 +130,7 @@ No direct invalidation; the named consumers own any request-prefix changes.
|
||||
|
||||
These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a general platform comparison or a task backlog.
|
||||
|
||||
- **Windows extra roots** — Windows ACL rejects every workspace-write subprocess carrying extra directories with `SANDBOX_UNSUPPORTED_POLICY`, before grants or child creation. Filesystem tools can enforce these grants independently. Remove the extra grants or use a supported host for process operations.
|
||||
- **Windows extra roots** — Additional directory grants apply to the harness `write` and `edit` tools only. PowerShell, terminals, PTC, and other sandboxed processes retain workspace and private-temp access under `workspace-write`. Extra grants do not change their ACLs or block their execution; see the [decision](../../../.agents/notes/implemented/architecture/2026-10-29-windows-file-tool-directory-grants.md).
|
||||
- **Windows ACL enforcement is partial** — NTFS hard links alias one file object across workspace and external paths, reads stay unconfined, and a tree another AppContainer tool has ACL'd with a package SID is unreadable to the Low-integrity child. The provider reports `enforcement: 'partial'` rather than overstating that boundary as full.
|
||||
- **Landlock may be partial** — older supported kernel ABIs confine only the access classes they expose, reported as `enforcement: 'partial'` rather than overstated as full.
|
||||
- **Seatbelt depends on deprecated `sandbox-exec`** — macOS still ships it, but this provider cannot replace or probe that private policy engine if Apple removes it.
|
||||
|
||||
@@ -56,7 +56,7 @@ kind: "package-reference"
|
||||
|
||||
不受支持的平台或不可用的 runner 会拒绝执行:`confine()` 以 `SANDBOX_UNAVAILABLE` 拒绝 并列出该平台的 runner 选项,消费方会呈现该错误,而不是让命令不受限制地运行。启动后拒绝自身 profile 的 runner 由其致命 stderr 签名与退出码识别,因此损坏的沙箱不会被误认为被拒绝的命令。`runnerCommand` 覆盖是操作方断言:它跳过功能探测,并假定配置的 runner 诚实实现与 bwrap 兼容的 profile。
|
||||
|
||||
Seatbelt、bwrap 和 Landlock 在 `workspace-write` 下包含策略显式指定的 `additionalWritableRoots`。只读模式忽略这些授权。无效的额外目录会在命令启动前触发 `SANDBOX_INVALID_ROOT`。
|
||||
Seatbelt、bwrap 和 Landlock 在 `workspace-write` 下包含策略显式指定的 `additionalWritableRoots`。只读模式忽略这些授权。不存在的路径、非目录、符号链接目录和其他检查失败会在命令启动前触发 `SANDBOX_INVALID_ROOT`。Windows ACL 后端不检查这些额外根目录,仅保留工作区与私有临时目录访问权限。
|
||||
|
||||
-----
|
||||
|
||||
@@ -130,7 +130,7 @@ Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,同
|
||||
|
||||
这些限制说明提供方何时不合适,或何时需要特别运维。它们是当前包约束,不是通用平台对比或任务积压。
|
||||
|
||||
- **Windows 额外根目录**——Windows ACL 在授权或创建子进程前,以 `SANDBOX_UNSUPPORTED_POLICY` 拒绝所有携带额外目录的 workspace-write 子进程。文件系统工具可独立执行这些授权。运行进程操作前,请移除额外授权或使用支持此功能的主机。
|
||||
- **Windows 额外根目录**——额外目录授权仅适用于 Harness 的 `write` 与 `edit` 工具。PowerShell、终端、PTC 及其他受沙箱限制的进程在 `workspace-write` 下保留工作区与私有临时目录访问权限。额外授权不修改其 ACL,也不阻塞其执行;参见[决策](../../../.agents/notes/implemented/architecture/2026-10-29-windows-file-tool-directory-grants.zh.md)。
|
||||
- **Windows ACL 只能实现部分强制执行**——NTFS 硬链接会使工作区路径与外部路径指向同一个文件对象,读取仍不受限,且被其他 AppContainer 工具以包 SID 标记过的目录树对 Low 完整性子进程不可读。提供方报告 `enforcement: 'partial'`,而不会把该边界夸大为完整强制执行。
|
||||
- **Landlock 可能只实现部分强制执行**——较旧且受支持的内核 ABI 只能限制自身公开的访问类别,因此报告 `enforcement: 'partial'`,不会夸大为完整强制执行。
|
||||
- **Seatbelt 依赖已弃用的 `sandbox-exec`**——macOS 仍会提供它,但若 Apple 移除该私有策略引擎,该提供方无法替换或探测。
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from '@deepseek-ai/node-addon-system/landlock-run'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { SandboxProvider, SandboxUnavailableError, SandboxUnsupportedPolicyError, canonicalPath } from '@deepseek-ai/dsh-sandbox'
|
||||
import { SandboxProvider, SandboxUnavailableError, canonicalPath } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { ConfinedArgv, ConfinedSandboxMode, RunnerFailureRule, SandboxEnforcement, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { AclWriteGrant, assertTempRootOutsideWorkspace, registerAclDiagnosisSkill, tempWriteSid, workspaceWriteSid } from '@deepseek-ai/dsh-sandbox-windows-acl'
|
||||
@@ -363,13 +363,12 @@ export class LocalSandboxProvider extends SandboxProvider {
|
||||
* `--temp-write-sid` and grants nothing itself. Agentless workspace-write
|
||||
* calls pass the ambient temp ROOT and no SID flags: the runner creates and
|
||||
* removes a random private child directory for that one invocation.
|
||||
* Additional directory grants apply only to harness filesystem operations
|
||||
* on Windows; process confinement keeps the workspace and private temp grants.
|
||||
* @param policy - the resolved per-call policy.
|
||||
* @returns the runner invocation.
|
||||
*/
|
||||
private windowsAclRunnerArgv(policy: SandboxPolicy): string[] {
|
||||
if (policy.mode === 'workspace-write' && (policy.additionalWritableRoots?.length ?? 0) > 0) {
|
||||
throw new SandboxUnsupportedPolicyError('Windows ACL confinement does not support additional writable directories; remove the extra grants or use a supported host')
|
||||
}
|
||||
const sessionId = policy.sessionId
|
||||
if (sessionId === undefined || policy.mode === 'read-only') {
|
||||
return [
|
||||
|
||||
@@ -146,8 +146,13 @@ describe('windows-acl write grants (LocalSandboxProvider)', () => {
|
||||
])
|
||||
expect(existsSync(tempDir ?? '')).toBe(true)
|
||||
|
||||
expect((await sandbox.confine(['pwsh', '/Command', 'x'], policy)).argv).toEqual(confined.argv)
|
||||
const extra = workspaceRoot()
|
||||
scratch.push(extra)
|
||||
expect((await sandbox.confine(['pwsh', '/Command', 'x'], {
|
||||
...policy, additionalWritableRoots: [extra],
|
||||
})).argv).toEqual(confined.argv)
|
||||
expect(mockState.grants).toHaveLength(2)
|
||||
expect(mockState.grants.flatMap(grant => grant.added.map(added => added.path))).toEqual([ws, tempDir])
|
||||
|
||||
await fiber.dispose()
|
||||
expect(mockState.grants.every(grant => grant.disposed)).toBe(true)
|
||||
|
||||
@@ -29,7 +29,8 @@ export async function checkExtraWrites(provider: LocalSandboxProvider, directory
|
||||
const workspaceRoot = realpathSync.native(await directory(homedir()))
|
||||
const extra = realpathSync.native(await directory(homedir()))
|
||||
const outside = realpathSync.native(await directory(homedir()))
|
||||
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot, additionalWritableRoots: [extra] }
|
||||
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot,
|
||||
additionalWritableRoots: [extra] }
|
||||
const allowed = join(extra, 'allowed.txt')
|
||||
const result = await run(provider, `printf allowed > ${quote(allowed)}`, policy)
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
|
||||
@@ -430,16 +430,21 @@ describe('the default seatbelt probe (sandbox-exec contract)', () => {
|
||||
})
|
||||
|
||||
describe('the windows-acl probe (runner invocation contract)', () => {
|
||||
it('refuses unsupported extra grants before a Windows runner or ACL mutation', async () => {
|
||||
it.each([RO, WW])('ignores extra directory grants in $mode runner arguments', async (policy) => {
|
||||
const { ctx, sandbox } = await setup({}, {
|
||||
chain: ['windows-acl'],
|
||||
probeWindowsAcl: () => true,
|
||||
windowsAclRunnerArgs: ['node', 'windows-acl-runner.js'],
|
||||
})
|
||||
const extra = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-extra-acl-')))
|
||||
tempDirs.push(extra)
|
||||
const file = join(extra, 'file.txt')
|
||||
writeFileSync(file, 'file')
|
||||
try {
|
||||
await expect(sandbox.confine(['true'], { ...WW, additionalWritableRoots: ['/extra'] }))
|
||||
.rejects.toThrow('Windows ACL confinement does not support additional writable directories')
|
||||
await expect(sandbox.confine(['true'], { ...RO, additionalWritableRoots: ['/extra'] })).resolves.toBeDefined()
|
||||
const original = await sandbox.confine(['true'], policy)
|
||||
await expect(sandbox.confine(['true'], {
|
||||
...policy, additionalWritableRoots: [extra, join(extra, 'missing'), file],
|
||||
})).resolves.toEqual(original)
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
en: 6704447e52ec295b
|
||||
zh: 9ed812d9e29d5533
|
||||
/deepseek-ai-dsh-sandbox-policy/use-this-package/minimal-configuration:
|
||||
en: 069de80f17900193
|
||||
zh: 2833f134c8dcb2e4
|
||||
en: 0616478ef5bb0566
|
||||
zh: e3a5802aefcf8424
|
||||
/deepseek-ai-dsh-sandbox-policy/use-this-package/registering-a-session-s-writable-directory:
|
||||
en: 841ea47ef1ed7ee6
|
||||
zh: cd573e67a67742e2
|
||||
/deepseek-ai-dsh-sandbox-policy/use-this-package/switching-a-session-s-mode:
|
||||
en: 957d72691e470e5c
|
||||
zh: c22a3a750af0e4a3
|
||||
@@ -75,8 +78,8 @@
|
||||
en: a16a657e0e5145fb
|
||||
zh: 4b19263a728990dc
|
||||
/deepseek-ai-dsh-sandbox-policy/known-limitations-and-deferred-work:
|
||||
en: 97f355c35ab69187
|
||||
zh: d64611e9b17e1ae0
|
||||
en: 8582027d60300aaf
|
||||
zh: c30cfcebf8a63e99
|
||||
/deepseek-ai-dsh-sandbox-policy/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 3bb0c20c93f16f98
|
||||
@@ -46,12 +46,18 @@ Load the package with a default mode; the fail-safe default is `read-only`, and
|
||||
|---|---|---|
|
||||
| `mode` | `read-only` | The deployment default mode a session starts from, validated at load |
|
||||
| `workspaceRoot` | `process.cwd()` | Absolute fallback root for agentless calls or sessions without a cwd; relative values fail at load. Normal agent calls use the session's immutable cwd |
|
||||
| `additionalWritableRoots` | `[]` | Explicit physical absolute directories also writable under `workspace-write`; other confined modes grant no access to them |
|
||||
| `additionalWritableRoots` | `[]` | Physical absolute directories also writable under `workspace-write`; Windows file tools honor them, Windows subprocesses ignore them, and `read-only` grants no access |
|
||||
|
||||
For a linked Git worktree, the caller can grant its shared Git directory with `additionalWritableRoots`. The caller selects that directory explicitly; the policy does not inspect Git files or grant a parent checkout. These grants are deployment configuration shared by the composition's sessions, rather than saved Session permissions. Granting the entire Git directory also permits changes to hooks and configuration that a later host Git invocation can execute outside the sandbox; use that grant only for trusted tasks. Use physical absolute directory paths. The enforcing host rejects missing paths, files, and symlinked roots, including a directory replaced by a symlink between calls. Remote grants are validated on the remote host.
|
||||
For a linked Git worktree, the caller can grant its shared Git directory with `additionalWritableRoots`. The caller selects that directory explicitly; the policy does not inspect Git files or grant a parent checkout. These grants are deployment configuration shared by the composition's sessions, rather than saved Session permissions. Granting the entire Git directory also permits changes to hooks and configuration that a later host Git invocation can execute outside the sandbox; use that grant only for trusted tasks. Use physical absolute directory paths. The enforcing host rejects missing paths, non-directories, and symlinked directories on each call, including a directory replaced by a symlink between calls. Remote grants are validated on the remote host.
|
||||
|
||||
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-sandbox-policy) is the exhaustive source for every accepted field and its JSDoc.
|
||||
|
||||
### Registering a session's writable directory
|
||||
|
||||
A plugin calls `ctx.sandboxPolicy.registerWritableRoot(session, directory)` through its owning context to add one physical absolute directory to that live Session's `workspace-write` permissions. The method returns the exact Cordis effect disposer; plugin unload or explicit disposal removes only that registration. Duplicate registrations and configured grants remain independent. Mount session-specific plugins under the Agent's context so their effects also end with the Agent. Registration validates the path spelling; the directory must exist on the execution host before an operation uses the grant. The same path checks as configured roots apply.
|
||||
|
||||
Registration and disposal affect subsequent policy resolutions and the next model request's logged context. Other Sessions, including subagent Sessions, and agentless calls receive no such grant. Grants are not saved or inherited through Session history; a resumed or forked Session needs its own registration. Already-resolved operations and persistent processes keep their original permissions, including directories whose registration has been removed. Start a new process to use the current grants. `read-only` remains read-only. On Windows, `write` and `edit` honor extra directories, while sandboxed processes ignore them and keep workspace and private-temp access; see [sandbox-local](../sandbox-local/README.md#known-limitations-and-deferred-work).
|
||||
|
||||
### Switching a session's mode
|
||||
|
||||
A session's mode can be switched at runtime through a UI policy control or an explicit switch; the switch is recorded in the session log and takes effect on the session's next confined call. The switch survives restart through replay, and each session keeps its own mode — two sessions never see each other's state. A switched session keeps its immutable workspace cwd as the writable boundary.
|
||||
@@ -128,7 +134,7 @@ Current DSH file policy: workspace-write. Any available operation enforced by th
|
||||
##### Workspace-write with extra directories
|
||||
|
||||
```markdown
|
||||
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Additional writable directories: ["/repo/.git"]. 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>". Additional writable directories: ["/repo/.git"]. On Windows, these additional grants apply to file tools only, not sandboxed processes. Some platform temporary areas may also be writable.
|
||||
```
|
||||
|
||||
##### Danger-full-access
|
||||
@@ -152,7 +158,7 @@ The stable system prompt remains byte-identical across mode changes. A changed f
|
||||
|
||||
These limits define the policy surface this package provides. They are current package constraints, not a general sandbox comparison or a task backlog.
|
||||
|
||||
- **Deployment-scoped extra grants** — additional directories come from configuration; they are not per-session permission settings. Backend support is defined by [sandbox-local](../sandbox-local/README.md).
|
||||
- **Runtime grants are not persisted** — configured directories are shared across sessions; effect registrations belong to one live Session and must be renewed after restore or fork. Backend support is defined by [sandbox-local](../sandbox-local/README.md).
|
||||
- **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 current context.
|
||||
|
||||
|
||||
@@ -46,12 +46,18 @@ kind: "package-reference"
|
||||
|---|---|---|
|
||||
| `mode` | `read-only` | 会话起始的部署默认模式,加载时验证 |
|
||||
| `workspaceRoot` | `process.cwd()` | 无 agent 调用或没有 cwd 的会话所用的绝对回退根目录;相对值在加载时拒绝。普通 agent 调用使用会话的不可变 cwd |
|
||||
| `additionalWritableRoots` | `[]` | 在 `workspace-write` 下额外允许写入的显式物理绝对目录;其他受限模式不授予这些目录的写权限 |
|
||||
| `additionalWritableRoots` | `[]` | 在 `workspace-write` 下额外允许写入的物理绝对目录;Windows 文件工具执行这些授权,Windows 子进程忽略它们,`read-only` 不授予访问权限 |
|
||||
|
||||
对于关联的 Git worktree,调用方可通过 `additionalWritableRoots` 授予共享 Git 目录的写权限。调用方必须显式选择该目录;策略不会读取 Git 文件或授权父检出目录。这些授权是组合中所有会话共享的部署配置,而非保存的 Session 权限。授权整个 Git 目录也允许修改钩子和配置,后续主机 Git 调用可能在沙箱之外执行它们;仅对可信任务使用此授权。使用物理绝对目录路径。执行限制的主机拒绝不存在的路径、文件和符号链接根目录,包括两次调用之间被替换为符号链接的目录。远端授权在远端主机上验证。
|
||||
对于关联的 Git worktree,调用方可通过 `additionalWritableRoots` 授予共享 Git 目录的写权限。调用方必须显式选择该目录;策略不会读取 Git 文件或授权父检出目录。这些授权是组合中所有会话共享的部署配置,而非保存的 Session 权限。授权整个 Git 目录也允许修改钩子和配置,后续主机 Git 调用可能在沙箱之外执行它们;仅对可信任务使用此授权。使用物理绝对目录路径。执行限制的主机在每次调用时拒绝不存在的路径、非目录和符号链接目录,包括两次调用之间被替换为符号链接的目录。远端授权在远端主机上验证。
|
||||
|
||||
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-sandbox-policy)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
||||
|
||||
### 注册会话的可写目录
|
||||
|
||||
插件通过自己的所属上下文调用 `ctx.sandboxPolicy.registerWritableRoot(session, directory)`,为该活跃 Session 的 `workspace-write` 权限添加一个物理绝对目录。该方法返回原始 Cordis effect 清理函数;插件卸载或显式清理只移除该次注册。重复注册与配置授权彼此独立。将会话专用插件挂载在 Agent 的上下文下,使其 effect 也随 Agent 结束。注册时验证路径写法;操作使用该授权前,目录必须已存在于执行主机上。路径检查与配置根目录相同。
|
||||
|
||||
注册与清理影响后续策略解析及下一次模型请求中记录的上下文。其他 Session(包括子 agent 的 Session)与无 agent 调用不会获得此授权。授权不随 Session 历史保存或继承;恢复或派生的 Session 需要自行注册。已经解析策略的操作与持久进程保留原权限,包括注册已被移除的目录。启动新进程才能使用当前授权。`read-only` 仍然只读。在 Windows 上,`write` 与 `edit` 执行额外目录授权,受沙箱限制的进程则忽略这些授权,保留工作区与私有临时目录访问权限;参见 [sandbox-local](../sandbox-local/README.zh.md#known-limitations-and-deferred-work)。
|
||||
|
||||
### 切换会话模式
|
||||
|
||||
会话的模式可以在运行时通过 UI 策略控件或显式切换来更改;切换记录在会话日志中,并在该会话的下一次受限调用时生效。切换通过回放跨重启保留,每个会话保持自己的模式——两个会话绝不会看到彼此状态。切换后的会话继续以不可变的工作区 cwd 作为写入边界。
|
||||
@@ -128,7 +134,7 @@ Current DSH file policy: workspace-write. Any available operation enforced by th
|
||||
##### 带额外目录的工作区写入
|
||||
|
||||
```markdown
|
||||
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Additional writable directories: ["/repo/.git"]. 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>". Additional writable directories: ["/repo/.git"]. On Windows, these additional grants apply to file tools only, not sandboxed processes. Some platform temporary areas may also be writable.
|
||||
```
|
||||
|
||||
##### 完全访问
|
||||
@@ -152,7 +158,7 @@ Current DSH file policy: danger-full-access. The DSH file sandbox does not restr
|
||||
|
||||
这些限制界定了本包提供的策略范围。它们是当前的包级约束,并非通用沙箱对比,也不是待办事项清单。
|
||||
|
||||
- **部署级额外授权**——额外目录来自配置,而非逐会话权限设置。后端支持情况见 [sandbox-local](../sandbox-local/README.zh.md)。
|
||||
- **运行时授权不持久化**——配置目录由各会话共享;effect 注册属于一个存活 Session,恢复或派生后必须重新注册。后端支持情况见 [sandbox-local](../sandbox-local/README.zh.md)。
|
||||
- **仅限文件操作模式**——`SandboxMode` 管控文件操作;网络和进程策略不在其词汇中,因此这里没有限制它们的旋钮。
|
||||
- **有意概述临时区域**——强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在当前上下文中如实枚举。
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the
|
||||
* deployment's sandbox fallbacks plus per-session resolution: the file-effect
|
||||
* {@link SandboxMode}, the `workspace-write` root, the explicit extra writable
|
||||
* directories, and the override kit (the `sandbox/mode` event, its fold, and
|
||||
* its write path; the fold is the `sandboxMode` session-projection unit
|
||||
* directories from configuration and live Session registrations, and the override
|
||||
* kit (the `sandbox/mode` event, its fold, and its write path; the fold is the
|
||||
* `sandboxMode` session-projection unit
|
||||
* registered here, while the event and write path come from
|
||||
* `./session-mode.ts`).
|
||||
* Before each agent request, the owner also contributes the resolved policy to
|
||||
@@ -50,7 +51,7 @@ function resolveAdditionalWritableRoots(roots: readonly string[] | undefined): r
|
||||
if (roots === undefined || roots.length === 0) return undefined
|
||||
for (const root of roots) {
|
||||
if (!isAbsolute(root) || root.includes('\0')) {
|
||||
throw new Error('sandbox-policy: additionalWritableRoots entries must be absolute execution-world paths')
|
||||
throw new Error('sandbox-policy: writable directories must be absolute execution-world paths without NUL bytes')
|
||||
}
|
||||
}
|
||||
return Object.freeze([...new Set(roots)])
|
||||
@@ -65,7 +66,7 @@ function renderPolicyContext(policy: SandboxExecutionPolicy): string {
|
||||
const additional = policy.additionalWritableRoots
|
||||
const extra = additional === undefined
|
||||
? ''
|
||||
: ` Additional writable directories: ${JSON.stringify(additional)}.`
|
||||
: ` Additional writable directories: ${JSON.stringify(additional)}. On Windows, these additional grants apply to file tools only, not sandboxed processes.`
|
||||
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)}.${extra} Some platform temporary areas may also be writable.`
|
||||
}
|
||||
case 'danger-full-access':
|
||||
@@ -105,7 +106,7 @@ export interface Config {
|
||||
* automatic repository or gitfile discovery. Entries are execution-world
|
||||
* paths: this service validates absoluteness and detaches the list, while
|
||||
* the enforcing host rejects missing, non-directory, or symlinked paths. They grant nothing under
|
||||
* `read-only`.
|
||||
* `read-only`. Windows process confinement ignores them; harness file tools honor them.
|
||||
*/
|
||||
additionalWritableRoots?: string[]
|
||||
}
|
||||
@@ -127,8 +128,8 @@ const sandboxModeStateSchema = zod.union([
|
||||
|
||||
/**
|
||||
* 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
|
||||
* default mode, fallback workspace root, session-scoped directory grants, and
|
||||
* 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 {
|
||||
@@ -149,6 +150,8 @@ export class SandboxPolicyService extends Service {
|
||||
readonly workspaceRoot: string
|
||||
/** The frozen extra writable directories shared by every resolved policy, or `undefined` for none. */
|
||||
readonly additionalWritableRoots: readonly string[] | undefined
|
||||
/** Independently owned grants for each live Session; never restored from its log. */
|
||||
private readonly writableRootRegistrations = new WeakMap<Session, Map<symbol, string>>()
|
||||
private sandboxModeProjectionHandle!: ProjectionHandle<SandboxMode | null>
|
||||
|
||||
constructor(ctx: Context, config: Config) {
|
||||
@@ -190,20 +193,56 @@ export class SandboxPolicyService extends Service {
|
||||
* mode outranks the session's last `sandbox/mode` event, which outranks the
|
||||
* deployment default. A session cwd is its workspace-write boundary; the
|
||||
* configured root is the fallback for agentless calls and sessions without a
|
||||
* cwd.
|
||||
* cwd. Configured extra roots and this Session's active registrations are
|
||||
* snapshotted together; existing operations and processes keep their policy.
|
||||
* @param request - optional session and approved mode override.
|
||||
* @returns the fully resolved per-call mode and absolute workspace root.
|
||||
*/
|
||||
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy {
|
||||
const { session } = request
|
||||
const registrations = session === undefined ? undefined : this.writableRootRegistrations.get(session)
|
||||
const additionalWritableRoots = registrations === undefined
|
||||
? this.additionalWritableRoots
|
||||
: resolveAdditionalWritableRoots([...(this.additionalWritableRoots ?? []), ...registrations.values()])
|
||||
return {
|
||||
mode: request.mode ?? (session === undefined ? undefined : this.overrideOf(session)) ?? this.defaultMode,
|
||||
workspaceRoot: resolveWorkspaceRoot(session?.header.cwd ?? this.workspaceRoot),
|
||||
...this.additionalWritableRoots === undefined ? {} : { additionalWritableRoots: this.additionalWritableRoots },
|
||||
...additionalWritableRoots === undefined ? {} : { additionalWritableRoots },
|
||||
...session === undefined ? {} : { sessionId: session.id },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permit writes under one directory in this Session's future workspace-write
|
||||
* operations. The calling context owns the effect; disposal removes only this
|
||||
* registration. Other grants to the directory remain effective. Existing
|
||||
* operations and persistent processes retain their resolved permissions.
|
||||
* Grants are runtime-only and must be registered again after resume or fork.
|
||||
* On Windows, only harness file tools honor extra grants; process permissions stay unchanged.
|
||||
* @param session - exact live Session receiving the grant; other Sessions and agentless calls are unaffected.
|
||||
* @param directory - absolute execution-world path; execution rejects missing, non-directory, or symlinked paths.
|
||||
* @returns the exact Cordis effect disposer for this registration.
|
||||
* @throws when the directory is relative or contains a NUL byte.
|
||||
*/
|
||||
registerWritableRoot(session: Session, directory: string): () => void {
|
||||
resolveAdditionalWritableRoots([directory])
|
||||
const dispose = this.ctx.effect(() => {
|
||||
let registrations = this.writableRootRegistrations.get(session)
|
||||
if (registrations === undefined) {
|
||||
registrations = new Map()
|
||||
this.writableRootRegistrations.set(session, registrations)
|
||||
}
|
||||
const key = Symbol()
|
||||
registrations.set(key, directory)
|
||||
return () => {
|
||||
registrations.delete(key)
|
||||
if (registrations.size === 0) this.writableRootRegistrations.delete(session)
|
||||
}
|
||||
}, 'sandboxPolicy.registerWritableRoot()')
|
||||
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup preserves the exact Cordis effect disposer
|
||||
return dispose
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the session override without applying the deployment default.
|
||||
* @param session - session whose log supplies the override.
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('SandboxPolicyService', () => {
|
||||
try {
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
await expect(ctx.plugin(SandboxPolicyService, { additionalWritableRoots: ['relative/extra'] }))
|
||||
.rejects.toThrow('sandbox-policy: additionalWritableRoots entries must be absolute execution-world paths')
|
||||
.rejects.toThrow('sandbox-policy: writable directories must be absolute execution-world paths without NUL bytes')
|
||||
expect(ctx.get('sandboxPolicy')).toBeUndefined()
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
@@ -182,6 +182,84 @@ describe('SandboxPolicyService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('session writable-root registrations', () => {
|
||||
it('isolates live Sessions, preserves configured roots, and detaches resolved policies', async ({ onTestFinished }) => {
|
||||
const ctx = await mounted({ mode: 'workspace-write', additionalWritableRoots: ['/shared'] })
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
const first = session('first', '/workspace')
|
||||
const second = session('second', '/workspace')
|
||||
const before = ctx.sandboxPolicy.resolve({ session: first })
|
||||
const remove = ctx.sandboxPolicy.registerWritableRoot(first, '/memory')
|
||||
const granted = ctx.sandboxPolicy.resolve({ session: first })
|
||||
|
||||
expect(granted.additionalWritableRoots).toEqual(['/shared', '/memory'])
|
||||
expect(Object.isFrozen(granted.additionalWritableRoots)).toBe(true)
|
||||
expect(before.additionalWritableRoots).toEqual(['/shared'])
|
||||
expect(ctx.sandboxPolicy.resolve({ session: second }).additionalWritableRoots).toEqual(['/shared'])
|
||||
expect(ctx.sandboxPolicy.resolve().additionalWritableRoots).toEqual(['/shared'])
|
||||
const resumed = Session.create(first.id, first.snapshotEvents(), first.header)
|
||||
expect(ctx.sandboxPolicy.resolve({ session: resumed }).additionalWritableRoots).toEqual(['/shared'])
|
||||
|
||||
remove()
|
||||
expect(ctx.sandboxPolicy.resolve({ session: first }).additionalWritableRoots).toEqual(['/shared'])
|
||||
expect(granted.additionalWritableRoots).toEqual(['/shared', '/memory'])
|
||||
})
|
||||
|
||||
it('keeps duplicate grants independently owned across plugin unload and explicit disposal', async ({ onTestFinished }) => {
|
||||
const ctx = await mounted({ mode: 'workspace-write', additionalWritableRoots: ['/shared'] })
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
const active = session('duplicates')
|
||||
const fiber = await ctx.plugin({
|
||||
inject: ['sandboxPolicy'],
|
||||
apply(scope: Context) {
|
||||
scope.sandboxPolicy.registerWritableRoot(active, '/memory')
|
||||
scope.sandboxPolicy.registerWritableRoot(active, '/plugin-only')
|
||||
scope.sandboxPolicy.registerWritableRoot(active, '/shared')
|
||||
},
|
||||
})
|
||||
const remove = ctx.sandboxPolicy.registerWritableRoot(active, '/memory')
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active }).additionalWritableRoots).toEqual(['/shared', '/memory', '/plugin-only'])
|
||||
await fiber.dispose()
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active }).additionalWritableRoots).toEqual(['/shared', '/memory'])
|
||||
remove()
|
||||
const replacement = ctx.sandboxPolicy.registerWritableRoot(active, '/replacement')
|
||||
remove()
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active }).additionalWritableRoots).toEqual(['/shared', '/replacement'])
|
||||
replacement()
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active }).additionalWritableRoots).toEqual(['/shared'])
|
||||
})
|
||||
|
||||
it.each(['relative/memory', '/memory\0invalid'])('rejects an invalid directory without installing a grant: %j', async (directory) => {
|
||||
const ctx = await mounted()
|
||||
try {
|
||||
const active = session('invalid')
|
||||
expect(() => ctx.sandboxPolicy.registerWritableRoot(active, directory)).toThrow('absolute execution-world paths')
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active }).additionalWritableRoots).toBeUndefined()
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('updates request context without changing read-only policy or the stable prompt', async ({ onTestFinished }) => {
|
||||
const ctx = await mounted({ mode: 'workspace-write' })
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
await ctx.plugin(SystemPrompt)
|
||||
const active = session('context', '/workspace')
|
||||
const before = await ctx.systemPrompt.assemble({ agent: agentFor(active) })
|
||||
const remove = ctx.sandboxPolicy.registerWritableRoot(active, '/memory')
|
||||
const after = await ctx.systemPrompt.assemble({ agent: agentFor(active) })
|
||||
expect(renderPrompt(after)).toBe(renderPrompt(before))
|
||||
expect(renderContextSnapshot(after)).toContain('Additional writable directories: ["/memory"]')
|
||||
setSandboxMode(active, 'read-only')
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active }).mode).toBe('read-only')
|
||||
expect(await policyContext(ctx, active)).not.toContain('/memory')
|
||||
setSandboxMode(active, 'workspace-write')
|
||||
remove()
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active }).additionalWritableRoots).toBeUndefined()
|
||||
expect(await policyContext(ctx, active)).not.toContain('/memory')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sandbox:policy request context', () => {
|
||||
async function promptMounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string; additionalWritableRoots?: string[] } = {}): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
@@ -210,7 +288,7 @@ describe('sandbox:policy request context', () => {
|
||||
additionalWritableRoots: ['/projects/shared/.git', '/projects/shared/worktrees'],
|
||||
})
|
||||
expect(await policyContext(ctx, session('sess-extra', '/projects/current'))).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('/projects/current')}. Additional writable directories: ["/projects/shared/.git","/projects/shared/worktrees"]. 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: ${JSON.stringify('/projects/current')}. Additional writable directories: ["/projects/shared/.git","/projects/shared/worktrees"]. On Windows, these additional grants apply to file tools only, not sandboxed processes. Some platform temporary areas may also be writable.`,
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: c5540889017b6a7f
|
||||
zh: 679bfbd5254bf92d
|
||||
/deepseek-ai-dsh-sandbox/use-this-package/modes-and-enforcement:
|
||||
en: aabe9791ece99ec1
|
||||
zh: 1dbc86eabb3a5548
|
||||
en: d3c906ddb4c47cbc
|
||||
zh: d264b9103e3566d6
|
||||
/deepseek-ai-dsh-sandbox/use-this-package/denied-calls-and-escalation:
|
||||
en: 3960c33a1e322cf9
|
||||
zh: 4d85c962067fe7a1
|
||||
@@ -45,8 +45,8 @@
|
||||
en: 1b418d41f571ab3d
|
||||
zh: 62b289cdd6f4d272
|
||||
/deepseek-ai-dsh-sandbox/understand-the-implementation/writable-roots:
|
||||
en: 00dcc13597416429
|
||||
zh: eaff9233d0611ad0
|
||||
en: 6be9f7d4c65761d6
|
||||
zh: f00d9970ed58e3e0
|
||||
/deepseek-ai-dsh-sandbox/further-exploration:
|
||||
en: 33d09277fb15d59f
|
||||
zh: 920ffe66671b56f3
|
||||
|
||||
@@ -56,7 +56,7 @@ The mode names the file effects a command may perform; enforcement completeness
|
||||
| Mode | Effect |
|
||||
|---|---|
|
||||
| `read-only` | Denies writes except required sinks such as `/dev/null` |
|
||||
| `workspace-write` | Allows writes under the workspace root, explicitly configured additional roots, and a backend-defined temp area |
|
||||
| `workspace-write` | Allows writes under the workspace root, additional roots where supported, and a backend-defined temp area |
|
||||
| `danger-full-access` | Bypasses confinement; the consumer spawns its original argv |
|
||||
|
||||
Enforcement is reported per call: `full` means the backend governs every promised file effect, while `partial` means an active backend or older kernel ABI governs only a subset — the Windows ACL rung and older Landlock ABIs are the current partial cases, so a consumer that requires the absolute boundary can reject or surface them.
|
||||
@@ -100,7 +100,7 @@ The ladder is a closed table — `read-only` may escalate to `workspace-write` o
|
||||
|
||||
### Writable roots
|
||||
|
||||
`workspaceWritableRoots` derives the workspace and explicit extra directories for bwrap and Landlock. `writableRoots` adds the host temporary areas for Seatbelt and the in-process filesystem fence. Both deduplicate roots and reject extra paths that are missing, are not directories, or resolve through symlinks. See the [mode definitions](../../../docs/subsystems/sandbox.md).
|
||||
`workspaceWritableRoots` derives the workspace and explicit extra directories for bwrap and Landlock. `writableRoots` adds the host temporary areas for Seatbelt and the in-process filesystem fence. Both deduplicate roots and reject extra paths that are missing, are not directories, or resolve through symlinks. Path comparison accepts Windows casing and separator differences. Each execution checks the paths again. Windows subprocesses ignore additional roots; file tools still enforce them. See the [mode definitions](../../../docs/subsystems/sandbox.md).
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ kind: "package-reference"
|
||||
| 模式 | 效果 |
|
||||
|---|---|
|
||||
| `read-only` | 拒绝写入,必需 sink(如 `/dev/null`)除外 |
|
||||
| `workspace-write` | 允许写入工作区根目录、显式配置的额外根目录及后端定义的临时区域 |
|
||||
| `workspace-write` | 允许写入工作区根目录、受支持的额外根目录及后端定义的临时区域 |
|
||||
| `danger-full-access` | 绕过隔离;消费方直接 spawn 原始 argv |
|
||||
|
||||
强制执行逐调用报告:`full` 表示后端管辖模式承诺的每个文件操作,`partial` 表示活动后端或较旧的内核 ABI 只管辖子集——Windows ACL 档与较旧的 Landlock ABI 是当前的部分强制执行情形,需要绝对边界的消费方可以拒绝或向上暴露它们。
|
||||
@@ -100,7 +100,7 @@ kind: "package-reference"
|
||||
|
||||
### 可写根目录
|
||||
|
||||
`workspaceWritableRoots` 为 bwrap 和 Landlock 推导工作区与显式额外目录。`writableRoots` 为 Seatbelt 和进程内文件系统围栏加入主机临时区域。两者都对根目录去重,并拒绝不存在、不是目录或经过符号链接解析的额外路径。参见[模式定义](../../../docs/subsystems/sandbox.zh.md)。
|
||||
`workspaceWritableRoots` 为 bwrap 和 Landlock 推导工作区与显式额外目录。`writableRoots` 为 Seatbelt 和进程内文件系统围栏加入主机临时区域。两者都对根目录去重,并拒绝不存在、不是目录或经过符号链接解析的额外路径。路径比较接受 Windows 上的大小写和分隔符差异。每次执行都会重新检查这些路径。Windows 子进程忽略额外根目录;文件工具仍然执行这些授权。参见[模式定义](../../../docs/subsystems/sandbox.zh.md)。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export { canonicalPath, workspaceWritableRoots, writableRoots, SandboxInvalidRoo
|
||||
/**
|
||||
* File-effect policy for confined processes. `read-only` permits only required
|
||||
* sinks such as `/dev/null`; `workspace-write` also permits the workspace,
|
||||
* explicit extra roots, and backend-defined temp areas; `danger-full-access` bypasses confinement. Network
|
||||
* explicit extra roots where supported, and backend-defined temp areas; `danger-full-access` bypasses confinement. Network
|
||||
* and process visibility are outside this vocabulary.
|
||||
*/
|
||||
export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
@@ -46,6 +46,7 @@ export interface SandboxExecutionPolicy {
|
||||
* Extra physical absolute directories `workspace-write` may also write under.
|
||||
* Absence means none. The enforcing host rejects missing, non-directory, and
|
||||
* symlinked entries on each call. Remote paths are checked on the remote host.
|
||||
* Windows process confinement ignores these; harness filesystem operations honor them.
|
||||
* These grant nothing under `read-only` or `danger-full-access`.
|
||||
*/
|
||||
additionalWritableRoots?: readonly string[]
|
||||
@@ -151,14 +152,6 @@ export class SandboxUnavailableError extends HarnessError {
|
||||
}
|
||||
}
|
||||
|
||||
/** The selected backend is available but cannot enforce the requested policy. */
|
||||
export class SandboxUnsupportedPolicyError extends HarnessError {
|
||||
constructor(detail: string) {
|
||||
super(detail, 'SANDBOX_UNSUPPORTED_POLICY')
|
||||
this.name = 'SandboxUnsupportedPolicyError'
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
sandbox: SandboxProvider
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import { realpathSync, statSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { resolve } from 'node:path'
|
||||
import { relative } from 'node:path'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { SandboxExecutionPolicy } from './index.ts'
|
||||
|
||||
@@ -27,7 +27,7 @@ export class SandboxInvalidRootError extends HarnessError {
|
||||
}
|
||||
}
|
||||
|
||||
/** Reject mutable path aliases before they can redirect an explicit grant. */
|
||||
/** Resolve existing directories, rejecting aliases that differ under the host's path comparison. */
|
||||
function additionalRoot(path: string): string {
|
||||
let physical: string
|
||||
let directory: boolean
|
||||
@@ -38,7 +38,7 @@ function additionalRoot(path: string): string {
|
||||
throw new SandboxInvalidRootError(path, `cannot read the directory: ${String(error)}`)
|
||||
}
|
||||
if (!directory) throw new SandboxInvalidRootError(path, 'expected an existing directory')
|
||||
if (physical !== resolve(path)) {
|
||||
if (relative(path, physical) !== '') {
|
||||
throw new SandboxInvalidRootError(path, `use the physical directory path ${JSON.stringify(physical)}; symlinked grants are not supported`)
|
||||
}
|
||||
return physical
|
||||
@@ -70,18 +70,19 @@ export function canonicalPath(path: string): string {
|
||||
/**
|
||||
* The workspace-owned writable roots — the policy's workspace root plus its
|
||||
* explicit extra directories, as a canonical, deduplicated allow-list. Both
|
||||
* the workspace and every extra root must be granted together by every
|
||||
* backend; this is the one derivation of that set, kept separate from the
|
||||
* platform temp policy each backend documents for itself (Seatbelt and the fs
|
||||
* the workspace and every extra root are included by the filesystem fence and
|
||||
* POSIX process backends; Windows process confinement ignores extra roots.
|
||||
* This derivation is separate from each backend's platform temp policy (Seatbelt and the fs
|
||||
* fence use {@link writableRoots}; bwrap and Landlock add their own temp
|
||||
* grants).
|
||||
* @param policy - the file-effect policy to derive the allow-list from.
|
||||
* @returns the canonical workspace-owned roots; empty under `read-only`.
|
||||
* @throws {SandboxInvalidRootError} An extra root is missing, is not a directory, or uses a symlink.
|
||||
* @throws {SandboxInvalidRootError} An extra root cannot be inspected, is not a directory, or uses a symlink.
|
||||
*/
|
||||
export function workspaceWritableRoots(policy: SandboxExecutionPolicy): string[] {
|
||||
if (policy.mode !== 'workspace-write') return []
|
||||
return [...new Set([canonicalPath(policy.workspaceRoot), ...(policy.additionalWritableRoots ?? []).map(additionalRoot)])]
|
||||
const additional = (policy.additionalWritableRoots ?? []).map(additionalRoot)
|
||||
return [...new Set([canonicalPath(policy.workspaceRoot), ...additional])]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* additional set stays one shared derivation across backend dialects.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { canonicalPath, workspaceWritableRoots, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
||||
|
||||
/** Every temp root created by this file, removed after each test. */
|
||||
@@ -52,15 +52,45 @@ describe('workspaceWritableRoots', () => {
|
||||
.toThrow('use the physical directory path')
|
||||
})
|
||||
|
||||
it('rejects missing and non-directory extra roots before granting access', () => {
|
||||
it.skipIf(process.platform !== 'win32')('accepts Windows casing and separator differences and retains the canonical directory identity', () => {
|
||||
const ws = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-roots-')))
|
||||
roots.push(ws)
|
||||
const extra = join(ws, 'Memory')
|
||||
mkdirSync(extra)
|
||||
const alias = join(ws, 'memory').replaceAll('\\', '/')
|
||||
const physical = realpathSync.native(extra)
|
||||
expect(alias).not.toBe(physical)
|
||||
const grants = workspaceWritableRoots({
|
||||
mode: 'workspace-write', workspaceRoot: ws, additionalWritableRoots: [alias, extra],
|
||||
})
|
||||
expect(grants).toEqual([ws, physical])
|
||||
})
|
||||
|
||||
it('rejects invalid extra roots and checks them again on each call', () => {
|
||||
const ws = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-roots-')))
|
||||
roots.push(ws)
|
||||
const file = join(ws, 'file.txt')
|
||||
const extra = join(ws, 'extra')
|
||||
writeFileSync(file, 'file')
|
||||
expect(() => workspaceWritableRoots({ mode: 'workspace-write', workspaceRoot: ws, additionalWritableRoots: [file] }))
|
||||
.toThrow('expected an existing directory')
|
||||
expect(() => workspaceWritableRoots({ mode: 'workspace-write', workspaceRoot: ws, additionalWritableRoots: [join(ws, 'missing')] }))
|
||||
.toThrow('cannot read the directory')
|
||||
for (const path of [file, join(file, 'child'), extra]) {
|
||||
expect(() => workspaceWritableRoots({ mode: 'workspace-write', workspaceRoot: ws, additionalWritableRoots: [path] }))
|
||||
.toThrow('Invalid additional writable directory')
|
||||
}
|
||||
const policy = { mode: 'workspace-write' as const, workspaceRoot: ws, additionalWritableRoots: [extra] }
|
||||
mkdirSync(extra)
|
||||
expect(workspaceWritableRoots(policy)).toEqual([ws, extra])
|
||||
rmSync(extra, { recursive: true })
|
||||
expect(() => workspaceWritableRoots(policy)).toThrow('cannot read the directory')
|
||||
})
|
||||
|
||||
it('reports inspection failures that do not establish a missing or non-directory path', () => {
|
||||
const inspect = vi.spyOn(realpathSync, 'native').mockImplementationOnce(() => {
|
||||
throw Object.assign(new Error('access denied'), { code: 'EACCES' })
|
||||
})
|
||||
try {
|
||||
expect(() => workspaceWritableRoots({ mode: 'workspace-write', workspaceRoot: '/workspace', additionalWritableRoots: ['/private'] }))
|
||||
.toThrow('cannot read the directory')
|
||||
} finally { inspect.mockRestore() }
|
||||
})
|
||||
|
||||
it('read-only grants nothing even when extra roots are carried on the policy', () => {
|
||||
|
||||
@@ -7,17 +7,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError, SandboxUnsupportedPolicyError } from '@deepseek-ai/dsh-sandbox'
|
||||
|
||||
describe('SandboxUnsupportedPolicyError', () => {
|
||||
it('distinguishes an unsupported policy from an unavailable backend', () => {
|
||||
const error = new SandboxUnsupportedPolicyError('The selected backend cannot grant additional directories.')
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(error.name).toBe('SandboxUnsupportedPolicyError')
|
||||
expect(error.code).toBe('SANDBOX_UNSUPPORTED_POLICY')
|
||||
expect(error.message).toBe('The selected backend cannot grant additional directories.')
|
||||
})
|
||||
})
|
||||
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||
|
||||
describe('SandboxUnavailableError', () => {
|
||||
it('carries the structured { name, code } identity consumers key on', () => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/** Session-owned write-grant changes observed through the real request context. */
|
||||
import { join } from 'node:path'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
||||
|
||||
/** Snapshot fixture plugin name. */
|
||||
export const name = 'session-write-grant'
|
||||
/** The shared policy owns effect-scoped directory registrations. */
|
||||
export const inject = ['sandboxPolicy']
|
||||
|
||||
/**
|
||||
* Grant directories until the first tool result, before the next request.
|
||||
* @param ctx - fixture lifetime and policy service.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
const extra = join(process.cwd(), 'extra')
|
||||
const dispose = ctx.sandboxPolicy.registerWritableRoot(agent.session, extra)
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session === agent.session && event.type === 'tool/result') {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Generated
+3
@@ -12138,6 +12138,9 @@ importers:
|
||||
'@deepseek-ai/dsh-sandbox-policy':
|
||||
specifier: workspace:*
|
||||
version: link:../../sandbox/sandbox-policy
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-session-projection':
|
||||
specifier: workspace:*
|
||||
version: link:../../session/session-projection
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"initial": [
|
||||
{
|
||||
"name": "run_code",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript REPL statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript REPL statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"initial": [
|
||||
{
|
||||
"name": "run_code",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"initial": [
|
||||
{
|
||||
"name": "run_code",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"initial": [
|
||||
{
|
||||
"name": "run_code",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"initial": [
|
||||
{
|
||||
"name": "run_code",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"initial": [
|
||||
{
|
||||
"name": "run_code",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run. The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. Emit output with console.log(...) or a final lossless JSON expression. The final expression is awaited if it is a Promise. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation. If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
config:
|
||||
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
|
||||
workspaceRoot: /tmp
|
||||
additionalWritableRoots: !!js "[process.cwd() + '/extra']"
|
||||
|
||||
- id: headless-runner
|
||||
inject: [headlessStartup]
|
||||
@@ -56,6 +55,8 @@
|
||||
permissionMode: workspace-write
|
||||
|
||||
- insert:
|
||||
- id: session-write-grant
|
||||
name: '../../../packages/test-support/session-snapshot/tests/fixtures/session-write-grant.ts'
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
config:
|
||||
mode: !!js "process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')"
|
||||
workspaceRoot: /tmp
|
||||
additionalWritableRoots: !!js "[process.cwd() + '/extra']"
|
||||
|
||||
- id: headless-runner
|
||||
inject: [headlessStartup]
|
||||
@@ -16,3 +15,7 @@
|
||||
sessionId: !!js ctx.headlessStartup.sessionId
|
||||
json: !!js ctx.headlessStartup.json
|
||||
permissionMode: workspace-write
|
||||
|
||||
- insert:
|
||||
- id: session-write-grant
|
||||
name: '../../../packages/test-support/session-snapshot/tests/fixtures/session-write-grant.ts'
|
||||
@@ -11,15 +11,16 @@
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create session-root.txt in the current directory containing exactly: session root. Then reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Additional writable directories: [\"{{cwd}}/extra\"]. Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Additional writable directories: [\"{{cwd}}/extra\"]. Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Additional writable directories: [\"{{cwd}}/extra\"]. On Windows, these additional grants apply to file tools only, not sandboxed processes. Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Additional writable directories: [\"{{cwd}}/extra\"]. On Windows, these additional grants apply to file tools only, not sandboxed processes. Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Use the write tool (NOT","messageSeqs":[11],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792249808887,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792249808887,"index":0,"dt":[],"id":"call_session_root","name":"write","args":["{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"]},{"type":"chunk","time":1792249808887,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}},{"type":"chunk","time":1792249808887,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792249808887,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793015794532,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793015794532,"index":0,"dt":[],"id":"call_session_root","name":"write","args":["{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"]},{"type":"chunk","time":1793015794532,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}},{"type":"chunk","time":1793015794532,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793015794532,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_session_root"},"toolCallId":"call_session_root","content":[{"type":"text","text":"<path>{{cwd}}/session-root.txt</path>\n<type>file</type>\n<content>\nCreated file\n</content>"}],"isError":false,"id":"{{message:5}}"},"meta":{"operation":"create","path":"{{cwd}}/session-root.txt","diffs":[]}},"sourceEventSeqs":[17],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1792249809020,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792249809020,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1792249809020,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1792249809020,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1792249809020,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"<agent-name>\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1793015794554,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1793015794554,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1793015794554,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1793015794554,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1793015794554,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
Reference in new issue
Block a user