@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.md +255 -146
- package/README.zh.md +237 -143
- package/cordis.patch.yml +13 -1
- package/lib/helpers.d.ts +21 -5
- package/lib/helpers.d.ts.map +1 -1
- package/lib/helpers.js +38 -6
- package/lib/helpers.js.map +1 -1
- package/lib/index.d.ts +14 -17
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +186 -33
- package/lib/index.js.map +1 -1
- package/lib/invariant.d.ts +4 -4
- package/lib/invariant.d.ts.map +1 -1
- package/lib/invariant.js +31 -8
- package/lib/invariant.js.map +1 -1
- package/lib/permissions.d.ts +164 -0
- package/lib/permissions.d.ts.map +1 -0
- package/lib/permissions.js +351 -0
- package/lib/permissions.js.map +1 -0
- package/lib/types.d.ts +44 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,146 +1,255 @@
|
|
|
1
|
-
# dsh-plan-build-mode
|
|
2
|
-
|
|
3
|
-
OpenCode-style Plan / Build hard permission model for DeepSeek Harness.
|
|
4
|
-
|
|
5
|
-
This plugin adds an independent Plan/Build mode switch to DSH
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- **
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
1
|
+
# dsh-plan-build-mode
|
|
2
|
+
|
|
3
|
+
OpenCode-style Plan / Build hard permission model for DeepSeek Harness.
|
|
4
|
+
|
|
5
|
+
This plugin adds an independent Plan/Build mode switch to DSH and gates tool
|
|
6
|
+
access with an OpenCode-style permission engine (`allow | ask | deny`):
|
|
7
|
+
|
|
8
|
+
- **Plan mode** is read-only *by policy*: every write-category tool is denied
|
|
9
|
+
except writes under the configured plans directory (`.opencode/plans/`), so
|
|
10
|
+
you can persist plan files while the agent stays read-only everywhere else.
|
|
11
|
+
- **Build mode** is writable: the approved plan can be executed.
|
|
12
|
+
- **Reads are unrestricted in both modes**, including files outside the
|
|
13
|
+
workspace — matching DSH, where reads pass through every sandbox mode. Only
|
|
14
|
+
*modifications* are gated (OpenCode `external_directory`).
|
|
15
|
+
- DSH's built-in `/plan` soft-guidance mode is left untouched.
|
|
16
|
+
|
|
17
|
+
The shipped `cordis.patch.yml` configures both sandboxes as `workspace-write`;
|
|
18
|
+
"read-only" in Plan mode is enforced by the permission gate, not by the
|
|
19
|
+
sandbox. This is what makes plan files persist under `.opencode/plans/` while
|
|
20
|
+
everything else stays read-only.
|
|
21
|
+
|
|
22
|
+
## Version compatibility
|
|
23
|
+
|
|
24
|
+
| Plugin version | DSH version | Notes |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `0.5.x` | `>= 0.1.5-rc.1` | Adds the OpenCode permission gate and `/permission`; mode is carried by the `plan-build/mode` session event (legacy sessions are inferred from `sandbox/mode`). |
|
|
27
|
+
| `0.4.x` | `>= 0.1.5-rc.1` | Uses `session.snapshotEvents()`; DSH 0.1.5 removed the `session.events` snapshot getter. |
|
|
28
|
+
| `<= 0.3.1` | `0.1.0-rc.x` | Legacy releases; broken on DSH 0.1.5+ because of the API change above. |
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
DSH plugins are loaded through a DSH profile. Install the plugin into the profile(s) you use:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# For the web profile
|
|
36
|
+
dsh plugin --profile web add @xth26/dsh-plan-build-mode
|
|
37
|
+
|
|
38
|
+
# For the TUI profile
|
|
39
|
+
dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The plugin ships with a `cordis.patch.yml` that auto-injects the required row
|
|
43
|
+
when the package is loaded by a DSH profile.
|
|
44
|
+
|
|
45
|
+
## Using the same Plan/Build mode in both web and TUI
|
|
46
|
+
|
|
47
|
+
Plan/Build mode state is stored in the session event log (the plugin-owned
|
|
48
|
+
`plan-build/mode` event, plus `sandbox/mode` for sandbox enforcement). Both
|
|
49
|
+
`dsh web` and `dsh --profile <name>` use the same session store when they share
|
|
50
|
+
the same profile and session ID.
|
|
51
|
+
|
|
52
|
+
- **Same profile**: if you run `dsh web --profile web` and `dsh --profile web`, the mode is shared because the session events are shared.
|
|
53
|
+
- **Different profiles**: by default `web` and `dsh-tui` are separate profiles with separate session directories. Switching in one does **not** affect the other.
|
|
54
|
+
|
|
55
|
+
To make web and TUI share the same mode and session history, use the **same profile** for both:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Use the web profile for both interfaces
|
|
59
|
+
dsh web --profile web
|
|
60
|
+
dsh --profile web
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or, if you prefer to keep separate profiles but want to share only the session store, override the session root in each profile's `cordis.patch.yml` to point to the same directory:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- id: session-root
|
|
67
|
+
config:
|
|
68
|
+
root: /path/to/shared/sessions
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Note: sharing session directories across profiles requires both profiles to mount compatible service bundles; otherwise event interpretation may differ.
|
|
72
|
+
|
|
73
|
+
## Local development / link
|
|
74
|
+
|
|
75
|
+
To try the plugin from a local checkout without publishing:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# From the plugin checkout
|
|
79
|
+
cd /path/to/dsh-plan-build-mode
|
|
80
|
+
pnpm link --global
|
|
81
|
+
|
|
82
|
+
# From your DSH profile directory
|
|
83
|
+
pnpm link --global @xth26/dsh-plan-build-mode
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then start DSH with that profile. The `dsh.bundle.patch` field in the plugin's
|
|
87
|
+
`package.json` makes the patch apply automatically.
|
|
88
|
+
|
|
89
|
+
## Updating
|
|
90
|
+
|
|
91
|
+
For `0.x` versions, the semver range `^0.1.0` only matches `0.1.x` and will **never** auto-update to `0.2.0`. Always bump the declaration explicitly and restart the DSH process:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Bump the installed version in the target profile
|
|
95
|
+
# (run this outside a sandboxed agent session; the profile directory is not writable from DSH)
|
|
96
|
+
dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode@0.5.0
|
|
97
|
+
|
|
98
|
+
# Restart the DSH process that uses that profile
|
|
99
|
+
# (old Node process still holds the previous plugin code in memory)
|
|
100
|
+
Stop-Process -Name dsh -Force # PowerShell
|
|
101
|
+
# or: taskkill /IM dsh.exe /F # CMD
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then start `dsh web` or `dsh tui` again. To always pull the latest published version:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
dsh plugin --profile dsh-tui add @xth26/dsh-plan-build-mode@latest
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Upgrade notes (0.4.x → 0.5.x)
|
|
111
|
+
|
|
112
|
+
- `planSandbox` and `buildSandbox` **may now be equal** (the shipped patch uses
|
|
113
|
+
`workspace-write` for both); the permission gate enforces the read-only
|
|
114
|
+
policy in Plan mode. The old "must differ" requirement is gone.
|
|
115
|
+
- Mode state moves to the plugin-owned `plan-build/mode` event. Sessions that
|
|
116
|
+
only have `sandbox/mode` events are inferred on load and pinned to a real
|
|
117
|
+
`plan-build/mode` event at session start, so no history is lost.
|
|
118
|
+
- The legacy `denyWriteTools` option still works: setting it to `false` lets
|
|
119
|
+
the agent write in Plan mode (equivalent to overriding `modes.plan.edit =
|
|
120
|
+
allow`).
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
| Command | Effect |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `/plan-build` | Toggle between Plan and Build modes |
|
|
127
|
+
| `/plan-build status` | Show the current mode |
|
|
128
|
+
| `/plan-build switch plan` | Enter Plan mode (read-only by policy) |
|
|
129
|
+
| `/plan-build switch build` | Enter Build mode (writable) |
|
|
130
|
+
| `/plan-build switch` | Same as `/plan-build`; toggles between modes |
|
|
131
|
+
| `/permission status` | Show the external-path default, rules, and the session allowlist |
|
|
132
|
+
| `/permission allow <pattern>` | Add a pattern to this session's allowlist (OpenCode "always") |
|
|
133
|
+
| `/permission deny <pattern>` | Remove a pattern from this session's allowlist |
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
- id: plan-build-mode
|
|
139
|
+
name: '@xth26/dsh-plan-build-mode'
|
|
140
|
+
config:
|
|
141
|
+
planSandbox: workspace-write
|
|
142
|
+
buildSandbox: workspace-write
|
|
143
|
+
denyWriteTools: true
|
|
144
|
+
section: true
|
|
145
|
+
plansDirectory: .opencode/plans
|
|
146
|
+
externalDirectory:
|
|
147
|
+
default: allow # allow | ask | deny — reads outside the workspace
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`planSandbox` and `buildSandbox` may be equal or differ. When they differ, mode
|
|
151
|
+
switches also move the sandbox (`read-only` → `workspace-write` etc.). When
|
|
152
|
+
they are equal (shipped default), the permission gate is what enforces the
|
|
153
|
+
Plan-mode policy.
|
|
154
|
+
|
|
155
|
+
### Permission model
|
|
156
|
+
|
|
157
|
+
Each tool maps to a category:
|
|
158
|
+
|
|
159
|
+
| Category | Tools |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `read` | `read`, `read_image`, `pdf_read`, `docx_read`, `pptx_read`, `xlsx_read` |
|
|
162
|
+
| `edit` | `write`, `edit`, `pdf_create`, `docx_create`, `pptx_create`, `xlsx_write`, `xlsx_edit` |
|
|
163
|
+
| `glob` | `glob` |
|
|
164
|
+
| `grep` | `grep` |
|
|
165
|
+
| `bash` | `bash` |
|
|
166
|
+
| `pwsh` | `pwsh` |
|
|
167
|
+
| `webfetch` | `web_fetch` |
|
|
168
|
+
| `websearch` | `web_search` |
|
|
169
|
+
|
|
170
|
+
A category value is either a scalar (`allow | ask | deny`) or an object of
|
|
171
|
+
ordered rules, like OpenCode:
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
permission:
|
|
175
|
+
read:
|
|
176
|
+
'*': allow
|
|
177
|
+
'**/.env': deny
|
|
178
|
+
'**/.env.*': deny
|
|
179
|
+
'**/.env.example': allow # last matching rule wins
|
|
180
|
+
bash: ask
|
|
181
|
+
edit: deny
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Patterns support OpenCode wildcards (`*` matches any characters including `/`,
|
|
185
|
+
`?` matches exactly one character, `**` behaves like `*`) and `~`/`$HOME`
|
|
186
|
+
expansion. Rules are evaluated last-match-wins. The default rules mirror
|
|
187
|
+
OpenCode: most categories `allow`; `.env*` reads are denied (`.env.example`
|
|
188
|
+
allowed back).
|
|
189
|
+
|
|
190
|
+
Plan mode overrides (unless overridden by `modes.plan` or `denyWriteTools:
|
|
191
|
+
false`): `edit: deny`, `bash: ask`, `pwsh: ask` — mirroring OpenCode's plan
|
|
192
|
+
agent defaults. `modes.plan` / `modes.build` override the global permission for
|
|
193
|
+
that mode, like OpenCode's agent-specific overrides.
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
modes:
|
|
197
|
+
plan:
|
|
198
|
+
bash: deny # instead of the default ask
|
|
199
|
+
build:
|
|
200
|
+
read: allow
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Plans carve-out
|
|
204
|
+
|
|
205
|
+
In Plan mode, `edit`-category writes under `plansDirectory` (default
|
|
206
|
+
`.opencode/plans/`, relative to the session workspace) are always allowed, so
|
|
207
|
+
`/plan-build` and plan files keep working. Every other write in Plan mode is
|
|
208
|
+
denied with a reason that includes `forbidden in Plan Mode`.
|
|
209
|
+
|
|
210
|
+
### External paths
|
|
211
|
+
|
|
212
|
+
- **Reads** outside the workspace are allowed by default in both modes
|
|
213
|
+
(`externalDirectory.default: allow`). Set it to `ask` for full OpenCode
|
|
214
|
+
behavior: the agent asks for approval on external reads.
|
|
215
|
+
- **Writes** outside the workspace are governed by the sandbox. Under
|
|
216
|
+
`workspace-write` they cannot be permitted, so instead of prompting (which
|
|
217
|
+
would just fail at the sandbox), the plugin denies with an escalation hint —
|
|
218
|
+
retry the same tool with `sandbox_permissions` + `justification`.
|
|
219
|
+
- `/permission allow <pattern>` adds a session-scoped allowlist entry
|
|
220
|
+
(OpenCode "always"), persisted in memory for the session's lifetime.
|
|
221
|
+
|
|
222
|
+
## Plan mode: command-line Python for inspection
|
|
223
|
+
|
|
224
|
+
While Plan mode denies `write`/`edit` tools, it is **encouraged** to run short,
|
|
225
|
+
read-only command-line Python snippets to inspect data and validate
|
|
226
|
+
assumptions. For example:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
python -B -c "import json, sys; data = json.load(open('data/sample.json')); print(len(data))"
|
|
230
|
+
python -B -c "import pandas as pd; print(pd.read_csv('data.csv').describe())"
|
|
231
|
+
pytest -p no:cacheprovider -q tests/test_sanity.py
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Use `-B` (or set `PYTHONDONTWRITEBYTECODE=1`) and `pytest -p no:cacheprovider`
|
|
235
|
+
to avoid writing `__pycache__` or `.pytest_cache`. The permission gate and the
|
|
236
|
+
sandbox are the final guardrails; anything that tries to write files is blocked
|
|
237
|
+
at the gate (Plan mode) or the sandbox layer (Build mode).
|
|
238
|
+
|
|
239
|
+
## Local integration check
|
|
240
|
+
|
|
241
|
+
After installing/linking the plugin in a DSH profile:
|
|
242
|
+
|
|
243
|
+
1. Start DSH with that profile.
|
|
244
|
+
2. Run `/plan-build` to enter Plan mode (read-only by policy). You should see a confirmation.
|
|
245
|
+
3. Ask the agent to call the `write` tool on `src/x.ts`. It should be denied with a reason containing `forbidden in Plan Mode`.
|
|
246
|
+
4. Ask the agent to call `write` on `.opencode/plans/x.md`. It should be **allowed** (plans carve-out).
|
|
247
|
+
5. Run `/plan-build` again to enter Build mode (writable). You should see a confirmation.
|
|
248
|
+
6. Ask the agent to call `write` on `src/x.ts`. It should now be allowed.
|
|
249
|
+
7. Ask the agent to read a file outside the workspace. It should be allowed without prompting (default `external_directory: allow`).
|
|
250
|
+
8. Run `/permission status` to inspect the gate; try `/permission allow ~/projects/**` to add a session allowlist entry.
|
|
251
|
+
9. Run `/plan-build status` to check the current mode without switching.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|