@deepseek-ai/dsh-sandbox-policy 0.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +69 -0
- package/README.zh.md +69 -0
- package/lib/index.js +156 -0
- package/lib/invariant.js +55 -0
- package/lib/types/index.d.ts +84 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/session-mode.d.ts +59 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-policy/README.md
|
|
5
|
+
README.md: b2512790c5cf5b3a06523cf91b50dd5d288b1522
|
|
6
|
+
README.zh.md: 58354adf55a55951cad1ea9adb88aea2457c7f7b
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# dsh-sandbox-policy — the sandbox policy home (`ctx.sandboxPolicy`)
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The single owner of sandbox-policy resolution: the deployment's default [`SandboxMode`](../sandbox/README.md) and fallback root, plus each session's durable mode override and immutable workspace root. Every enforcing capability receives one resolved mode-and-root policy per call; before each request, the model receives the current policy without a separate capability inventory.
|
|
6
|
+
|
|
7
|
+
## Why a shared home
|
|
8
|
+
|
|
9
|
+
Filesystem tools, one-shot bash commands, and terminal sessions may enforce the same mode vocabulary in different combinations. If each resolved its own `mode` + `workspaceRoot`, they could drift into a split world, exactly what [the sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) warns against. Each enforcing backend consumes the complete owner-resolved policy, while the current context describes only what that policy means for any available operation the DSH file sandbox enforces. The [cross-family fs sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) records the shared-policy decision.
|
|
10
|
+
|
|
11
|
+
## Config
|
|
12
|
+
|
|
13
|
+
- `mode` — the deployment default `SandboxMode` (`read-only` / `workspace-write` / `danger-full-access`), validated at load. Default `read-only` (fail-safe).
|
|
14
|
+
- `workspaceRoot` — the fallback directory `workspace-write` may write under for agentless calls or sessions without a cwd. Default `process.cwd()`, resolved to its absolute filesystem identity either way. A normal agent call uses its session header's immutable `cwd` instead.
|
|
15
|
+
|
|
16
|
+
## Surface
|
|
17
|
+
|
|
18
|
+
- `ctx.sandboxPolicy.resolve({ session?, mode? })` — resolves one complete per-call policy. An explicit approved mode outranks the session's last `sandbox/mode` event, which outranks `defaultMode`; the session's immutable `cwd` is canonicalized with filesystem semantics before becoming `workspaceRoot`, otherwise the configured fallback applies. Canonicalization precedes lexical normalization so `symlink/..` agrees with process working-directory resolution.
|
|
19
|
+
- `ctx.sandboxPolicy.defaultMode` / `ctx.sandboxPolicy.workspaceRoot` — the deployment default and fallback root used by `resolve()`.
|
|
20
|
+
- `sandbox:policy` — a request-time cache-safe context contribution derived directly from `resolve({ session })`. It states the mode's capability-neutral file-effect contract and the canonical session workspace under `workspace-write`; tool owners retain operation-specific denial and escalation guidance.
|
|
21
|
+
- `effectiveSandboxMode(events)` — the pure fold of a session's `sandbox/mode` events (the last switch wins, or `undefined`), used inside `resolve()`.
|
|
22
|
+
- `setSandboxMode(session, mode)` — THE write path for a per-session override: appends exactly one `sandbox/mode` event. The switch IS its event; nothing mutates the mode out of band.
|
|
23
|
+
- `SANDBOX_MODES` — every mode, for option advertisement and runtime validation.
|
|
24
|
+
|
|
25
|
+
The optional `./invariant` companion rejects a forged durable `sandbox/mode` event whose value falls outside that closed vocabulary; Session and its companion own the surrounding storage and core execution-enclosure rules. The agent loop logs the assembled full runtime-context snapshot as a sourced `user/message`, so exact policy input remains reconstructable without an in-memory “last told” mirror.
|
|
26
|
+
|
|
27
|
+
## The per-session store
|
|
28
|
+
|
|
29
|
+
A runtime switch is one log-only `sandbox/mode` event on the session it applies to. `effective = explicit grant ?? fold(events) ?? deployment default`, so an override survives restart by replay and two sessions never see each other's state. Workspace identity does not need another event: the immutable `SessionHeader.cwd` recorded at creation is the root for every call in that session. The event stays log-only; before the next request, the owner contributes the current fact to the full runtime-context snapshot.
|
|
30
|
+
|
|
31
|
+
## Model Experience
|
|
32
|
+
|
|
33
|
+
### Current file sandbox policy
|
|
34
|
+
|
|
35
|
+
#### What the model sees
|
|
36
|
+
|
|
37
|
+
One `sandbox:policy` contribution in the current runtime-context snapshot for every agent session. It does not enumerate mounted capabilities. Tool plugins retain operation and escalation guidance, approval policy contributes separately to the same snapshot, and plan guidance remains `dsh-plan-mode`'s system section.
|
|
38
|
+
|
|
39
|
+
##### Read-only
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
##### Workspace-write
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
##### Danger-full-access
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Token effect
|
|
58
|
+
|
|
59
|
+
One concise durable context message on the first request and each effective policy change; unchanged requests add nothing. `workspace-write` carries only the canonical session workspace path; platform-specific temporary paths are summarized without adding host-dependent bytes.
|
|
60
|
+
|
|
61
|
+
#### KV Cache effect
|
|
62
|
+
|
|
63
|
+
The stable system prompt remains byte-identical across mode changes. A changed full context snapshot is appended after retained history, preserving the prior cached prefix; subsequent unchanged requests reuse that retained snapshot.
|
|
64
|
+
|
|
65
|
+
## Known Limitations and Deferred Work
|
|
66
|
+
|
|
67
|
+
- **One primary workspace root per session** — policy resolves `SessionHeader.cwd`; extra writable roots are not part of `SandboxExecutionPolicy`.
|
|
68
|
+
- **File-effect modes only** — `SandboxMode` governs file effects; network and process policy are outside its vocabulary, so no knob here restricts them.
|
|
69
|
+
- **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.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# dsh-sandbox-policy:沙箱策略归属位置(`ctx.sandboxPolicy`)
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每项负责强制执行的能力在每次调用时都会收到一项解析完成的模式与根目录策略;模型在每次请求前会收到当前策略,而不会另收一份能力清单。
|
|
6
|
+
|
|
7
|
+
## 为何需要共享归属位置
|
|
8
|
+
|
|
9
|
+
文件系统工具、一次性 bash 命令和终端会话可以用不同组合强制执行同一套模式词汇。如果各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界,正是[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。每个强制执行后端都会消费归属方解析出的完整策略,而当前上下文只说明该策略对于任何受 DSH 文件沙箱强制执行的可用操作有何含义。[跨家族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了共享策略决策。
|
|
10
|
+
|
|
11
|
+
## 配置
|
|
12
|
+
|
|
13
|
+
- `mode`:部署默认 `SandboxMode`(`read-only`/`workspace-write`/`danger-full-access`),加载时验证。默认为 `read-only`(故障安全)。
|
|
14
|
+
- `workspaceRoot`:无 agent(智能体)的调用或没有 cwd 的会话在 `workspace-write` 下可写入的回退目录。默认为 `process.cwd()`;无论显式配置还是采用默认值,都会解析为其绝对文件系统标识。普通 agent 调用改用其会话头中不可变的 `cwd`。
|
|
15
|
+
|
|
16
|
+
## 接口
|
|
17
|
+
|
|
18
|
+
- `ctx.sandboxPolicy.resolve({ session?, mode? })`:解析一项完整的逐调用策略。显式批准的模式优先于会话最后一条 `sandbox/mode` 事件,后者又优先于 `defaultMode`;会话不可变的 `cwd` 会先按文件系统语义规范化,再成为 `workspaceRoot`,否则使用配置的回退值。规范化先于词法归一化,因此 `symlink/..` 与进程工作目录解析保持一致。
|
|
19
|
+
- `ctx.sandboxPolicy.defaultMode`/`ctx.sandboxPolicy.workspaceRoot`:`resolve()` 使用的部署默认值与回退根目录。
|
|
20
|
+
- `sandbox:policy`:直接派生自 `resolve({ session })` 的请求时缓存安全上下文贡献。它说明该模式中与具体能力无关的文件操作约定,以及 `workspace-write` 下规范化的会话工作区;工具归属方仍负责特定于操作的拒绝与升权引导。
|
|
21
|
+
- `effectiveSandboxMode(events)`:会话 `sandbox/mode` 事件的纯 fold(最后一次切换胜出,没有则为 `undefined`),在 `resolve()` 内使用。
|
|
22
|
+
- `setSandboxMode(session, mode)`:逐会话覆盖的唯一写入路径:恰好追加一条 `sandbox/mode` 事件。切换本身就是事件;不会在带外修改模式。
|
|
23
|
+
- `SANDBOX_MODES`:所有模式,用于选项展示与运行时验证。
|
|
24
|
+
|
|
25
|
+
可选的 `./invariant` 配套组件会拒绝伪造的持久 `sandbox/mode` 事件,只要其值不在该封闭词汇中;Session 与其配套组件负责相关存储与核心执行封闭规则。agent loop(智能体循环)会将组装后的完整运行时上下文快照记录为一条带来源的 `user/message`,因此无需内存中的「上次告知」镜像,也能重建确切的策略输入。
|
|
26
|
+
|
|
27
|
+
## 逐会话存储
|
|
28
|
+
|
|
29
|
+
运行时切换是在对应会话日志中追加的一条 `sandbox/mode` 事件。`effective = explicit grant ?? fold(events) ?? deployment default`,因此覆盖会通过回放跨重启保留,两个会话也绝不会看到彼此状态。工作区标识无需另一条事件:创建时记录的不可变 `SessionHeader.cwd` 是该会话每次调用使用的根。该事件仍只进入日志;在下一次请求前,归属方会将当前事实贡献给完整运行时上下文快照。
|
|
30
|
+
|
|
31
|
+
## 模型体验
|
|
32
|
+
|
|
33
|
+
### 当前文件沙箱策略
|
|
34
|
+
|
|
35
|
+
#### 模型看到的内容
|
|
36
|
+
|
|
37
|
+
每个 agent 会话的当前运行时上下文快照中都有一项 `sandbox:policy` 贡献。它不枚举已装载的能力。工具插件继续负责操作与升权引导,批准策略单独贡献给同一份快照,计划引导仍由 `dsh-plan-mode` 的系统段落管理。
|
|
38
|
+
|
|
39
|
+
##### 只读
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
##### 工作区写入
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspace root>". Some platform temporary areas may also be writable.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
##### 完全访问
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Token 影响
|
|
58
|
+
|
|
59
|
+
首次请求和有效策略每次变化时增加一条简洁的持久上下文消息;未变化的请求不增加内容。`workspace-write` 只携带规范化的会话工作区路径;平台特定的临时路径会以摘要表述,不会加入依赖主机的字节。
|
|
60
|
+
|
|
61
|
+
#### KV Cache 影响
|
|
62
|
+
|
|
63
|
+
模式切换时,稳定的系统提示词仍逐字节相同。变化后的完整上下文快照会追加到保留的历史之后,从而保留此前已缓存的前缀;后续未变化的请求会复用该保留快照。
|
|
64
|
+
|
|
65
|
+
## 已知限制与暂缓事项
|
|
66
|
+
|
|
67
|
+
- **每个会话只有一个主要工作区根目录**:策略解析 `SessionHeader.cwd`;额外可写根目录不属于 `SandboxExecutionPolicy`。
|
|
68
|
+
- **仅限文件操作模式**:`SandboxMode` 管控文件操作;网络和进程策略不在其词汇中,因此这里没有限制它们的旋钮。
|
|
69
|
+
- **有意概述临时区域**:强制执行后端会授予不同的平台临时区域,这些区域在策略解析后才会选定,因此无法在当前上下文中如实枚举。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
|
|
5
|
+
//#region lib/types/session-mode.js
|
|
6
|
+
/**
|
|
7
|
+
* Per-session sandbox-mode override: the session log as the store. A runtime
|
|
8
|
+
* switch (a UI policy control or test scenario) is recorded as one
|
|
9
|
+
* `sandbox/mode` event on the session it applies to;
|
|
10
|
+
* `effective = fold(events) ?? the deployment default`, so an override
|
|
11
|
+
* survives restart by replay, two sessions can never see each other's state,
|
|
12
|
+
* and there is no external config store. The event is log-only (the
|
|
13
|
+
* `approval/*` precedent): the policy owner projects the fold into each model
|
|
14
|
+
* request, while enforcing tools report operation-specific boundary markers.
|
|
15
|
+
* EXECUTION honors the same fold through `ctx.sandboxPolicy.resolve()` — it
|
|
16
|
+
* stamps the mode together with the calling session's workspace root onto each
|
|
17
|
+
* capability call, weakest-precedence beneath an escalation grant.
|
|
18
|
+
*
|
|
19
|
+
* The override is policy state shared by every enforcing family (bash and
|
|
20
|
+
* filesystem alike), so it lives here in the policy package rather than in any
|
|
21
|
+
* one capability's seam.
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-sandbox-policy/session-mode
|
|
24
|
+
*/
|
|
25
|
+
/** Every {@link SandboxMode}, for option advertisement and runtime validation of untrusted mode strings. */
|
|
26
|
+
const SANDBOX_MODES = [
|
|
27
|
+
"read-only",
|
|
28
|
+
"workspace-write",
|
|
29
|
+
"danger-full-access"
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* The session's sandbox-mode override: the last `sandbox/mode` event in the
|
|
33
|
+
* log, or undefined when the session never switched (callers apply the
|
|
34
|
+
* deployment default). The pure fold — resume needs no catch-up machinery
|
|
35
|
+
* because replaying the log IS the state.
|
|
36
|
+
* @param events - session events in log order (other event types are skipped).
|
|
37
|
+
* @returns the mode of the last switch event, or undefined without one.
|
|
38
|
+
*/
|
|
39
|
+
function effectiveSandboxMode(events) {
|
|
40
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
const event = events[index];
|
|
42
|
+
if (event.type === "sandbox/mode") return event.data.mode;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* THE write path for a session's sandbox-mode override: appends exactly one
|
|
47
|
+
* `sandbox/mode` event — the switch IS its event; nothing mutates mode state
|
|
48
|
+
* out of band. Takes effect on the session's next confined call (bash or fs)
|
|
49
|
+
* — the consumers fold on every read.
|
|
50
|
+
* @param session - the session the override belongs to.
|
|
51
|
+
* @param mode - the mode every subsequent confined call in this session runs
|
|
52
|
+
* under (until the next switch).
|
|
53
|
+
*/
|
|
54
|
+
function setSandboxMode(session, mode) {
|
|
55
|
+
session.append("sandbox/mode", { mode });
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region lib/types/index.js
|
|
59
|
+
/**
|
|
60
|
+
* The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the
|
|
61
|
+
* deployment's sandbox fallbacks plus per-session resolution: the file-effect
|
|
62
|
+
* {@link SandboxMode}, the `workspace-write` root, and the override kit (the
|
|
63
|
+
* `sandbox/mode` event, its fold, and its write path, from `./session-mode.ts`).
|
|
64
|
+
* Before each agent request, the owner also contributes the resolved policy to
|
|
65
|
+
* the cache-safe runtime-context snapshot. The agent loop logs that snapshot as
|
|
66
|
+
* model history, so replay reconstructs the same mode and root the enforcing
|
|
67
|
+
* consumers resolve without rewriting the stable system prompt.
|
|
68
|
+
*
|
|
69
|
+
* Enforcing filesystem, one-shot bash, and terminal backends read the SAME
|
|
70
|
+
* resolved policy here. The context describes that policy without inventorying
|
|
71
|
+
* capabilities, while each backend retains its own enforcement dialect and each
|
|
72
|
+
* tool owns its operation-specific denial and escalation guidance. The service
|
|
73
|
+
* reads session state once at each operation boundary; executors and providers
|
|
74
|
+
* remain session-free.
|
|
75
|
+
*
|
|
76
|
+
* @module @deepseek-ai/dsh-sandbox-policy
|
|
77
|
+
*/
|
|
78
|
+
/** Resolve filesystem identity before lexical normalization can erase symlink-sensitive components. */
|
|
79
|
+
function resolveWorkspaceRoot(path) {
|
|
80
|
+
return resolve(canonicalPath(path));
|
|
81
|
+
}
|
|
82
|
+
/** Render the policy without claiming which capabilities are mounted. */
|
|
83
|
+
function renderPolicyContext(policy) {
|
|
84
|
+
switch (policy.mode) {
|
|
85
|
+
case "read-only": return "Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.";
|
|
86
|
+
case "workspace-write": return `Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(policy.workspaceRoot)}. Some platform temporary areas may also be writable.`;
|
|
87
|
+
case "danger-full-access": return "Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.";
|
|
88
|
+
/* v8 ignore next 4 -- SandboxMode is a typed same-process closed union; this branch is only the static exhaustiveness guard. */
|
|
89
|
+
default: {
|
|
90
|
+
const mode = policy.mode;
|
|
91
|
+
throw new Error(`unreachable sandbox mode: ${String(mode)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
|
|
97
|
+
* default mode, fallback workspace root, and current request-time policy
|
|
98
|
+
* section. Tool layers call {@link resolve} for each execution so a session's
|
|
99
|
+
* mode log and immutable cwd travel together to every enforcing capability.
|
|
100
|
+
*/
|
|
101
|
+
var SandboxPolicyService = class extends Service {
|
|
102
|
+
static Config = z.object({
|
|
103
|
+
mode: z.union([
|
|
104
|
+
"read-only",
|
|
105
|
+
"workspace-write",
|
|
106
|
+
"danger-full-access"
|
|
107
|
+
]).default("read-only"),
|
|
108
|
+
workspaceRoot: z.string()
|
|
109
|
+
});
|
|
110
|
+
/** The deployment default mode — the fallback beneath a session override. */
|
|
111
|
+
defaultMode;
|
|
112
|
+
/** The absolute `workspace-write` fallback root for calls without a session cwd. */
|
|
113
|
+
workspaceRoot;
|
|
114
|
+
constructor(ctx, config) {
|
|
115
|
+
super(ctx, "sandboxPolicy");
|
|
116
|
+
this.defaultMode = config.mode;
|
|
117
|
+
this.workspaceRoot = resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd());
|
|
118
|
+
ctx.inject(["systemPrompt"], (scope) => {
|
|
119
|
+
scope.systemPrompt.context({
|
|
120
|
+
name: "sandbox:policy",
|
|
121
|
+
order: 110,
|
|
122
|
+
text: (context) => {
|
|
123
|
+
const session = context.agent?.session;
|
|
124
|
+
return session === void 0 ? "" : renderPolicyContext(this.resolve({ session }));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the complete policy for one capability call. An approved explicit
|
|
131
|
+
* mode outranks the session's last `sandbox/mode` event, which outranks the
|
|
132
|
+
* deployment default. A session cwd is its workspace-write boundary; the
|
|
133
|
+
* configured root is the fallback for agentless calls and sessions without a
|
|
134
|
+
* cwd.
|
|
135
|
+
* @param request - optional session and approved mode override.
|
|
136
|
+
* @returns the fully resolved per-call mode and absolute workspace root.
|
|
137
|
+
*/
|
|
138
|
+
resolve(request = {}) {
|
|
139
|
+
const { session } = request;
|
|
140
|
+
return {
|
|
141
|
+
mode: request.mode ?? (session === void 0 ? void 0 : this.overrideOf(session)) ?? this.defaultMode,
|
|
142
|
+
workspaceRoot: resolveWorkspaceRoot(session?.header.cwd ?? this.workspaceRoot),
|
|
143
|
+
...session === void 0 ? {} : { sessionId: session.id }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read the session override without applying the deployment default.
|
|
148
|
+
* @param session - session whose log supplies the override.
|
|
149
|
+
* @returns the last logged mode, or `undefined` without one.
|
|
150
|
+
*/
|
|
151
|
+
overrideOf(session) {
|
|
152
|
+
return effectiveSandboxMode(session.events);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
156
|
+
export { SANDBOX_MODES, SandboxPolicyService, SandboxPolicyService as default, effectiveSandboxMode, setSandboxMode };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region lib/types/session-mode.js
|
|
2
|
+
/**
|
|
3
|
+
* Per-session sandbox-mode override: the session log as the store. A runtime
|
|
4
|
+
* switch (a UI policy control or test scenario) is recorded as one
|
|
5
|
+
* `sandbox/mode` event on the session it applies to;
|
|
6
|
+
* `effective = fold(events) ?? the deployment default`, so an override
|
|
7
|
+
* survives restart by replay, two sessions can never see each other's state,
|
|
8
|
+
* and there is no external config store. The event is log-only (the
|
|
9
|
+
* `approval/*` precedent): the policy owner projects the fold into each model
|
|
10
|
+
* request, while enforcing tools report operation-specific boundary markers.
|
|
11
|
+
* EXECUTION honors the same fold through `ctx.sandboxPolicy.resolve()` — it
|
|
12
|
+
* stamps the mode together with the calling session's workspace root onto each
|
|
13
|
+
* capability call, weakest-precedence beneath an escalation grant.
|
|
14
|
+
*
|
|
15
|
+
* The override is policy state shared by every enforcing family (bash and
|
|
16
|
+
* filesystem alike), so it lives here in the policy package rather than in any
|
|
17
|
+
* one capability's seam.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-sandbox-policy/session-mode
|
|
20
|
+
*/
|
|
21
|
+
/** Every {@link SandboxMode}, for option advertisement and runtime validation of untrusted mode strings. */
|
|
22
|
+
const SANDBOX_MODES = [
|
|
23
|
+
"read-only",
|
|
24
|
+
"workspace-write",
|
|
25
|
+
"danger-full-access"
|
|
26
|
+
];
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region lib/types/invariant.js
|
|
29
|
+
/** Package-owned session-event invariants for sandbox policy. @module @deepseek-ai/dsh-sandbox-policy/invariant */
|
|
30
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-sandbox-policy";
|
|
31
|
+
/** Cordis companion plugin name. */
|
|
32
|
+
const name = "sandbox-policy-invariant";
|
|
33
|
+
/** Service required before the companion can reserve package ownership. */
|
|
34
|
+
const inject = ["invariants"];
|
|
35
|
+
/** Validate the package-owned event fields and ignore unrelated events. */
|
|
36
|
+
function validateEvent(event, fail) {
|
|
37
|
+
if (event.type === "sandbox/mode" && !SANDBOX_MODES.includes(event.data.mode)) fail(`sandbox/mode carries unknown mode ${JSON.stringify(event.data.mode)}`);
|
|
38
|
+
}
|
|
39
|
+
/** Install validation for loaded and newly appended sandbox modes. */
|
|
40
|
+
const install = Object.assign((ctx, fail) => {
|
|
41
|
+
for (const session of ctx.sessions.list()) for (const event of session.events) validateEvent(event, fail);
|
|
42
|
+
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
43
|
+
if (eventName !== "session/event") return;
|
|
44
|
+
const event = args[1];
|
|
45
|
+
validateEvent(event, fail);
|
|
46
|
+
}, { global: true });
|
|
47
|
+
}, { inject: ["sessions"] });
|
|
48
|
+
/**
|
|
49
|
+
* Register this package's invariant companion.
|
|
50
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
51
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
52
|
+
*/
|
|
53
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
54
|
+
//#endregion
|
|
55
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the
|
|
3
|
+
* deployment's sandbox fallbacks plus per-session resolution: the file-effect
|
|
4
|
+
* {@link SandboxMode}, the `workspace-write` root, and the override kit (the
|
|
5
|
+
* `sandbox/mode` event, its fold, and its write path, from `./session-mode.ts`).
|
|
6
|
+
* Before each agent request, the owner also contributes the resolved policy to
|
|
7
|
+
* the cache-safe runtime-context snapshot. The agent loop logs that snapshot as
|
|
8
|
+
* model history, so replay reconstructs the same mode and root the enforcing
|
|
9
|
+
* consumers resolve without rewriting the stable system prompt.
|
|
10
|
+
*
|
|
11
|
+
* Enforcing filesystem, one-shot bash, and terminal backends read the SAME
|
|
12
|
+
* resolved policy here. The context describes that policy without inventorying
|
|
13
|
+
* capabilities, while each backend retains its own enforcement dialect and each
|
|
14
|
+
* tool owns its operation-specific denial and escalation guidance. The service
|
|
15
|
+
* reads session state once at each operation boundary; executors and providers
|
|
16
|
+
* remain session-free.
|
|
17
|
+
*
|
|
18
|
+
* @module @deepseek-ai/dsh-sandbox-policy
|
|
19
|
+
*/
|
|
20
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
21
|
+
import z from '@deepseek-ai/schemastery';
|
|
22
|
+
import { type SandboxExecutionPolicy, type SandboxMode } from '@deepseek-ai/dsh-sandbox';
|
|
23
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
24
|
+
export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts';
|
|
25
|
+
declare module '@deepseek-ai/cordis' {
|
|
26
|
+
interface Context {
|
|
27
|
+
sandboxPolicy: SandboxPolicyService;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Plugin config: the deployment's sandbox default. All optional — `Config`
|
|
32
|
+
* supplies the defaults (`mode: 'read-only'` is the fail-safe default; a
|
|
33
|
+
* deployment that wants a workspace-writable agent opts in explicitly). The
|
|
34
|
+
* runner choice is NOT here (it is the `ctx.sandbox` provider's config), nor
|
|
35
|
+
* is any per-family knob: this is the one shared policy home.
|
|
36
|
+
*/
|
|
37
|
+
export interface Config {
|
|
38
|
+
/** File-sandbox mode a session starts from (default: `read-only`). */
|
|
39
|
+
mode?: SandboxMode;
|
|
40
|
+
/**
|
|
41
|
+
* Fallback root for agentless calls and sessions without a cwd (default:
|
|
42
|
+
* `process.cwd()`). Normal agent calls use their session cwd instead.
|
|
43
|
+
*/
|
|
44
|
+
workspaceRoot?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Inputs that select the sandbox policy for one capability call. */
|
|
47
|
+
export interface SandboxPolicyRequest {
|
|
48
|
+
/** Calling session; its immutable cwd becomes the workspace boundary. */
|
|
49
|
+
session?: Session;
|
|
50
|
+
/** Explicit approved mode override, which outranks session policy. */
|
|
51
|
+
mode?: SandboxMode;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
|
|
55
|
+
* default mode, fallback workspace root, and current request-time policy
|
|
56
|
+
* section. Tool layers call {@link resolve} for each execution so a session's
|
|
57
|
+
* mode log and immutable cwd travel together to every enforcing capability.
|
|
58
|
+
*/
|
|
59
|
+
export declare class SandboxPolicyService extends Service {
|
|
60
|
+
static Config: z<Config>;
|
|
61
|
+
/** The deployment default mode — the fallback beneath a session override. */
|
|
62
|
+
readonly defaultMode: SandboxMode;
|
|
63
|
+
/** The absolute `workspace-write` fallback root for calls without a session cwd. */
|
|
64
|
+
readonly workspaceRoot: string;
|
|
65
|
+
constructor(ctx: Context, config: Config);
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the complete policy for one capability call. An approved explicit
|
|
68
|
+
* mode outranks the session's last `sandbox/mode` event, which outranks the
|
|
69
|
+
* deployment default. A session cwd is its workspace-write boundary; the
|
|
70
|
+
* configured root is the fallback for agentless calls and sessions without a
|
|
71
|
+
* cwd.
|
|
72
|
+
* @param request - optional session and approved mode override.
|
|
73
|
+
* @returns the fully resolved per-call mode and absolute workspace root.
|
|
74
|
+
*/
|
|
75
|
+
resolve(request?: SandboxPolicyRequest): SandboxExecutionPolicy;
|
|
76
|
+
/**
|
|
77
|
+
* Read the session override without applying the deployment default.
|
|
78
|
+
* @param session - session whose log supplies the override.
|
|
79
|
+
* @returns the last logged mode, or `undefined` without one.
|
|
80
|
+
*/
|
|
81
|
+
overrideOf(session: Session): SandboxMode | undefined;
|
|
82
|
+
}
|
|
83
|
+
export default SandboxPolicyService;
|
|
84
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned session-event invariants for sandbox policy. @module @deepseek-ai/dsh-sandbox-policy/invariant */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "sandbox-policy-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register this package's invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session sandbox-mode override: the session log as the store. A runtime
|
|
3
|
+
* switch (a UI policy control or test scenario) is recorded as one
|
|
4
|
+
* `sandbox/mode` event on the session it applies to;
|
|
5
|
+
* `effective = fold(events) ?? the deployment default`, so an override
|
|
6
|
+
* survives restart by replay, two sessions can never see each other's state,
|
|
7
|
+
* and there is no external config store. The event is log-only (the
|
|
8
|
+
* `approval/*` precedent): the policy owner projects the fold into each model
|
|
9
|
+
* request, while enforcing tools report operation-specific boundary markers.
|
|
10
|
+
* EXECUTION honors the same fold through `ctx.sandboxPolicy.resolve()` — it
|
|
11
|
+
* stamps the mode together with the calling session's workspace root onto each
|
|
12
|
+
* capability call, weakest-precedence beneath an escalation grant.
|
|
13
|
+
*
|
|
14
|
+
* The override is policy state shared by every enforcing family (bash and
|
|
15
|
+
* filesystem alike), so it lives here in the policy package rather than in any
|
|
16
|
+
* one capability's seam.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-sandbox-policy/session-mode
|
|
19
|
+
*/
|
|
20
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
21
|
+
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
|
|
22
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
23
|
+
interface SessionEventMap {
|
|
24
|
+
/**
|
|
25
|
+
* The session's sandbox mode was switched — log-only (like `approval/*`;
|
|
26
|
+
* NOT a surface event, carries no `surfaceOp`): durable and replayable,
|
|
27
|
+
* never in the model transcript. The LAST such event is the session's
|
|
28
|
+
* override ({@link effectiveSandboxMode}). `source: 'delegation'` marks
|
|
29
|
+
* an override seeded into a child; an absent source is a runtime switch.
|
|
30
|
+
*/
|
|
31
|
+
'sandbox/mode': {
|
|
32
|
+
mode: SandboxMode;
|
|
33
|
+
/** Marks an override seeded into a child at delegation. */
|
|
34
|
+
source?: 'delegation';
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Every {@link SandboxMode}, for option advertisement and runtime validation of untrusted mode strings. */
|
|
39
|
+
export declare const SANDBOX_MODES: readonly SandboxMode[];
|
|
40
|
+
/**
|
|
41
|
+
* The session's sandbox-mode override: the last `sandbox/mode` event in the
|
|
42
|
+
* log, or undefined when the session never switched (callers apply the
|
|
43
|
+
* deployment default). The pure fold — resume needs no catch-up machinery
|
|
44
|
+
* because replaying the log IS the state.
|
|
45
|
+
* @param events - session events in log order (other event types are skipped).
|
|
46
|
+
* @returns the mode of the last switch event, or undefined without one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMode | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* THE write path for a session's sandbox-mode override: appends exactly one
|
|
51
|
+
* `sandbox/mode` event — the switch IS its event; nothing mutates mode state
|
|
52
|
+
* out of band. Takes effect on the session's next confined call (bash or fs)
|
|
53
|
+
* — the consumers fold on every read.
|
|
54
|
+
* @param session - the session the override belongs to.
|
|
55
|
+
* @param mode - the mode every subsequent confined call in this session runs
|
|
56
|
+
* under (until the next switch).
|
|
57
|
+
*/
|
|
58
|
+
export declare function setSandboxMode(session: Session, mode: SandboxMode): void;
|
|
59
|
+
//# sourceMappingURL=session-mode.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-sandbox-policy",
|
|
3
|
+
"description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/sandbox/sandbox-policy"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
37
|
+
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.1",
|
|
38
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
39
|
+
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
52
|
+
}
|
|
53
|
+
}
|