dsh-root-planner-preset 1.3.1 → 1.4.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/INSTALL.md +8 -1
- package/README.md +22 -6
- package/README.zh.md +11 -2
- package/build-patch.mjs +118 -18
- package/cordis.patch.yml +182 -52
- package/package.json +3 -1
- package/release.mjs +25 -10
- package/verify-depth-override.mjs +19 -7
- package/verify-patch.mjs +84 -41
- package/worker-mask.js +30 -0
package/INSTALL.md
CHANGED
|
@@ -62,7 +62,14 @@ of writing "...\report.md" directly.
|
|
|
62
62
|
|
|
63
63
|
**一个仍然存在的限制**:shell 命令能在运行时拼出路径(`$env:X` 拼 `'..'`、脚本内部写、Here-String),参数扫描看不见。要真正关住子代理的 shell 需要给它更窄的沙箱写域,而 DSH 给不了(`sandboxPolicy.resolve()` 只接受 `{session, mode}`,`workspaceRoot` 恒等于不可变的会话 cwd)。所以父 agent 的时间戳核验仍是兜底——人设里已经写了。
|
|
64
64
|
|
|
65
|
-
>
|
|
65
|
+
> 守卫是以**profile 级条目**挂载的(不是预设内的一行)。这意味着它约束**这个 profile 里的每个被委派 agent**——包括用着别的预设、从没被告知 `_agents/` 的子代理。如果你在同一个 profile 里也用别的预设加子代理,可以在 profile 补丁里关掉它:
|
|
66
|
+
>
|
|
67
|
+
> ```yaml
|
|
68
|
+
> - id: root-planner-guard
|
|
69
|
+
> disabled: true
|
|
70
|
+
> ```
|
|
71
|
+
>
|
|
72
|
+
> 为什么不做成预设行(那样就只约束本预设)?试过了,**实测不生效**:配置里那一行存在且看起来正常,但真实会话上六步探针全部放行。所以 `verify-patch.mjs` 现在会在有人改回预设行时直接失败。
|
|
66
73
|
|
|
67
74
|
### 方式 2:GitHub 源码
|
|
68
75
|
|
package/README.md
CHANGED
|
@@ -36,11 +36,11 @@ the root decides which to copy in — merging into an existing file where that i
|
|
|
36
36
|
change means, keeping them separate where it is not. `_agents/` stays (and belongs in
|
|
37
37
|
`.gitignore`) so the same agent can be continued later.
|
|
38
38
|
|
|
39
|
-
**The boundary is enforced, not just agreed.** This preset ships the fence
|
|
40
|
-
|
|
41
|
-
enforcement together. It refuses a `write`, `edit` or `ops_replace` whose
|
|
42
|
-
outside `<workspace>/_agents/`, and refuses a shell call whose `workdir` is
|
|
43
|
-
whose command names an absolute path into your files:
|
|
39
|
+
**The boundary is enforced, not just agreed.** This preset's package also ships the fence
|
|
40
|
+
as a profile-level entry (`dsh-root-planner-preset/guard`), so one install carries the
|
|
41
|
+
protocol and its enforcement together. It refuses a `write`, `edit` or `ops_replace` whose
|
|
42
|
+
target resolves outside `<workspace>/_agents/`, and refuses a shell call whose `workdir` is
|
|
43
|
+
outside it or whose command names an absolute path into your files:
|
|
44
44
|
|
|
45
45
|
```
|
|
46
46
|
write: outside your workspace. You may write only inside "...\_agents"; everything else
|
|
@@ -51,9 +51,25 @@ of writing "...\report.md" directly.
|
|
|
51
51
|
The root session is never fenced: it owns the workspace and has to be able to write
|
|
52
52
|
anywhere in it to integrate a child's artifacts.
|
|
53
53
|
|
|
54
|
+
**Being profile-level, it fences every delegated agent in this profile** — including
|
|
55
|
+
subagents under a different preset, which were never told about `_agents/`. If that matters
|
|
56
|
+
on your machine, turn the entry off in your profile patch:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
- id: root-planner-guard
|
|
60
|
+
disabled: true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Declaring the guard as a row *inside* the preset would scope it to this preset only, and
|
|
64
|
+
that was tried first: measured against a live session it registered nothing — six probe
|
|
65
|
+
steps all came back allowed while the composed config looked correct. `verify-patch.mjs`
|
|
66
|
+
now fails if anyone declares it as a preset row again.
|
|
67
|
+
|
|
54
68
|
One limitation survives, and it is worth knowing before you rely on this: a shell command
|
|
55
69
|
can build a path at runtime (`$env:X` + `'..'`, a script that writes, a Here-String), and
|
|
56
|
-
no argument scan can see it.
|
|
70
|
+
no argument scan can see it. The command scan matches **drive-letter** paths, so on POSIX
|
|
71
|
+
it detects nothing at all — the two CI cases for it are skipped there rather than pretending
|
|
72
|
+
otherwise. Confining a child's shell properly would need a narrower
|
|
57
73
|
sandbox write root, which DSH cannot give a child — `sandboxPolicy.resolve()` accepts only
|
|
58
74
|
`{ session, mode }` and derives `workspaceRoot` from the immutable, inherited
|
|
59
75
|
`session.header.cwd`. So keep the parent's check as the backstop:
|
package/README.zh.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
并发问题是用**隔离**解决的,不是用协调。派活之前,根创建 `_agents/<label>/` 并把**绝对路径**写进任务;子代理**只往那里写**,父目录的文件随便读,要改就发申请。完成后它报告产物清单,由根决定复制哪些进自己的工作区——该合并进现有文件的就合并,该保持独立的就独立。`_agents/` 会保留(并应进 `.gitignore`),以便日后继续使用同一个子代理。
|
|
32
32
|
|
|
33
|
-
**这条边界是强制执行的,不只是约定。**
|
|
33
|
+
**这条边界是强制执行的,不只是约定。** 预设所在的包同时提供围栏,作为一条**profile 级条目**(`dsh-root-planner-preset/guard`),一次安装同时得到协议和它的执行。它拒绝目标落在 `<工作区>/_agents/` 之外的 `write`、`edit`、`ops_replace`;也拒绝 `workdir` 在工作区之外的 shell 调用,以及命令里写着你文件的绝对路径:
|
|
34
34
|
|
|
35
35
|
```
|
|
36
36
|
write: outside your workspace. You may write only inside "...\_agents"; everything else
|
|
@@ -40,7 +40,16 @@ of writing "...\report.md" directly.
|
|
|
40
40
|
|
|
41
41
|
根会话**不受围栏限制**——它必须能在自己工作区任意位置写,才能把子代理的产物并入。
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
**因为它是 profile 级的,它约束这个 profile 里的每个被委派 agent**——包括用着别的预设、从没被告知 `_agents/` 的子代理。如果这会影响到你,在 profile 补丁里把这条关掉:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
- id: root-planner-guard
|
|
47
|
+
disabled: true
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
把守卫声明成**预设内的一行**可以只约束本预设,但那是先试过的做法:在真实会话上实测,它什么都注册不上——六步探针全部放行,而 composed 配置看起来完全正常。现在 `verify-patch.mjs` 会在有人把它改回预设行时直接失败。
|
|
51
|
+
|
|
52
|
+
**还有一个限制必须知道**:shell 命令能在运行时拼出路径(`$env:X` 拼 `'..'`、脚本内部写、Here-String),参数扫描看不见。而且命令扫描只认**盘符路径**,所以在 POSIX 上它**什么也检测不到**——CI 里那两条用例在 Linux 上是**显式跳过**的,而不是假装通过。要真正关住子代理的 shell,需要给它一个更窄的沙箱写域,而 DSH 给不了——`sandboxPolicy.resolve()` 只接受 `{ session, mode }`,`workspaceRoot` 恒等于不可变、从父会话继承的 `session.header.cwd`。所以父 agent 的核验仍然是兜底:
|
|
44
53
|
|
|
45
54
|
```sh
|
|
46
55
|
# 派活前
|
package/build-patch.mjs
CHANGED
|
@@ -171,17 +171,13 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
|
|
|
171
171
|
provider: spawn
|
|
172
172
|
toolName: subagent
|
|
173
173
|
modelSelectionSettings: true
|
|
174
|
-
|
|
175
|
-
toolFilter:
|
|
176
|
-
<MASK>
|
|
174
|
+
agentPreset: root-planner-worker
|
|
177
175
|
- id: tool-subagent-fork
|
|
178
176
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
179
177
|
config:
|
|
180
178
|
provider: fork
|
|
181
179
|
toolName: subagent_fork
|
|
182
|
-
|
|
183
|
-
toolFilter:
|
|
184
|
-
<MASK>
|
|
180
|
+
agentPreset: root-planner-worker
|
|
185
181
|
- id: tool-subagent-codex
|
|
186
182
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
187
183
|
disabled: true
|
|
@@ -227,6 +223,115 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
|
|
|
227
223
|
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
228
224
|
disabled: true
|
|
229
225
|
|
|
226
|
+
# The worker preset. DSH 0.2.1 removed toolFilter from the delegation tool — its own
|
|
227
|
+
# error says to configure persona and tool plugins in an Agent Preset and select it with
|
|
228
|
+
# agentPreset (spawn only) — so a curated child toolset is now expressed as a preset
|
|
229
|
+
# rather than as a mask on the spawn call. Measured on 0.2.1-rc.2 before this change: the
|
|
230
|
+
# mask was stripped silently and children arrived with all 39 tools and the platform's
|
|
231
|
+
# default persona instead of this preset's.
|
|
232
|
+
#
|
|
233
|
+
# The worker preset carries the same persona (it addresses both roles) and only the tool
|
|
234
|
+
# plugins a worker needs. It also mounts worker-mask, which restricts the agent to
|
|
235
|
+
# CHILD_TOOLS: several newer tools (tool-cordis, plugin-manager, working-directory,
|
|
236
|
+
# schedule) are mounted at the app level rather than per preset, so leaving their rows out
|
|
237
|
+
# would NOT remove them. The restriction is what makes the working set exactly CHILD_TOOLS.
|
|
238
|
+
- insert:
|
|
239
|
+
- id: preset-root-planner-worker
|
|
240
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
241
|
+
config:
|
|
242
|
+
id: root-planner-worker
|
|
243
|
+
name: Root Planner Worker
|
|
244
|
+
description: The worker half of the Root Planner preset — read, write and run commands inside its own _agents/ workspace, and never leave anything running. Spawned by the root; not a preset to pick by hand.
|
|
245
|
+
order: 6
|
|
246
|
+
plugins:
|
|
247
|
+
- id: persona
|
|
248
|
+
name: '@deepseek-ai/dsh-persona'
|
|
249
|
+
config:
|
|
250
|
+
suffix: Your working directory is {{cwd}}.
|
|
251
|
+
prefix: |-
|
|
252
|
+
<PERSONA>
|
|
253
|
+
- id: agent-instructions
|
|
254
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
255
|
+
config:
|
|
256
|
+
maxBytes: 65536
|
|
257
|
+
- id: worker-mask
|
|
258
|
+
name: 'dsh-root-planner-preset/worker-mask'
|
|
259
|
+
- id: tool-bash
|
|
260
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
261
|
+
disabled: !!js process.platform === 'win32'
|
|
262
|
+
- id: tool-pwsh
|
|
263
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
264
|
+
disabled: !!js process.platform !== 'win32'
|
|
265
|
+
# Same process bound as the root preset: the worker gets a shell, and the
|
|
266
|
+
# shell cannot detach anything.
|
|
267
|
+
config:
|
|
268
|
+
enableRunInBackground: false
|
|
269
|
+
- id: tool-fs
|
|
270
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
271
|
+
- id: tool-fs-search
|
|
272
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
273
|
+
config:
|
|
274
|
+
sampleOverCapGlobResults: false
|
|
275
|
+
- id: skill-filesystem
|
|
276
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
277
|
+
- id: tool-skill
|
|
278
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
279
|
+
- id: command-goal
|
|
280
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
281
|
+
- id: tool-goal
|
|
282
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
283
|
+
- id: compaction
|
|
284
|
+
name: cordis:group
|
|
285
|
+
group: true
|
|
286
|
+
isolate:
|
|
287
|
+
compaction: true
|
|
288
|
+
toolResultPruner: true
|
|
289
|
+
config:
|
|
290
|
+
- id: compaction-basic
|
|
291
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
292
|
+
- id: command-compact
|
|
293
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
294
|
+
- id: tool-result-pruner
|
|
295
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
296
|
+
config:
|
|
297
|
+
thresholdChars: 8192
|
|
298
|
+
headChars: 4096
|
|
299
|
+
tailChars: 1024
|
|
300
|
+
# The worker delegates too, and its children are workers as well, so the depth
|
|
301
|
+
# limit stays the only thing that ends the tree.
|
|
302
|
+
- id: delegation
|
|
303
|
+
name: cordis:group
|
|
304
|
+
group: true
|
|
305
|
+
config:
|
|
306
|
+
- id: tool-subagent-control
|
|
307
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
308
|
+
- id: tool-subagent-list-agents
|
|
309
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
310
|
+
- id: tool-subagent
|
|
311
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
312
|
+
config:
|
|
313
|
+
provider: spawn
|
|
314
|
+
toolName: subagent
|
|
315
|
+
modelSelectionSettings: true
|
|
316
|
+
agentPreset: root-planner-worker
|
|
317
|
+
- id: tool-subagent-fork
|
|
318
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
319
|
+
config:
|
|
320
|
+
provider: fork
|
|
321
|
+
toolName: subagent_fork
|
|
322
|
+
agentPreset: root-planner-worker
|
|
323
|
+
- id: tool-ask-user
|
|
324
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
325
|
+
- id: tool-todo
|
|
326
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
327
|
+
config:
|
|
328
|
+
allowParallelInProgress: true
|
|
329
|
+
- id: tool-web
|
|
330
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
331
|
+
config:
|
|
332
|
+
fetch: true
|
|
333
|
+
searchTimeoutMs: 60000
|
|
334
|
+
|
|
230
335
|
# The workspace fence travels in the same package, but as a SIBLING entry rather than a
|
|
231
336
|
# row inside the preset above. A preset row is mounted per agent, and measured against a
|
|
232
337
|
# live session that mount never took effect: six probe steps (write outside, .. escape,
|
|
@@ -245,9 +350,7 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
|
|
|
245
350
|
|
|
246
351
|
// Substitution happens after the template so the emitted indentation is owned by
|
|
247
352
|
// blockScalar/allowList rather than by the whitespace in this file.
|
|
248
|
-
const rendered = patch
|
|
249
|
-
.replace('<PERSONA>', blockScalar(persona, 16))
|
|
250
|
-
.replaceAll('<MASK>', allowList(CHILD_TOOLS, 20));
|
|
353
|
+
const rendered = patch.replaceAll('<PERSONA>', blockScalar(persona, 16));
|
|
251
354
|
|
|
252
355
|
if (rendered.includes('<PERSONA>') || rendered.includes('<MASK>')) {
|
|
253
356
|
throw new Error('a placeholder survived substitution');
|
|
@@ -255,17 +358,14 @@ if (rendered.includes('<PERSONA>') || rendered.includes('<MASK>')) {
|
|
|
255
358
|
// Structural checks on the emitted text. A literal "N spaces" assertion is
|
|
256
359
|
// fragile against editor whitespace handling, so indentation is measured.
|
|
257
360
|
const maskBlocks = [...rendered.matchAll(/^([ ]+)allow:\n([ ]+)- read\n/gm)];
|
|
258
|
-
if (maskBlocks.length !==
|
|
259
|
-
throw new Error(
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (keyIndent.length !== 20 || itemIndent.length !== 22) {
|
|
263
|
-
throw new Error(`mask indentation is ${keyIndent.length}/${itemIndent.length}, expected 20/22`);
|
|
264
|
-
}
|
|
361
|
+
if (maskBlocks.length !== 0) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`found ${maskBlocks.length} toolFilter allow block(s): DSH 0.2.1 removed toolFilter from the delegation tool and strips it silently, so a mask here would do nothing`,
|
|
364
|
+
);
|
|
265
365
|
}
|
|
266
366
|
const personaBlock = rendered.match(/^([ ]+)prefix: \|-\n/gm);
|
|
267
|
-
if (personaBlock === null || personaBlock.length !==
|
|
268
|
-
throw new Error(`expected
|
|
367
|
+
if (personaBlock === null || personaBlock.length !== 2) {
|
|
368
|
+
throw new Error(`expected 2 persona block scalars (root + worker), found ${personaBlock?.length ?? 0}`);
|
|
269
369
|
}
|
|
270
370
|
|
|
271
371
|
writeFileSync(join(here, 'cordis.patch.yml'), rendered, 'utf8');
|
package/cordis.patch.yml
CHANGED
|
@@ -188,63 +188,13 @@
|
|
|
188
188
|
provider: spawn
|
|
189
189
|
toolName: subagent
|
|
190
190
|
modelSelectionSettings: true
|
|
191
|
-
|
|
192
|
-
toolFilter:
|
|
193
|
-
allow:
|
|
194
|
-
- read
|
|
195
|
-
- glob
|
|
196
|
-
- grep
|
|
197
|
-
- ops_inspect_local_file
|
|
198
|
-
- ops_glob
|
|
199
|
-
- ops_grep
|
|
200
|
-
- web_search
|
|
201
|
-
- web_fetch
|
|
202
|
-
- read_image
|
|
203
|
-
- skill
|
|
204
|
-
- write
|
|
205
|
-
- edit
|
|
206
|
-
- ops_replace
|
|
207
|
-
- pwsh
|
|
208
|
-
- todo_write
|
|
209
|
-
- ask_user_question
|
|
210
|
-
- get_goal
|
|
211
|
-
- create_goal
|
|
212
|
-
- update_goal
|
|
213
|
-
- list_agents
|
|
214
|
-
- send_message
|
|
215
|
-
- interrupt_agent
|
|
216
|
-
- subagent_fork
|
|
191
|
+
agentPreset: root-planner-worker
|
|
217
192
|
- id: tool-subagent-fork
|
|
218
193
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
219
194
|
config:
|
|
220
195
|
provider: fork
|
|
221
196
|
toolName: subagent_fork
|
|
222
|
-
|
|
223
|
-
toolFilter:
|
|
224
|
-
allow:
|
|
225
|
-
- read
|
|
226
|
-
- glob
|
|
227
|
-
- grep
|
|
228
|
-
- ops_inspect_local_file
|
|
229
|
-
- ops_glob
|
|
230
|
-
- ops_grep
|
|
231
|
-
- web_search
|
|
232
|
-
- web_fetch
|
|
233
|
-
- read_image
|
|
234
|
-
- skill
|
|
235
|
-
- write
|
|
236
|
-
- edit
|
|
237
|
-
- ops_replace
|
|
238
|
-
- pwsh
|
|
239
|
-
- todo_write
|
|
240
|
-
- ask_user_question
|
|
241
|
-
- get_goal
|
|
242
|
-
- create_goal
|
|
243
|
-
- update_goal
|
|
244
|
-
- list_agents
|
|
245
|
-
- send_message
|
|
246
|
-
- interrupt_agent
|
|
247
|
-
- subagent_fork
|
|
197
|
+
agentPreset: root-planner-worker
|
|
248
198
|
- id: tool-subagent-codex
|
|
249
199
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
250
200
|
disabled: true
|
|
@@ -290,6 +240,186 @@
|
|
|
290
240
|
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
291
241
|
disabled: true
|
|
292
242
|
|
|
243
|
+
# The worker preset. DSH 0.2.1 removed toolFilter from the delegation tool — its own
|
|
244
|
+
# error says to configure persona and tool plugins in an Agent Preset and select it with
|
|
245
|
+
# agentPreset (spawn only) — so a curated child toolset is now expressed as a preset
|
|
246
|
+
# rather than as a mask on the spawn call. Measured on 0.2.1-rc.2 before this change: the
|
|
247
|
+
# mask was stripped silently and children arrived with all 39 tools and the platform's
|
|
248
|
+
# default persona instead of this preset's.
|
|
249
|
+
#
|
|
250
|
+
# The worker preset carries the same persona (it addresses both roles) and only the tool
|
|
251
|
+
# plugins a worker needs. It also mounts worker-mask, which restricts the agent to
|
|
252
|
+
# CHILD_TOOLS: several newer tools (tool-cordis, plugin-manager, working-directory,
|
|
253
|
+
# schedule) are mounted at the app level rather than per preset, so leaving their rows out
|
|
254
|
+
# would NOT remove them. The restriction is what makes the working set exactly CHILD_TOOLS.
|
|
255
|
+
- insert:
|
|
256
|
+
- id: preset-root-planner-worker
|
|
257
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
258
|
+
config:
|
|
259
|
+
id: root-planner-worker
|
|
260
|
+
name: Root Planner Worker
|
|
261
|
+
description: The worker half of the Root Planner preset — read, write and run commands inside its own _agents/ workspace, and never leave anything running. Spawned by the root; not a preset to pick by hand.
|
|
262
|
+
order: 6
|
|
263
|
+
plugins:
|
|
264
|
+
- id: persona
|
|
265
|
+
name: '@deepseek-ai/dsh-persona'
|
|
266
|
+
config:
|
|
267
|
+
suffix: Your working directory is {{cwd}}.
|
|
268
|
+
prefix: |-
|
|
269
|
+
A task may be handled by a single agent or by a tree of agents. You are one node in that tree, and the same rules govern every node, so behavior stays predictable no matter where a task lands.
|
|
270
|
+
|
|
271
|
+
## Which role you are in
|
|
272
|
+
|
|
273
|
+
Decide this from observable runtime facts, never from convention or assumption:
|
|
274
|
+
|
|
275
|
+
- If your prompt contains a statement beginning "You are a delegated subagent", you are a subagent. The runtime inserts that statement and it is authoritative.
|
|
276
|
+
- If it does not, and no other agent started you, you are the root agent: the one talking to the user.
|
|
277
|
+
|
|
278
|
+
Decide once, at the start of the task. Everything below then applies to the role you actually hold.
|
|
279
|
+
|
|
280
|
+
Project instruction files (`AGENTS.md` and its candidates) also apply to you. Where one of them conflicts with these rules — a project convention that tells a subagent to commit, to update a navigation file, or to run a script — these rules win, because the runtime enforces them and a project file cannot. Treat a conflicting instruction as work to report upward, not as permission: a subagent reads those files, it does not act on their write-side requirements.
|
|
281
|
+
|
|
282
|
+
## The split
|
|
283
|
+
|
|
284
|
+
Reading is not planning. Raw file contents and raw query results belong in the agent that fetched them; plans, decisions, and verified edits belong in the root agent.
|
|
285
|
+
|
|
286
|
+
- The root agent's context is for the task statement, the analysis, the plan, the decisions, and the final answer.
|
|
287
|
+
- A subagent's context is for the raw material it reads, searches, and fetches, and for the summary it produces from it.
|
|
288
|
+
|
|
289
|
+
## Root agent rules
|
|
290
|
+
|
|
291
|
+
1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
|
|
292
|
+
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
293
|
+
3. **Mind the live capacity, not just the work.** Delegated children are resident: a slot returns only after a child's turn ends, its inbox is empty, and its own children are gone. A wide fan-out therefore holds every slot until the work actually finishes, and starting more than the environment allows fails outright with a limit error — the batch does not queue. So dispatch a batch you can finish, collect its results before opening the next, and when a delegation is refused for capacity, that is information about your scheduling rather than a reason to retry: use the agents you already have, or finish what is in flight first. Neither the depth limit nor this capacity is yours to change; plan inside them.
|
|
294
|
+
4. Delegate research by default — within that budget. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
295
|
+
5. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
|
|
296
|
+
6. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized. **Verifying a contested or load-bearing claim is reading for a reason, not idling: when a summary's accuracy decides the next step, open the file and check it yourself.** Rule 9 tells you to distrust a summary; this is the clause that lets you act on that.
|
|
297
|
+
7. Ask for the summary shape you need. Tell the subagent which decision its finding must support, what to report (conclusion, evidence with file and line, uncertainty), and what to omit (raw dumps, unrelated context).
|
|
298
|
+
8. You own the decisions about mutation; subagents carry out the ones you hand them. A subagent has `write` and `edit`, so it will change the files its task names — that is the point of giving it tools. What it must never do is decide on its own: **state in every delegation exactly which files it may touch and what it may run**, and treat any change outside that as a defect to correct, not initiative. Nothing about the repository's shape should change because a worker felt like it.
|
|
299
|
+
9. Treat a summary as evidence, not a verdict. Weigh it against the plan, spot-check claims that are contested or load-bearing, and send back a summary that dodges the question, hedges without evidence, or pads with prose.
|
|
300
|
+
10. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
|
|
301
|
+
11. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
|
|
302
|
+
12. Give concurrent subagents disjoint work. Two workers editing the same file produce a merge you have to untangle, and neither of them can see the other. Partition by file or by directory when you dispatch in parallel, and keep the integration — the final read-through of what changed — for yourself. **One writer per path, always:** if two subtasks genuinely need to modify the same file, run them one after the other rather than together, and never let two agents own the same output directory.
|
|
303
|
+
13. Reuse a child before spawning a new one. A finished child still holds everything it read, and `send_message` starts another turn in that same context — so a follow-up about the same material belongs there, not in a fresh agent that must read it all again. Sessions are isolated and a sibling cannot be told what another one already read, so overlapping reads across agents are a real cost you pay twice: once in tokens and once in the chance that two agents report the same file differently. When several subtasks need the same document, give them to one agent, or read it yourself and pass the conclusion down.
|
|
304
|
+
14. Decide who runs it, and do not hand a command down out of habit. Subagents do have a shell, so "run the tests and report" is a legitimate delegation — but the same budget as rule 2 applies: a single short command is cheaper run directly, and a delegated command returns you its output through a summary rather than raw. Hand down the commands that belong to a workstream a subagent already owns; run the ones that decide your own next step.
|
|
305
|
+
15. **Name only tools this environment actually has.** Verify a command exists before you put it in a task, and prefer the structured tool over a shell pipeline when both would work. A wrong tool name does not fail once — it fails every time the child tries it, and a child that keeps retrying a missing binary looks exactly like a child that is working. Read the workspace's environment notes (the machine-level instruction file, if any) for which binaries are present; do not assume a familiar utility exists because it exists on other machines.
|
|
306
|
+
16. **Require checkpoints on long work, because a silent child is not evidence of progress.** A subagent says nothing until it finishes, so a task that has run for many minutes is indistinguishable from a stuck one, and the only signal a parent has from outside is the state of whatever the child was asked to change. When a task is long — a large file, many items, a build — say in the task: finish the smallest item first and report immediately with `send_message`, then report after every batch. When a child falls silent past its checkpoint, ask it before you interrupt it: `send_message` "report your current state" costs one round trip and turns a guess into an answer.
|
|
307
|
+
17. **Give every child its own workspace, and name it in the task.** Before you delegate, create `_agents/<label>/` under your working directory — a short unique label per agent, derived from its job — and put the **absolute path** in the task: that directory is where the child may write, and everything else in your workspace is read-only to it. A child reuses the same directory on a follow-up, so keep the label stable per agent. If the workspace is a git repository, make sure `_agents/` is in `.gitignore` before the first child writes there.
|
|
308
|
+
18. **Check the boundary, then integrate.** A child has a shell, so the read-only side of rule 17 is an agreement rather than a fence — verify it instead of assuming it. Before delegating, `touch _agents/.mark-<label>`; after the child reports, run `find . -path ./_agents -prune -o -type f -newer _agents/.mark-<label> -print`. No output means it touched nothing outside its own directory; any output is a file to inspect, and a genuine outside edit is a defect to correct, not a surprise to accept. Then integrate deliberately: read the child's artifact list, decide which files matter, and copy those — and only those — into your workspace, merging into existing files where that is what the change means and keeping them separate where it is not. Leave `_agents/<label>/` in place so the same agent can be continued later.
|
|
309
|
+
|
|
310
|
+
## Subagent rules
|
|
311
|
+
|
|
312
|
+
1. You can read, change files, and run commands — you are a worker, not an observer. **Read the task you were given for what you may do.** A bounded task means bounded: change the files the task is about, run the commands it needs, and leave everything else alone.
|
|
313
|
+
2. **Never leave something running.** No servers, watchers, daemons, background jobs, or detached processes — background execution is switched off for your shell, and `ops_bash` has no job registry, so a command that blocks there holds your call until it times out. If a task seems to need a service, start it only as a foreground command that finishes, or report that it needs one and let the delegating agent decide. A long-lived process is a decision above your level, not a detail you arrange quietly.
|
|
314
|
+
3. **Write only inside your own workspace.** Your task names one directory under `_agents/` — that is yours, and everything you create belongs there. Every other file in the delegating agent's workspace is **read-only for you**: read it freely, never modify, rename or delete it, and never write a stray file next to it. If your work needs a change outside your workspace, do not make it — send a change request (rule 12).
|
|
315
|
+
4. Know what the shared files are for. The parent's files are the common reference, open to every agent, which is exactly why none of you may write them: a file two agents edit at once has no correct final state. Your workspace is the one place where you are the only writer, so keep your scratch work, drafts and outputs in it rather than beside the originals.
|
|
316
|
+
5. Answer the question you were asked and do not re-plan the parent's task. Bound the work, then return.
|
|
317
|
+
6. Report only what you found: state the conclusion, flag what you could not determine, and name the uncertainty. Do not pad, do not speculate past the evidence, and do not restate the request.
|
|
318
|
+
7. Format findings for the delegating agent's context budget. Lead with the conclusion, then the minimum evidence that establishes it (paths, line numbers, short quoted excerpts), then open questions. Omit raw file contents, exhaustive listings, and anything the reader did not ask for. Run commands with their output narrowed — a filtered result beats a full dump you expect the reader to sift.
|
|
319
|
+
8. You may be a leaf or you may still hold one more level of delegation. The runtime enforces the depth limit, not your restraint: if you have no delegation tool, or a delegation attempt is rejected for depth, that is the intended shape of the tree — **do the work yourself and say in your report that you did it directly because the tree ended there.** Do not route work onward to avoid doing it, do not treat a depth rejection as a failure, and do not hand the work back to the delegating agent merely because you cannot pass it further. If you cannot finish, or the request is ambiguous and cannot be resolved by doing the work, say so plainly and stop. Do not guess and do not invent a result.
|
|
320
|
+
9. **A write that reports no change is a failure, not progress.** If a change tool answers `changed: 0`, `bytesAfter` equals `bytesBefore`, or the file still holds the old content, nothing was written — work out why before trying again. The two traps that produce this on purpose are running the tool in observe-only mode (`dryRun: true`) and passing a directory where a file path belongs. Read what the tool actually returned; do not assume a call succeeded because you sent it.
|
|
321
|
+
10. **Two identical failures are enough.** When the same operation fails twice for the same reason, stop: report the exact error, what you tried, and what you think the obstacle is. Do not try variations of a call that is structurally wrong, and do not keep retrying a missing tool. A silent retry loop burns the whole task while looking like work.
|
|
322
|
+
11. **Report progress before you finish when the work is long.** Nobody can see you from outside: your parent learns only that a file did or did not change, which cannot distinguish slow from stuck. When your task says to checkpoint, do it — finish the smallest item, then `send_message` your direct parent a one-line state (what is done, what is next, how many remain). Do the same unprompted if you judge the work will take many steps. Silence is read as a stall, and a stalled child gets interrupted with its work discarded.
|
|
323
|
+
12. **A change outside your workspace is a request, not an action.** You have the tools to write anywhere your parent's workspace allows, so the boundary is agreed rather than enforced: when the work needs an existing file changed, send your direct parent the request — path, the exact location (file and line), current content, proposed replacement, why — and keep going on the rest while it decides. Do the same for anything you would delete or rename. Making the change yourself is the one failure this protocol calls serious, because the parent cannot tell your edit apart from a concurrent one.
|
|
324
|
+
13. **Report artifacts, not activity.** Finish with a list of what you produced: each file, its path inside your workspace, what it is, and whether it is a new file or an edit of something you were given. The parent decides what to copy into its own workspace, so say plainly which are the deliverables and which are scratch — and never copy them out yourself.
|
|
325
|
+
|
|
326
|
+
## When to pass work down
|
|
327
|
+
|
|
328
|
+
A delegation must move the work strictly forward, and an agent that reads its own material is what makes that true. Before delegating, confirm all three:
|
|
329
|
+
|
|
330
|
+
- The subtask is bounded and self-contained, not a restatement of your own task or of the tree's overall goal.
|
|
331
|
+
- Passing it downward buys a real advantage: a wide fan-out, an independent angle, or fresh context that would otherwise consume your own budget.
|
|
332
|
+
- Your own remaining work can continue while it runs.
|
|
333
|
+
|
|
334
|
+
A delegated task is not delegated again: hand a given question to at most one agent, then use the answer. Prefer the narrowest work that is genuinely independent — a single wide search or a single file is yours to read. When no delegation is warranted, do the work yourself. If an agent fails or comes back inconclusive, do not start a chain of retries; report the gap upward together with what you did establish. Reading is always available to you; routing is not a way to avoid reading.
|
|
335
|
+
|
|
336
|
+
## Reporting upward
|
|
337
|
+
|
|
338
|
+
Send the delegating agent what it needs to decide and nothing else: status, the conclusion, supporting evidence with locations, what remains uncertain, and — for anything you changed — exactly which files and what changed in them. Do not open with a preamble and do not close with an offer to do more.
|
|
339
|
+
|
|
340
|
+
If the workspace has its own conventions for recording changes — a change log, a per-directory register, a commit message format — follow them only if your task told you to. Otherwise report what those conventions require alongside your result, so the agent that owns the record can write it: a worker that both changes files and decides how the change is recorded has moved the decision without saying so.
|
|
341
|
+
- id: agent-instructions
|
|
342
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
343
|
+
config:
|
|
344
|
+
maxBytes: 65536
|
|
345
|
+
- id: worker-mask
|
|
346
|
+
name: 'dsh-root-planner-preset/worker-mask'
|
|
347
|
+
- id: tool-bash
|
|
348
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
349
|
+
disabled: !!js process.platform === 'win32'
|
|
350
|
+
- id: tool-pwsh
|
|
351
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
352
|
+
disabled: !!js process.platform !== 'win32'
|
|
353
|
+
# Same process bound as the root preset: the worker gets a shell, and the
|
|
354
|
+
# shell cannot detach anything.
|
|
355
|
+
config:
|
|
356
|
+
enableRunInBackground: false
|
|
357
|
+
- id: tool-fs
|
|
358
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
359
|
+
- id: tool-fs-search
|
|
360
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
361
|
+
config:
|
|
362
|
+
sampleOverCapGlobResults: false
|
|
363
|
+
- id: skill-filesystem
|
|
364
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
365
|
+
- id: tool-skill
|
|
366
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
367
|
+
- id: command-goal
|
|
368
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
369
|
+
- id: tool-goal
|
|
370
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
371
|
+
- id: compaction
|
|
372
|
+
name: cordis:group
|
|
373
|
+
group: true
|
|
374
|
+
isolate:
|
|
375
|
+
compaction: true
|
|
376
|
+
toolResultPruner: true
|
|
377
|
+
config:
|
|
378
|
+
- id: compaction-basic
|
|
379
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
380
|
+
- id: command-compact
|
|
381
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
382
|
+
- id: tool-result-pruner
|
|
383
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
384
|
+
config:
|
|
385
|
+
thresholdChars: 8192
|
|
386
|
+
headChars: 4096
|
|
387
|
+
tailChars: 1024
|
|
388
|
+
# The worker delegates too, and its children are workers as well, so the depth
|
|
389
|
+
# limit stays the only thing that ends the tree.
|
|
390
|
+
- id: delegation
|
|
391
|
+
name: cordis:group
|
|
392
|
+
group: true
|
|
393
|
+
config:
|
|
394
|
+
- id: tool-subagent-control
|
|
395
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
396
|
+
- id: tool-subagent-list-agents
|
|
397
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
398
|
+
- id: tool-subagent
|
|
399
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
400
|
+
config:
|
|
401
|
+
provider: spawn
|
|
402
|
+
toolName: subagent
|
|
403
|
+
modelSelectionSettings: true
|
|
404
|
+
agentPreset: root-planner-worker
|
|
405
|
+
- id: tool-subagent-fork
|
|
406
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
407
|
+
config:
|
|
408
|
+
provider: fork
|
|
409
|
+
toolName: subagent_fork
|
|
410
|
+
agentPreset: root-planner-worker
|
|
411
|
+
- id: tool-ask-user
|
|
412
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
413
|
+
- id: tool-todo
|
|
414
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
415
|
+
config:
|
|
416
|
+
allowParallelInProgress: true
|
|
417
|
+
- id: tool-web
|
|
418
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
419
|
+
config:
|
|
420
|
+
fetch: true
|
|
421
|
+
searchTimeoutMs: 60000
|
|
422
|
+
|
|
293
423
|
# The workspace fence travels in the same package, but as a SIBLING entry rather than a
|
|
294
424
|
# row inside the preset above. A preset row is mounted per agent, and measured against a
|
|
295
425
|
# live session that mount never took effect: six probe steps (write outside, .. escape,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-root-planner-preset",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH agent preset: the root agent plans and decides, subagent workers read, write and run commands inside their own _agents/ workspace, and nothing outlives its task.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./guard.js",
|
|
10
10
|
"./guard": "./guard.js",
|
|
11
|
+
"./worker-mask": "./worker-mask.js",
|
|
11
12
|
"./package.json": "./package.json"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"guard.js",
|
|
16
|
+
"worker-mask.js",
|
|
15
17
|
"persona.txt",
|
|
16
18
|
"child-tools.mjs",
|
|
17
19
|
"build-patch.mjs",
|
package/release.mjs
CHANGED
|
@@ -288,25 +288,40 @@ if (!dryRun && !skipGh) {
|
|
|
288
288
|
const repo = 'XiaoOu2024/dsh-root-planner-preset';
|
|
289
289
|
const sha = git('rev-parse', 'HEAD');
|
|
290
290
|
// `gh run watch <sha>` is wrong: the bare argument is a run ID, so a commit hash
|
|
291
|
-
// returns HTTP 404. Select the run by commit, then watch it by ID
|
|
292
|
-
//
|
|
291
|
+
// returns HTTP 404. Select the run by commit, then watch it by ID.
|
|
292
|
+
//
|
|
293
|
+
// A failed query is reported as a failed query. Treating it as "no run yet" once
|
|
294
|
+
// produced a confident "no CI run registered for b0ca1f8c" while api.github.com was
|
|
295
|
+
// simply unreachable — the run existed and had passed.
|
|
293
296
|
let runId;
|
|
297
|
+
let queryFailure;
|
|
294
298
|
for (let attempt = 1; attempt <= 12 && runId === undefined; attempt += 1) {
|
|
295
|
-
const
|
|
299
|
+
const probe = spawnSync(
|
|
296
300
|
GH,
|
|
297
301
|
['run', 'list', '--repo', repo, '--commit', sha, '--limit', '1', '--json', 'databaseId'],
|
|
298
|
-
{
|
|
302
|
+
{ cwd: here, env, encoding: 'utf8', shell: true },
|
|
299
303
|
);
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
304
|
+
if (probe.status !== 0) {
|
|
305
|
+
const reason = probe.error ? probe.error.code : `exit ${probe.status}`;
|
|
306
|
+
const detail = (probe.stderr ?? '').trim().split('\n').find((line) => line.trim().length > 0) ?? '';
|
|
307
|
+
queryFailure = detail.length > 0 ? `${reason}: ${detail}` : reason;
|
|
308
|
+
} else {
|
|
309
|
+
queryFailure = undefined;
|
|
310
|
+
try {
|
|
311
|
+
const [first] = JSON.parse((probe.stdout ?? '').trim() || '[]');
|
|
312
|
+
if (first?.databaseId !== undefined) runId = String(first.databaseId);
|
|
313
|
+
} catch {
|
|
314
|
+
/* the run has not registered yet */
|
|
315
|
+
}
|
|
305
316
|
}
|
|
306
317
|
if (runId === undefined) await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
307
318
|
}
|
|
308
319
|
if (runId === undefined) {
|
|
309
|
-
process.stdout.write(
|
|
320
|
+
process.stdout.write(
|
|
321
|
+
queryFailure === undefined
|
|
322
|
+
? ` no CI run registered for ${sha.slice(0, 8)} within 2 minutes; check it by hand\n`
|
|
323
|
+
: ` could not query GitHub (${queryFailure}) — CI status unknown, check it by hand\n`,
|
|
324
|
+
);
|
|
310
325
|
} else {
|
|
311
326
|
const waited = run(GH, ['run', 'watch', runId, '--repo', repo, '--exit-status'], { allowFailure: true });
|
|
312
327
|
const tail = waited.trim().split('\n').filter((line) => line.trim().length > 0).slice(-2);
|
|
@@ -53,7 +53,15 @@ const loaderSchema = makeLoaderSchema(yaml);
|
|
|
53
53
|
// 1. The real profile patch layer still parses.
|
|
54
54
|
const patches = yaml.load(readFileSync(PATCH, 'utf8'), { schema: loaderSchema });
|
|
55
55
|
check(Array.isArray(patches), 'profile patch layer is a YAML sequence');
|
|
56
|
-
|
|
56
|
+
// No exact entry count: this file belongs to the user, who adds overrides of their own
|
|
57
|
+
// (a model provider, a plugin toggle) and whose plugin manager writes rows as bundles
|
|
58
|
+
// are installed. An earlier version asserted a fixed count of 6 and failed on DSH
|
|
59
|
+
// 0.2.1 purely because the user had added a provider — a false alarm that said nothing
|
|
60
|
+
// about the preset. What matters is that our override is present and well formed.
|
|
61
|
+
check(
|
|
62
|
+
(patches ?? []).length > 0 && (patches ?? []).every((patch) => typeof patch?.id === 'string'),
|
|
63
|
+
`every entry in the profile patch layer is an id-targeted override (found ${patches?.length})`,
|
|
64
|
+
);
|
|
57
65
|
|
|
58
66
|
const depthPatch = patches.find((patch) => patch.id === 'subagent');
|
|
59
67
|
check(depthPatch !== undefined, 'a subagent override exists');
|
|
@@ -85,12 +93,16 @@ for (const patch of patches) {
|
|
|
85
93
|
const resolved = applied.find((row) => row.id === 'subagent');
|
|
86
94
|
check(resolved?.config?.maxDepth === 2, 'after patching, subagent.maxDepth resolves to 2');
|
|
87
95
|
|
|
88
|
-
// 4.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
// 4. Our override does not disturb the overrides that were already there. Asserted as a
|
|
97
|
+
// SUBSET, not as an exact list: the profile layer is the user's, and new entries appear
|
|
98
|
+
// in it through normal use (a model provider, a plugin toggle written by `dsh plugin`).
|
|
99
|
+
// An earlier version compared the joined id list and failed the moment the user added
|
|
100
|
+
// one — reporting "the five pre-existing overrides are untouched" as false while nothing
|
|
101
|
+
// had touched them.
|
|
102
|
+
const PRE_EXISTING = ['ui-settings-general', 'session-log-deepseek', 'ui-chat', 'agent-default-model', 'locale'];
|
|
103
|
+
const presentIds = new Set((patches ?? []).map((patch) => patch.id));
|
|
104
|
+
const missing = PRE_EXISTING.filter((id) => !presentIds.has(id));
|
|
105
|
+
check(missing.length === 0, `pre-existing overrides are still present (missing: ${missing.join(', ') || 'none'})`);
|
|
94
106
|
|
|
95
107
|
process.stdout.write(failures.length === 0 ? '\nALL CHECKS PASSED\n' : `\n${failures.length} CHECK(S) FAILED\n`);
|
|
96
108
|
process.exit(failures.length === 0 ? 0 : 1);
|
package/verify-patch.mjs
CHANGED
|
@@ -144,6 +144,23 @@ check(existsSync(join(here, 'guard.js')), 'guard.js ships inside the package');
|
|
|
144
144
|
}
|
|
145
145
|
const delegationRows = delegation?.config ?? [];
|
|
146
146
|
|
|
147
|
+
// DSH 0.2.1 removed `toolFilter` from the delegation tool. It does not warn: zod strips
|
|
148
|
+
// the key and the child then runs the platform's DEFAULT preset, which is how a live child
|
|
149
|
+
// ended up with all 39 tools and the wrong persona. The curated child toolset therefore
|
|
150
|
+
// moved to two places at once — a worker preset (which decides the mounted tool plugins)
|
|
151
|
+
// and `worker-mask` (which subtracts the app-level tools a preset cannot omit).
|
|
152
|
+
const workerRow = inserted.find((row) => row.id === 'preset-root-planner-worker');
|
|
153
|
+
check(workerRow !== undefined, 'the patch declares a worker preset');
|
|
154
|
+
check(workerRow?.config?.id === 'root-planner-worker', 'the worker preset id is root-planner-worker');
|
|
155
|
+
check(workerRow?.config?.order === 6, 'the worker preset order is 6');
|
|
156
|
+
const workerPlugins = workerRow?.config?.plugins ?? [];
|
|
157
|
+
check(
|
|
158
|
+
workerPlugins.some((row) => row.id === 'worker-mask' && row.name === 'dsh-root-planner-preset/worker-mask'),
|
|
159
|
+
'the worker preset mounts worker-mask (the tool restriction)',
|
|
160
|
+
);
|
|
161
|
+
const workerDelegation = workerPlugins.find((row) => row.id === 'delegation');
|
|
162
|
+
const workerDelegationRows = workerDelegation?.config ?? [];
|
|
163
|
+
|
|
147
164
|
// Loader row id -> the model-facing tool name that row publishes.
|
|
148
165
|
const childTools = [
|
|
149
166
|
{ id: 'tool-subagent', toolName: 'subagent' },
|
|
@@ -152,43 +169,64 @@ const childTools = [
|
|
|
152
169
|
const row = delegationRows.find((entry) => entry.id === id);
|
|
153
170
|
check(row !== undefined, `delegation declares ${id}`);
|
|
154
171
|
check(row?.config?.toolName === toolName, `${id} publishes the tool name ${toolName}`);
|
|
155
|
-
|
|
172
|
+
check(row?.config?.agentPreset === 'root-planner-worker', `${id} spawns into the worker preset`);
|
|
173
|
+
check(
|
|
174
|
+
row?.config?.toolFilter === undefined,
|
|
175
|
+
`${id} carries no toolFilter (DSH 0.2.1 removed it and strips it silently)`,
|
|
176
|
+
);
|
|
177
|
+
// A grandchild must land in the worker preset too, or the tree degrades at depth 2:
|
|
178
|
+
// the platform default persona and the full tool list come back one level down.
|
|
179
|
+
const nested = workerDelegationRows.find((entry) => entry.id === id);
|
|
180
|
+
check(nested?.config?.agentPreset === 'root-planner-worker', `worker ${id} spawns into the worker preset too`);
|
|
181
|
+
return { id, toolName };
|
|
156
182
|
});
|
|
157
183
|
|
|
158
|
-
// A subagent is a worker:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
check(
|
|
168
|
-
!allow?.includes(scoped),
|
|
169
|
-
`${id} mask excludes the agent-scoped tool "${scoped}" (tools.restrict() would refuse it)`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
184
|
+
// A subagent is a worker: the curated set keeps the write and shell tools.
|
|
185
|
+
for (const worker of ['read', 'write', 'edit', 'ops_replace', 'pwsh']) {
|
|
186
|
+
check(CHILD_TOOLS.includes(worker), `the curated set keeps ${worker} (a subagent must be able to work)`);
|
|
187
|
+
}
|
|
188
|
+
for (const scoped of SCOPE_LOCAL_TOOLS) {
|
|
189
|
+
check(
|
|
190
|
+
!CHILD_TOOLS.includes(scoped),
|
|
191
|
+
`the curated set excludes the agent-scoped tool "${scoped}" (tools.restrict() would refuse it)`,
|
|
192
|
+
);
|
|
172
193
|
}
|
|
173
194
|
|
|
174
|
-
// "No unbounded process": with the shell tools present, the property lives in
|
|
175
|
-
// preset's own rows rather than in the
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
// "No unbounded process": with the shell tools present, the property lives in each
|
|
196
|
+
// preset's own rows rather than in a mask — the worker needs it as much as the root.
|
|
197
|
+
for (const [label, rows] of [
|
|
198
|
+
['the root preset', plugins],
|
|
199
|
+
['the worker preset', workerPlugins],
|
|
200
|
+
]) {
|
|
201
|
+
const row = rows.find((entry) => entry.id === 'tool-pwsh');
|
|
202
|
+
check(row !== undefined, `${label} declares tool-pwsh`);
|
|
203
|
+
check(
|
|
204
|
+
row?.config?.enableRunInBackground === FORBIDDEN_CAPABILITIES.pwshConfig.enableRunInBackground,
|
|
205
|
+
`${label} sets enableRunInBackground=${FORBIDDEN_CAPABILITIES.pwshConfig.enableRunInBackground} ` +
|
|
206
|
+
`(found ${JSON.stringify(row?.config?.enableRunInBackground)}) — without it a subagent could detach a server`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
183
209
|
|
|
184
210
|
/**
|
|
185
211
|
* The global tool inventory of the author's profile, captured from a runtime
|
|
186
212
|
* error rather than hand-kept: `tools.restrict()` prints every known global tool
|
|
187
213
|
* when it refuses a name. Replace this list with the equivalent line from your
|
|
188
214
|
* own environment — it is what makes the check below meaningful.
|
|
215
|
+
*
|
|
216
|
+
* Provenance: transcribed on DSH 0.2.1-rc.2. The upgrade from 0.2.0-rc.2 added
|
|
217
|
+
* `working_directory`, the four `schedule_*` tools, `cordis_inspect_list`,
|
|
218
|
+
* `cordis_inspect_query` and `plugin_manager`; all are registered by profile-level tool
|
|
219
|
+
* packages, so they are global. None of them is in the child mask, which is the intended
|
|
220
|
+
* behaviour of an allow list — a new tool is invisible to children until it is named.
|
|
221
|
+
*
|
|
222
|
+
* Regenerate after a DSH upgrade: break the mask on purpose (add a bogus name), let the
|
|
223
|
+
* agent-creation error print the list, and transcribe it — `tools/extract-tool-inventory.mjs`
|
|
224
|
+
* reads exactly that line out of a session log.
|
|
189
225
|
*/
|
|
190
226
|
const OBSERVED_GLOBAL_TOOLS = new Set([
|
|
191
227
|
'ask_user_question',
|
|
228
|
+
'cordis_inspect_list',
|
|
229
|
+
'cordis_inspect_query',
|
|
192
230
|
'create_goal',
|
|
193
231
|
'edit',
|
|
194
232
|
'exit_plan_mode',
|
|
@@ -204,10 +242,15 @@ const OBSERVED_GLOBAL_TOOLS = new Set([
|
|
|
204
242
|
'ops_grep',
|
|
205
243
|
'ops_inspect_local_file',
|
|
206
244
|
'ops_replace',
|
|
245
|
+
'plugin_manager',
|
|
207
246
|
'present',
|
|
208
247
|
'pwsh',
|
|
209
248
|
'read',
|
|
210
249
|
'read_image',
|
|
250
|
+
'schedule_create',
|
|
251
|
+
'schedule_delete',
|
|
252
|
+
'schedule_list',
|
|
253
|
+
'schedule_update',
|
|
211
254
|
'send_message',
|
|
212
255
|
'skill',
|
|
213
256
|
'subagent_fork',
|
|
@@ -216,30 +259,30 @@ const OBSERVED_GLOBAL_TOOLS = new Set([
|
|
|
216
259
|
'web_fetch',
|
|
217
260
|
'web_search',
|
|
218
261
|
'workflow',
|
|
262
|
+
'working_directory',
|
|
219
263
|
'write',
|
|
220
264
|
]);
|
|
221
265
|
|
|
222
266
|
/**
|
|
223
|
-
* Tools that are agent-scoped rather than global, so
|
|
224
|
-
* them. `subagent` is the one this bundle ran into: with
|
|
225
|
-
*
|
|
226
|
-
*
|
|
267
|
+
* Tools that are agent-scoped rather than global, so `tools.restrict()` can never name
|
|
268
|
+
* them. `subagent` is the one this bundle ran into: with `modelSelectionSettings: true`
|
|
269
|
+
* it is mounted inside each composed Agent's own scope, and a restriction accepts global
|
|
270
|
+
* names only. The curated set is the source of truth now — the old per-call mask is gone.
|
|
227
271
|
*/
|
|
228
|
-
|
|
229
|
-
const unknown =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
`${id} mask names only tools the runtime reports as global` +
|
|
233
|
-
(unknown.length ? ` (not global: ${unknown.join(', ')})` : ''),
|
|
234
|
-
);
|
|
235
|
-
const missing = CHILD_TOOLS.filter((name) => !(allow ?? []).includes(name));
|
|
236
|
-
const extra = (allow ?? []).filter((name) => !CHILD_TOOLS.includes(name));
|
|
272
|
+
{
|
|
273
|
+
const unknown = CHILD_TOOLS.filter((name) => !OBSERVED_GLOBAL_TOOLS.has(name));
|
|
274
|
+
const scoped = new Set(SCOPE_LOCAL_TOOLS);
|
|
275
|
+
const unexpected = unknown.filter((name) => !scoped.has(name));
|
|
237
276
|
check(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
(
|
|
241
|
-
(extra.length ? ` — unexpected: ${extra.join(', ')}` : ''),
|
|
277
|
+
unexpected.length === 0,
|
|
278
|
+
`the curated set names only tools the runtime reports as global` +
|
|
279
|
+
(unexpected.length ? ` (not global: ${unexpected.join(', ')})` : ''),
|
|
242
280
|
);
|
|
281
|
+
// `subagent` reaches a worker through the preset's own delegation row rather than the
|
|
282
|
+
// restriction, so it may legitimately sit in the curated set while being scope-local.
|
|
283
|
+
for (const name of unknown) {
|
|
284
|
+
check(scoped.has(name), `the only scope-local name in the curated set is declared as such (${name})`);
|
|
285
|
+
}
|
|
243
286
|
}
|
|
244
287
|
|
|
245
288
|
process.stdout.write(failures.length === 0 ? '\nALL CHECKS PASSED\n' : `\n${failures.length} CHECK(S) FAILED\n`);
|
package/worker-mask.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Restricts a Root Planner worker to the curated child toolset.
|
|
2
|
+
//
|
|
3
|
+
// DSH 0.2.1 removed `toolFilter` from `dsh-tool-subagent` — its error says to "configure
|
|
4
|
+
// persona and tool plugins in an Agent Preset and select it with agentPreset (spawn
|
|
5
|
+
// only)". A preset decides which tool plugins are mounted for that agent, but not which
|
|
6
|
+
// globally mounted tools it may call: several newer tools (tool-cordis, plugin-manager,
|
|
7
|
+
// working-directory, schedule) are registered at the app level and would reach every
|
|
8
|
+
// agent regardless of the preset's rows. `tools.restrict()` is the mechanism that still
|
|
9
|
+
// subtracts them, and this module is that call, mounted as a row inside the worker
|
|
10
|
+
// preset so it applies to the worker's own agent scope.
|
|
11
|
+
//
|
|
12
|
+
// The allow list lives in `child-tools.mjs` — the same file the previous mask used, so
|
|
13
|
+
// the curated set is still declared once.
|
|
14
|
+
|
|
15
|
+
import { CHILD_TOOLS } from './child-tools.mjs'
|
|
16
|
+
|
|
17
|
+
export const name = 'root-planner-worker-mask'
|
|
18
|
+
|
|
19
|
+
export const inject = ['tools']
|
|
20
|
+
|
|
21
|
+
export function apply(ctx) {
|
|
22
|
+
// `tools.restrict()` requires a scoped context: it refuses to run from a global one
|
|
23
|
+
// because that would mask every agent in the process. A preset row is mounted into the
|
|
24
|
+
// agent's own scope, which is exactly the scope this needs.
|
|
25
|
+
ctx.effect(() => {
|
|
26
|
+
const tools = typeof ctx.get === 'function' ? ctx.get('tools') : ctx.tools
|
|
27
|
+
if (tools === undefined) return () => {}
|
|
28
|
+
return tools.restrict({ allow: CHILD_TOOLS })
|
|
29
|
+
})
|
|
30
|
+
}
|