@xth26/dsh-plan-build-mode 0.4.0 → 0.5.0

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/README.zh.md CHANGED
@@ -1,143 +1,237 @@
1
- # dsh-plan-build-mode
2
-
3
- OpenCode 风格的 Plan / Build 硬权限模型插件。
4
-
5
- 本插件为 DSH 增加一个独立的 Plan/Build 模式切换,不影响 DSH 原生的 `/plan` 软提示模式:
6
-
7
- - **Plan 模式**:强制 `read-only` 沙箱,并拦截写工具(`write`、`edit`)。
8
- - **Build 模式**:恢复 `workspace-write`,允许编辑。
9
-
10
- 写工具拦截仅在 Plan 模式配置为 `read-only` 时生效。首次使用或未切换时,默认处于 Build 模式。
11
-
12
- ## 版本兼容性
13
-
14
- | 插件版本 | DSH 版本 | 说明 |
15
- |---|---|---|
16
- | `0.4.x` | `>= 0.1.5-rc.1` | 使用 `session.snapshotEvents()`;DSH 0.1.5 移除了 `session.events` 快照 getter。 |
17
- | `<= 0.3.1` | `0.1.0-rc.x` | 旧版本;在 DSH 0.1.5+ 上因上述 API 变更无法工作。 |
18
-
19
- ## 安装
20
-
21
- DSH 插件通过 DSH profile 加载。把它装到你实际使用的 profile 里:
22
-
23
- ```bash
24
- # 装到 web profile
25
- dsh plugin --profile web add @xth26/dsh-plan-build-mode
26
-
27
- # 装到 TUI profile
28
- dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode
29
- ```
30
-
31
- 插件自带的 `cordis.patch.yml` 会在 DSH profile 加载时自动注入。
32
-
33
- ## Web 和 TUI 共享同一个 Plan/Build 模式
34
-
35
- Plan/Build 模式状态存在会话事件日志里(`sandbox/mode` 事件)。`dsh web` 和 `dsh --profile <名称>` 只有在共用同一个 profile、同一个 session ID 时,才会读写同一个会话事件流。
36
-
37
- - **同一个 profile**:比如 `dsh web --profile web` `dsh --profile web` 启动的两个入口,会共享 session 事件,因此模式切换会同步生效。
38
- - **不同 profile**:默认 `web` 和 `dsh-tui` 是两个独立 profile,会话目录也分开。在一处切换不会影响另一处。
39
-
40
- 想让 web TUI 共享模式状态,最简单的方法是**两个入口都使用同一个 profile**:
41
-
42
- ```bash
43
- # 两个入口都用 web profile
44
- dsh web --profile web
45
- dsh --profile web
46
- ```
47
-
48
- 如果你确实想用不同 profile,但只想共享会话存储,可以在每个 profile `cordis.patch.yml` 里把 session root 指向同一个目录:
49
-
50
- ```yaml
51
- - id: session-root
52
- config:
53
- root: /path/to/shared/sessions
54
- ```
55
-
56
- 注意:跨 profile 共享会话目录要求两个 profile 挂载的服务组合兼容,否则事件解释可能出现差异。
57
-
58
- ## 本地开发 / link 试用
59
-
60
- 想在发布前从本地源码试用:
61
-
62
- ```bash
63
- # 在插件目录
64
- cd /path/to/dsh-plan-build-mode
65
- pnpm link --global
66
-
67
- # 在你的 DSH profile 目录
68
- pnpm link --global @xth26/dsh-plan-build-mode
69
- ```
70
-
71
- 然后启动 DSH。插件 `package.json` 里的 `dsh.bundle.patch` 会自动生效。
72
-
73
- ## 更新
74
-
75
- 对于 `0.x` 版本,semver 范围 `^0.1.0` 只会匹配 `0.1.x`,**永远不会**自动更新到 `0.2.0`。升级时必须显式提升声明并重启 DSH 进程:
76
-
77
- ```bash
78
- # 在目标 profile 中提升已安装版本
79
- # (这条命令需要在非沙箱的终端里执行;DSH agent 无法写入 profile 目录)
80
- dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode@0.4.0
81
-
82
- # 重启使用该 profile DSH 进程
83
- # (旧的 Node 进程仍缓存着旧插件代码)
84
- Stop-Process -Name dsh -Force # PowerShell
85
- # 或: taskkill /IM dsh.exe /F # CMD
86
- ```
87
-
88
- 然后重新启动 `dsh web` 或 `dsh tui`。如果想始终安装最新发布版本:
89
-
90
- ```bash
91
- dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode@latest
92
- ```
93
-
94
- ## 使用
95
-
96
- | 命令 | 效果 |
97
- |---|---|
98
- | `/plan-build` | 在 Plan 与 Build 模式之间切换 |
99
- | `/plan-build status` | 显示当前模式 |
100
- | `/plan-build switch plan` | 进入 Plan 模式(只读) |
101
- | `/plan-build switch build` | 进入 Build 模式(可写) |
102
- | `/plan-build switch` | `/plan-build`,切换当前模式 |
103
-
104
- ## 配置
105
-
106
- ```yaml
107
- - id: plan-build-mode
108
- name: '@xth26/dsh-plan-build-mode'
109
- config:
110
- planSandbox: read-only
111
- buildSandbox: workspace-write
112
- denyWriteTools: true
113
- section: true
114
- ```
115
-
116
- `planSandbox` `buildSandbox` 必须不同。
117
-
118
- ## Plan 模式:命令行 Python 做数据检查
119
-
120
- Plan 模式只读并拦截 `write`/`edit` 工具,但**鼓励**在命令行里跑简短、只读的 Python 片段来读数据和验证假设。例如:
121
-
122
- ```bash
123
- python -B -c "import json, sys; data = json.load(open('data/sample.json')); print(len(data))"
124
- python -B -c "import pandas as pd; print(pd.read_csv('data.csv').describe())"
125
- pytest -p no:cacheprovider -q tests/test_sanity.py
126
- ```
127
-
128
- 使用 `-B`(或设置 `PYTHONDONTWRITEBYTECODE=1`)以及 `pytest -p no:cacheprovider`,避免写入 `__pycache__` 或 `.pytest_cache`。read-only 沙箱仍是最终防线,任何试图写文件的命令仍会在沙箱层被拒绝。
129
-
130
- ## 本地集成验证
131
-
132
- 安装或 link 插件到 DSH profile 后:
133
-
134
- 1. 启动 DSH。
135
- 2. 输入 `/plan-build`,应提示进入 Plan 模式。
136
- 3. 让 agent 调用 `write` 工具,应被拒绝。
137
- 4. 再次输入 `/plan-build`,应提示进入 Build 模式。
138
- 5. 再次让 agent 调用 `write`,应允许执行。
139
- 6. 输入 `/plan-build status`,可只查看当前模式而不切换。
140
-
141
- ## 许可证
142
-
143
- MIT
1
+ # dsh-plan-build-mode
2
+
3
+ OpenCode 风格的 Plan / Build 硬权限模型插件(DeepSeek Harness)。
4
+
5
+ 本插件为 DSH 增加一个独立的 Plan/Build 模式切换,并用 OpenCode 风格权限引擎
6
+ (`allow | ask | deny`)对工具访问做门禁,不影响 DSH 原生的 `/plan` 软提示模式:
7
+
8
+ - **Plan 模式按策略只读**:所有写类工具都被拒绝,唯一例外是写入配置的
9
+ plans 目录(`.opencode/plans/`)——这样 agent 在其余位置保持只读时,仍能持久化
10
+ 计划文件。
11
+ - **Build 模式可写**:可以执行已批准的计划。
12
+ - **两种模式下读取都不受限**,包括工作区之外的文件——与 DSH 一致,读取在每种
13
+ 沙箱模式下都放行。只有**修改**会被门禁(对应 OpenCode 的 `external_directory`)。
14
+ - DSH 自带的 `/plan` 软提示模式保持原样。
15
+
16
+ 随附的 `cordis.patch.yml` 把两个沙箱都配置为 `workspace-write`;Plan 模式的
17
+ “只读”由权限门禁而不是沙箱强制——这正是计划文件能写到 `.opencode/plans/`、
18
+ 其余位置保持只读的原因。
19
+
20
+ ## 版本兼容性
21
+
22
+ | 插件版本 | DSH 版本 | 说明 |
23
+ |---|---|---|
24
+ | `0.5.x` | `>= 0.1.5-rc.1` | 新增 OpenCode 权限门禁与 `/permission`;模式由 `plan-build/mode` 会话事件承载(旧会话从 `sandbox/mode` 推断)。 |
25
+ | `0.4.x` | `>= 0.1.5-rc.1` | 使用 `session.snapshotEvents()`;DSH 0.1.5 移除了 `session.events` 快照 getter。 |
26
+ | `<= 0.3.1` | `0.1.0-rc.x` | 旧版本;在 DSH 0.1.5+ 上因上述 API 变更无法工作。 |
27
+
28
+ ## 安装
29
+
30
+ DSH 插件通过 DSH profile 加载。把它装到你实际使用的 profile 里:
31
+
32
+ ```bash
33
+ # 装到 web profile
34
+ dsh plugin --profile web add @xth26/dsh-plan-build-mode
35
+
36
+ # 装到 TUI profile
37
+ dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode
38
+ ```
39
+
40
+ 插件自带的 `cordis.patch.yml` 会在 DSH profile 加载时自动注入。
41
+
42
+ ## 让 Web 和 TUI 共享同一个 Plan/Build 模式
43
+
44
+ Plan/Build 模式状态存在会话事件日志里(插件专属的 `plan-build/mode` 事件,
45
+ 以及用于沙箱执行的 `sandbox/mode` 事件)。`dsh web` 和 `dsh --profile <名称>`
46
+ 只有在共用同一个 profile、同一个 session ID 时,才会读写同一个会话事件流。
47
+
48
+ - **同一个 profile**:比如 `dsh web --profile web` `dsh --profile web` 启动的两个入口,会共享 session 事件,因此模式切换会同步生效。
49
+ - **不同 profile**:默认 `web` 和 `dsh-tui` 是两个独立 profile,会话目录也分开。在一处切换不会影响另一处。
50
+
51
+ 想让 web 和 TUI 共享模式状态,最简单的方法是**两个入口都使用同一个 profile**:
52
+
53
+ ```bash
54
+ # 两个入口都用 web profile
55
+ dsh web --profile web
56
+ dsh --profile web
57
+ ```
58
+
59
+ 如果你确实想用不同 profile,但只想共享会话存储,可以在每个 profile 的 `cordis.patch.yml` 里把 session root 指向同一个目录:
60
+
61
+ ```yaml
62
+ - id: session-root
63
+ config:
64
+ root: /path/to/shared/sessions
65
+ ```
66
+
67
+ 注意:跨 profile 共享会话目录要求两个 profile 挂载的服务组合兼容,否则事件解释可能出现差异。
68
+
69
+ ## 本地开发 / link 试用
70
+
71
+ 想在发布前从本地源码试用:
72
+
73
+ ```bash
74
+ # 在插件目录
75
+ cd /path/to/dsh-plan-build-mode
76
+ pnpm link --global
77
+
78
+ # 在你的 DSH profile 目录
79
+ pnpm link --global @xth26/dsh-plan-build-mode
80
+ ```
81
+
82
+ 然后启动 DSH。插件 `package.json` 里的 `dsh.bundle.patch` 会自动生效。
83
+
84
+ ## 更新
85
+
86
+ 对于 `0.x` 版本,semver 范围 `^0.1.0` 只会匹配 `0.1.x`,**永远不会**自动更新到 `0.2.0`。升级时必须显式提升声明并重启 DSH 进程:
87
+
88
+ ```bash
89
+ # 在目标 profile 中提升已安装版本
90
+ # (这条命令需要在非沙箱的终端里执行;DSH agent 无法写入 profile 目录)
91
+ dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode@0.5.0
92
+
93
+ # 重启使用该 profile 的 DSH 进程
94
+ # (旧的 Node 进程仍缓存着旧插件代码)
95
+ Stop-Process -Name dsh -Force # PowerShell
96
+ # 或: taskkill /IM dsh.exe /F # CMD
97
+ ```
98
+
99
+ 然后重新启动 `dsh web` `dsh tui`。如果想始终安装最新发布版本:
100
+
101
+ ```bash
102
+ dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode@latest
103
+ ```
104
+
105
+ ### 升级说明(0.4.x → 0.5.x)
106
+
107
+ - `planSandbox` 与 `buildSandbox` **现在可以相等**(随附补丁两者都用
108
+ `workspace-write`);Plan 模式的只读策略由权限门禁强制。旧的“必须不同”限制已移除。
109
+ - 模式状态迁移到插件专属的 `plan-build/mode` 事件。只有 `sandbox/mode` 事件的旧
110
+ 会话会在加载时被推断并在会话开始时固定为真实的 `plan-build/mode` 事件,历史不丢失。
111
+ - 旧的 `denyWriteTools` 选项仍然有效:设为 `false` 允许 agent 在 Plan 模式写入
112
+ (等价于覆盖 `modes.plan.edit = allow`)。
113
+
114
+ ## 使用
115
+
116
+ | 命令 | 效果 |
117
+ |---|---|
118
+ | `/plan-build` | 在 Plan Build 模式之间切换 |
119
+ | `/plan-build status` | 显示当前模式 |
120
+ | `/plan-build switch plan` | 进入 Plan 模式(策略只读) |
121
+ | `/plan-build switch build` | 进入 Build 模式(可写) |
122
+ | `/plan-build switch` | 同 `/plan-build`,切换当前模式 |
123
+ | `/permission status` | 显示外部路径默认值、规则与本次会话的 allowlist |
124
+ | `/permission allow <pattern>` | 把模式加入本次会话 allowlist(OpenCode 的 “always”) |
125
+ | `/permission deny <pattern>` | 从本次会话 allowlist 移除该模式 |
126
+
127
+ ## 配置
128
+
129
+ ```yaml
130
+ - id: plan-build-mode
131
+ name: '@xth26/dsh-plan-build-mode'
132
+ config:
133
+ planSandbox: workspace-write
134
+ buildSandbox: workspace-write
135
+ denyWriteTools: true
136
+ section: true
137
+ plansDirectory: .opencode/plans
138
+ externalDirectory:
139
+ default: allow # allow | ask | deny —— 工作区之外的读取
140
+ ```
141
+
142
+ `planSandbox` 和 `buildSandbox` 可以相等,也可以不同。不同时,模式切换还会联动
143
+ 沙箱(`read-only` ↔ `workspace-write` 等);相等时(随附默认),Plan 模式的策略由
144
+ 权限门禁强制。
145
+
146
+ ### 权限模型
147
+
148
+ 每个工具映射到一个类别:
149
+
150
+ | 类别 | 工具 |
151
+ |---|---|
152
+ | `read` | `read`、`read_image`、`pdf_read`、`docx_read`、`pptx_read`、`xlsx_read` |
153
+ | `edit` | `write`、`edit`、`pdf_create`、`docx_create`、`pptx_create`、`xlsx_write`、`xlsx_edit` |
154
+ | `glob` | `glob` |
155
+ | `grep` | `grep` |
156
+ | `bash` | `bash` |
157
+ | `pwsh` | `pwsh` |
158
+ | `webfetch` | `web_fetch` |
159
+ | `websearch` | `web_search` |
160
+
161
+ 类别值可以是标量(`allow | ask | deny`),也可以是带顺序的对象规则,和 OpenCode 一致:
162
+
163
+ ```yaml
164
+ permission:
165
+ read:
166
+ '*': allow
167
+ '**/.env': deny
168
+ '**/.env.*': deny
169
+ '**/.env.example': allow # 最后匹配的规则生效
170
+ bash: ask
171
+ edit: deny
172
+ ```
173
+
174
+ 模式支持 OpenCode 通配符(`*` 匹配任意字符、含 `/`;`?` 恰好匹配一个字符;
175
+ `**` 与 `*` 相同)以及 `~`/`$HOME` 展开。规则按“最后匹配者胜出”求值。默认规则对齐
176
+ OpenCode:大多数类别 `allow`;`.env*` 读取被拒绝(`.env.example` 恢复为允许)。
177
+
178
+ Plan 模式覆盖默认值(除非被 `modes.plan` 或 `denyWriteTools: false` 覆盖):
179
+ `edit: deny`、`bash: ask`、`pwsh: ask`——对应 OpenCode plan agent 的默认值。
180
+ `modes.plan` / `modes.build` 按模式覆盖全局权限,类似 OpenCode 的 agent 级覆盖。
181
+
182
+ ```yaml
183
+ modes:
184
+ plan:
185
+ bash: deny # 替代默认的 ask
186
+ build:
187
+ read: allow
188
+ ```
189
+
190
+ ### Plans 目录例外
191
+
192
+ 在 Plan 模式下,写入 `plansDirectory`(默认 `.opencode/plans/`,相对会话工作区)
193
+ 下的 `edit` 类工具**始终允许**,保证 `/plan-build` 与计划文件正常工作。Plan 模式下
194
+ 其它所有写入都会被拒绝,理由中包含 `forbidden in Plan Mode`。
195
+
196
+ ### 外部路径
197
+
198
+ - **读取**工作区之外的文件在两种模式下默认都允许(`externalDirectory.default:
199
+ allow`)。设为 `ask` 可获得完整的 OpenCode 行为:对外部读取发起审批请求。
200
+ - **写入**工作区之外由沙箱决定。在 `workspace-write` 下无法放行,因此插件不会
201
+ 弹审批(那也会在沙箱层失败),而是直接拒绝并给出升级提示——用
202
+ `sandbox_permissions` + `justification` 重试同一个工具。
203
+ - `/permission allow <pattern>` 会加入一个会话级 allowlist 条目(OpenCode
204
+ “always”),仅在该会话内存中保留。
205
+
206
+ ## Plan 模式:命令行 Python 做数据检查
207
+
208
+ Plan 模式拒绝 `write`/`edit` 工具,但**鼓励**在命令行里跑简短、只读的 Python
209
+ 片段来读数据和验证假设。例如:
210
+
211
+ ```bash
212
+ python -B -c "import json, sys; data = json.load(open('data/sample.json')); print(len(data))"
213
+ python -B -c "import pandas as pd; print(pd.read_csv('data.csv').describe())"
214
+ pytest -p no:cacheprovider -q tests/test_sanity.py
215
+ ```
216
+
217
+ 使用 `-B`(或设置 `PYTHONDONTWRITEBYTECODE=1`)以及 `pytest -p no:cacheprovider`,
218
+ 避免写入 `__pycache__` 或 `.pytest_cache`。权限门禁与沙箱是最终防线:任何试图写文件的
219
+ 操作都会在门禁(Plan 模式)或沙箱层(Build 模式)被拦截。
220
+
221
+ ## 本地集成验证
222
+
223
+ 安装或 link 插件到 DSH profile 后:
224
+
225
+ 1. 启动 DSH。
226
+ 2. 输入 `/plan-build`,应提示进入 Plan 模式(策略只读)。
227
+ 3. 让 agent 对 `src/x.ts` 调用 `write`,应被拒绝,理由包含 `forbidden in Plan Mode`。
228
+ 4. 让 agent 对 `.opencode/plans/x.md` 调用 `write`,应**允许**(plans 目录例外)。
229
+ 5. 再次输入 `/plan-build`,应提示进入 Build 模式。
230
+ 6. 让 agent 对 `src/x.ts` 调用 `write`,应允许执行。
231
+ 7. 让 agent 读取工作区之外的文件,应无需提示直接放行(默认 `external_directory: allow`)。
232
+ 8. 输入 `/permission status` 查看门禁状态;可尝试 `/permission allow ~/projects/**` 添加会话 allowlist。
233
+ 9. 输入 `/plan-build status`,可只查看当前模式而不切换。
234
+
235
+ ## 许可证
236
+
237
+ MIT
package/cordis.patch.yml CHANGED
@@ -1,10 +1,22 @@
1
1
  # Profile patch for dsh-plan-build-mode.
2
2
  # Loaded automatically when the package is installed in a DSH profile.
3
+ #
4
+ # OpenCode-aligned defaults (v0.5.0):
5
+ # - planSandbox == buildSandbox == workspace-write: "read-only" in Plan mode is
6
+ # enforced by the permission gate (deny every write except the plans
7
+ # directory), not by the sandbox — so plan files can be persisted under
8
+ # .opencode/plans/.
9
+ # - externalDirectory.default: allow — reading files outside the workspace
10
+ # stays permitted in both modes; only writes are gated. Set to 'ask' for
11
+ # full OpenCode behavior (external access prompts).
3
12
  - insert:
4
13
  - id: plan-build-mode
5
14
  name: '@xth26/dsh-plan-build-mode'
6
15
  config:
7
- planSandbox: read-only
16
+ planSandbox: workspace-write
8
17
  buildSandbox: workspace-write
9
18
  denyWriteTools: true
10
19
  section: true
20
+ plansDirectory: .opencode/plans
21
+ externalDirectory:
22
+ default: allow
package/lib/helpers.d.ts CHANGED
@@ -12,6 +12,10 @@ export declare const DEFAULT_PLAN_SANDBOX: SandboxMode;
12
12
  export declare const DEFAULT_BUILD_SANDBOX: SandboxMode;
13
13
  /** Tools forbidden while in Plan mode. */
14
14
  export declare const WRITE_TOOLS: Set<string>;
15
+ /** The durable Plan/Build mode event owned by this plugin. */
16
+ export declare const MODE_EVENT: "plan-build/mode";
17
+ /** A session's Plan/Build mode. */
18
+ export type PlanBuildMode = 'plan' | 'build';
15
19
  /**
16
20
  * Return the most recently written sandbox mode for a session, or undefined
17
21
  * when the session has never switched.
@@ -22,15 +26,24 @@ export declare const WRITE_TOOLS: Set<string>;
22
26
  */
23
27
  export declare function currentSandboxMode(session: Session): SandboxMode | undefined;
24
28
  /**
25
- * Whether the session is currently in OpenCode Plan mode.
26
- * Plan mode is defined as the current sandbox mode matching the configured
27
- * planSandbox (read-only by default).
29
+ * The session's durable Plan/Build mode: the last `plan-build/mode` event, or
30
+ * an inference from the last `sandbox/mode` event against `planSandbox` for
31
+ * sessions created before the mode event existed. Returns undefined only when
32
+ * the session log carries neither.
33
+ */
34
+ export declare function currentPlanBuildMode(session: Session, planSandbox: SandboxMode): PlanBuildMode | undefined;
35
+ /**
36
+ * Whether the session is currently in OpenCode Plan mode. The plugin's own
37
+ * `plan-build/mode` event is authoritative; legacy sessions (only
38
+ * `sandbox/mode` events) fall back to "current sandbox equals planSandbox".
28
39
  */
29
40
  export declare function isPlanModeActive(session: Session, planSandbox: SandboxMode): boolean;
30
41
  /**
31
- * Append a `sandbox/mode` event to switch between Plan and Build mode.
42
+ * Append the durable `plan-build/mode` event to switch between Plan and Build
43
+ * mode, and — only when the target sandbox actually differs from the current
44
+ * one — the `sandbox/mode` event for sandbox enforcement.
32
45
  */
33
- export declare function setPlanBuildMode(agent: Agent, mode: 'plan' | 'build', planSandbox: SandboxMode, buildSandbox: SandboxMode): void;
46
+ export declare function setPlanBuildMode(agent: Agent, mode: PlanBuildMode, planSandbox: SandboxMode, buildSandbox: SandboxMode): void;
34
47
  /**
35
48
  * Compute a denial reason if a tool call should be blocked in Plan mode.
36
49
  * Returns `undefined` when the call is allowed.
@@ -39,6 +52,9 @@ export declare function setPlanBuildMode(agent: Agent, mode: 'plan' | 'build', p
39
52
  * configured as read-only. If Plan mode is configured to a writable sandbox,
40
53
  * the plugin still tracks the mode but does not block tools — the real
41
54
  * protection is the read-only sandbox.
55
+ *
56
+ * Kept for backward compatibility; the plugin's full permission gate
57
+ * (`permissions.ts`) supersedes this check.
42
58
  */
43
59
  export declare function planModeDenial(exec: ToolExecution, planSandbox: SandboxMode, writeTools?: ReadonlySet<string>): string | undefined;
44
60
  //# sourceMappingURL=helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAG3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAE3D,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,WAAyB,CAAA;AAE5D,uDAAuD;AACvD,eAAO,MAAM,qBAAqB,EAAE,WAA+B,CAAA;AAEnE,0CAA0C;AAC1C,eAAO,MAAM,WAAW,aAA6B,CAAA;AAErD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAS5E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAEpF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,GAAG,IAAI,CAGhI;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,WAAW,CAAC,MAAM,CAAe,GAC5C,MAAM,GAAG,SAAS,CAQpB"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAG3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAI3D,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,WAAyB,CAAA;AAE5D,uDAAuD;AACvD,eAAO,MAAM,qBAAqB,EAAE,WAA+B,CAAA;AAEnE,0CAA0C;AAC1C,eAAO,MAAM,WAAW,aAA6B,CAAA;AAErD,8DAA8D;AAC9D,eAAO,MAAM,UAAU,EAAG,iBAA0B,CAAA;AAEpD,mCAAmC;AACnC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAA;AAE5C;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAS5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,GAAG,aAAa,GAAG,SAAS,CAe1G;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAEpF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,WAAW,GACxB,IAAI,CAMN;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,WAAW,CAAC,MAAM,CAAe,GAC5C,MAAM,GAAG,SAAS,CAQpB"}
package/lib/helpers.js CHANGED
@@ -8,6 +8,8 @@ export const DEFAULT_PLAN_SANDBOX = 'read-only';
8
8
  export const DEFAULT_BUILD_SANDBOX = 'workspace-write';
9
9
  /** Tools forbidden while in Plan mode. */
10
10
  export const WRITE_TOOLS = new Set(['write', 'edit']);
11
+ /** The durable Plan/Build mode event owned by this plugin. */
12
+ export const MODE_EVENT = 'plan-build/mode';
11
13
  /**
12
14
  * Return the most recently written sandbox mode for a session, or undefined
13
15
  * when the session has never switched.
@@ -27,19 +29,46 @@ export function currentSandboxMode(session) {
27
29
  return undefined;
28
30
  }
29
31
  /**
30
- * Whether the session is currently in OpenCode Plan mode.
31
- * Plan mode is defined as the current sandbox mode matching the configured
32
- * planSandbox (read-only by default).
32
+ * The session's durable Plan/Build mode: the last `plan-build/mode` event, or
33
+ * an inference from the last `sandbox/mode` event against `planSandbox` for
34
+ * sessions created before the mode event existed. Returns undefined only when
35
+ * the session log carries neither.
36
+ */
37
+ export function currentPlanBuildMode(session, planSandbox) {
38
+ const events = session.snapshotEvents();
39
+ for (let i = events.length - 1; i >= 0; i -= 1) {
40
+ const event = events[i];
41
+ if (event.type === MODE_EVENT) {
42
+ return event.data.mode;
43
+ }
44
+ }
45
+ for (let i = events.length - 1; i >= 0; i -= 1) {
46
+ const event = events[i];
47
+ if (event.type === 'sandbox/mode') {
48
+ return event.data.mode === planSandbox ? 'plan' : 'build';
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ /**
54
+ * Whether the session is currently in OpenCode Plan mode. The plugin's own
55
+ * `plan-build/mode` event is authoritative; legacy sessions (only
56
+ * `sandbox/mode` events) fall back to "current sandbox equals planSandbox".
33
57
  */
34
58
  export function isPlanModeActive(session, planSandbox) {
35
- return currentSandboxMode(session) === planSandbox;
59
+ return currentPlanBuildMode(session, planSandbox) === 'plan';
36
60
  }
37
61
  /**
38
- * Append a `sandbox/mode` event to switch between Plan and Build mode.
62
+ * Append the durable `plan-build/mode` event to switch between Plan and Build
63
+ * mode, and — only when the target sandbox actually differs from the current
64
+ * one — the `sandbox/mode` event for sandbox enforcement.
39
65
  */
40
66
  export function setPlanBuildMode(agent, mode, planSandbox, buildSandbox) {
67
+ agent.session.append(MODE_EVENT, { mode });
41
68
  const target = mode === 'plan' ? planSandbox : buildSandbox;
42
- agent.session.append('sandbox/mode', { mode: target });
69
+ if (currentSandboxMode(agent.session) !== target) {
70
+ agent.session.append('sandbox/mode', { mode: target });
71
+ }
43
72
  }
44
73
  /**
45
74
  * Compute a denial reason if a tool call should be blocked in Plan mode.
@@ -49,6 +78,9 @@ export function setPlanBuildMode(agent, mode, planSandbox, buildSandbox) {
49
78
  * configured as read-only. If Plan mode is configured to a writable sandbox,
50
79
  * the plugin still tracks the mode but does not block tools — the real
51
80
  * protection is the read-only sandbox.
81
+ *
82
+ * Kept for backward compatibility; the plugin's full permission gate
83
+ * (`permissions.ts`) supersedes this check.
52
84
  */
53
85
  export function planModeDenial(exec, planSandbox, writeTools = WRITE_TOOLS) {
54
86
  if (exec.agent === undefined)
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,sDAAsD;AACtD,MAAM,CAAC,MAAM,oBAAoB,GAAgB,WAAW,CAAA;AAE5D,uDAAuD;AACvD,MAAM,CAAC,MAAM,qBAAqB,GAAgB,iBAAiB,CAAA;AAEnE,0CAA0C;AAC1C,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA;AAErD;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,MAAM,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAA;IACvC,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAE,CAAA;QACxB,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAA;QACxB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB,EAAE,WAAwB;IACzE,OAAO,kBAAkB,CAAC,OAAO,CAAC,KAAK,WAAW,CAAA;AACpD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAY,EAAE,IAAsB,EAAE,WAAwB,EAAE,YAAyB;IACxH,MAAM,MAAM,GAAgB,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAA;IACxE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;AACxD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAmB,EACnB,WAAwB,EACxB,aAAkC,WAAW;IAE7C,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC9C,IAAI,WAAW,KAAK,WAAW;QAAE,OAAO,SAAS,CAAA;IACjD,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC;QAAE,OAAO,SAAS,CAAA;IACxE,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO,SAAS,IAAI,CAAC,IAAI,qHAAqH,CAAA;IAChJ,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC"}
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * Host half of `dsh-plan-build-mode`.
3
3
  *
4
- * Implements an OpenCode-style Plan / Build mode that is independent from
5
- * DSH's built-in `/plan` soft-guidance mode:
4
+ * Implements an OpenCode-style Plan / Build mode with a tool permission gate:
6
5
  *
7
6
  * - `/plan-build` -> Toggle between Plan and Build modes
8
7
  * - `/plan-build status` -> Show the current mode
9
- * - `/plan-build switch plan` -> Plan mode + read-only sandbox
10
- * - `/plan-build switch build` -> Build mode + workspace-write sandbox
11
- * - Blocks `write`/`edit` tool calls while in Plan mode
8
+ * - `/plan-build switch plan` -> Plan mode (read-only by policy)
9
+ * - `/plan-build switch build` -> Build mode (writable)
10
+ * - `/permission status|allow|deny` -> Inspect / adjust the permission gate
11
+ * - Denies write-category tools while in Plan mode (OpenCode `edit` permission)
12
+ * - Permits reads outside the workspace (DSH reads pass through every mode);
13
+ * external writes stay gated by the sandbox unless `external_directory` is
14
+ * configured to `ask`
12
15
  * - Injects a `plan-build:policy` system prompt section
13
16
  *
17
+ * Mode state lives in the plugin-owned `plan-build/mode` session event; the
18
+ * `sandbox/mode` event is still written for sandbox enforcement whenever the
19
+ * target sandbox actually differs.
20
+ *
14
21
  * @module dsh-plan-build-mode
15
22
  */
16
23
  import type { Context } from '@deepseek-ai/cordis';
@@ -21,21 +28,11 @@ export declare const name = "dsh-plan-build-mode";
21
28
  /**
22
29
  * Service dependencies for the host half. `commands` is injected optionally at
23
30
  * runtime via ctx.inject(['commands'], ...) so headless assemblies stay
24
- * unaffected; the hard services are systemPrompt and tools.
31
+ * unaffected; `approval` is read optionally via ctx.get('approval').
25
32
  */
26
33
  export declare const inject: readonly ["tools", "systemPrompt"];
27
34
  /** Configuration schema. */
28
- export declare const Config: z<Schemastery.ObjectS<{
29
- planSandbox: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access">;
30
- buildSandbox: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access">;
31
- denyWriteTools: z<boolean, boolean>;
32
- section: z<boolean, boolean>;
33
- }>, Schemastery.ObjectT<{
34
- planSandbox: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access">;
35
- buildSandbox: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access">;
36
- denyWriteTools: z<boolean, boolean>;
37
- section: z<boolean, boolean>;
38
- }>>;
35
+ export declare const Config: z;
39
36
  /**
40
37
  * Plugin entry point.
41
38
  * @param ctx - Cordis context carrying the injected services.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAmBxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD,0BAA0B;AAC1B,eAAO,MAAM,IAAI,wBAAwB,CAAA;AACzC;;;;GAIG;AACH,eAAO,MAAM,MAAM,oCAAqC,CAAA;AACxD,4BAA4B;AAC5B,eAAO,MAAM,MAAM;;;;;;;;;;GAajB,CAAA;AAEF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,mBAAwB,GAAG,IAAI,CAgG1E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AA0CxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD,0BAA0B;AAC1B,eAAO,MAAM,IAAI,wBAAwB,CAAA;AACzC;;;;GAIG;AACH,eAAO,MAAM,MAAM,oCAAqC,CAAA;AAOxD,4BAA4B;AAC5B,eAAO,MAAM,MAAM,EAeF,CAAC,CAAA;AAElB;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,mBAAwB,GAAG,IAAI,CAyK1E"}