dsh-root-planner-preset 1.3.0 → 1.3.2

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 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
- > 守卫以**预设行**形式挂载,所以只有用 `root-planner` 预设的 agent 才受约束:`_agents/` 这套约定只有这个预设的人设定义过。
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 as its own
40
- plugin row (`dsh-root-planner-preset/guard`), so one install carries the protocol and its
41
- enforcement together. It refuses a `write`, `edit` or `ops_replace` whose target resolves
42
- outside `<workspace>/_agents/`, and refuses a shell call whose `workdir` is outside it or
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. Confining a child's shell properly would need a narrower
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
- **这条边界是强制执行的,不只是约定。** 预设自带围栏(自己的插件行 `dsh-root-planner-preset/guard`),一次安装同时得到协议和它的执行。它拒绝目标落在 `<工作区>/_agents/` 之外的 `write`、`edit`、`ops_replace`;也拒绝 `workdir` 在工作区之外的 shell 调用,以及命令里写着你文件的绝对路径:
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
- **有一个限制必须知道**:shell 命令能在运行时拼出路径(`$env:X` 拼 `'..'`、脚本内部写、Here-String),参数扫描看不见。要真正关住子代理的 shell,需要给它一个更窄的沙箱写域,而 DSH 给不了——`sandboxPolicy.resolve()` 只接受 `{ session, mode }`,`workspaceRoot` 恒等于不可变、从父会话继承的 `session.header.cwd`。所以父 agent 的核验仍然是兜底:
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
@@ -226,17 +226,21 @@ const patch = `# Generated by build-patch.mjs from persona.txt. Do not hand-edit
226
226
  - id: tool-plugin-manager
227
227
  name: '@deepseek-ai/dsh-plugin-manager/tools'
228
228
  disabled: true
229
- # The workspace fence. Mounted as a preset row so one install carries the
230
- # protocol and its enforcement together: the persona tells a child to write
231
- # under its own _agents/ directory, and this refuses anything else before
232
- # the call runs. It is preset-scoped rather than a separate profile bundle
233
- # because the _agents/ convention only exists here — a guard without this
234
- # persona would fence agents that were never told where they may write.
235
- - id: root-planner-guard
236
- name: 'dsh-root-planner-preset/guard'
237
- config:
238
- directoryName: _agents
239
- guardShell: true
229
+
230
+ # The workspace fence travels in the same package, but as a SIBLING entry rather than a
231
+ # row inside the preset above. A preset row is mounted per agent, and measured against a
232
+ # live session that mount never took effect: six probe steps (write outside, .. escape,
233
+ # shell without workdir, shell absolute path, plus the two allowed controls) all came
234
+ # back allowed. As a profile-level entry — the shape the standalone guard bundle was
235
+ # proved in — one instance sees every call and decides from the calling agent's session
236
+ # header, which is all this guard needs. Shipping it here still means one install carries
237
+ # the protocol and its enforcement together.
238
+ - insert:
239
+ - id: root-planner-guard
240
+ name: 'dsh-root-planner-preset/guard'
241
+ config:
242
+ directoryName: _agents
243
+ guardShell: true
240
244
  `;
241
245
 
242
246
  // Substitution happens after the template so the emitted indentation is owned by
package/cordis.patch.yml CHANGED
@@ -289,14 +289,18 @@
289
289
  - id: tool-plugin-manager
290
290
  name: '@deepseek-ai/dsh-plugin-manager/tools'
291
291
  disabled: true
292
- # The workspace fence. Mounted as a preset row so one install carries the
293
- # protocol and its enforcement together: the persona tells a child to write
294
- # under its own _agents/ directory, and this refuses anything else before
295
- # the call runs. It is preset-scoped rather than a separate profile bundle
296
- # because the _agents/ convention only exists here — a guard without this
297
- # persona would fence agents that were never told where they may write.
298
- - id: root-planner-guard
299
- name: 'dsh-root-planner-preset/guard'
300
- config:
301
- directoryName: _agents
302
- guardShell: true
292
+
293
+ # The workspace fence travels in the same package, but as a SIBLING entry rather than a
294
+ # row inside the preset above. A preset row is mounted per agent, and measured against a
295
+ # live session that mount never took effect: six probe steps (write outside, .. escape,
296
+ # shell without workdir, shell absolute path, plus the two allowed controls) all came
297
+ # back allowed. As a profile-level entry — the shape the standalone guard bundle was
298
+ # proved in — one instance sees every call and decides from the calling agent's session
299
+ # header, which is all this guard needs. Shipping it here still means one install carries
300
+ # the protocol and its enforcement together.
301
+ - insert:
302
+ - id: root-planner-guard
303
+ name: 'dsh-root-planner-preset/guard'
304
+ config:
305
+ directoryName: _agents
306
+ guardShell: true
package/guard.js CHANGED
@@ -170,8 +170,17 @@ export function apply(ctx, config = {}) {
170
170
  // Registered on the plugin's own context: a plain-context guard applies globally,
171
171
  // and the decision is made per call from `exec.agent`, so no per-agent wiring is
172
172
  // needed and no registration leaks when an agent is disposed.
173
+ //
174
+ // The service is read with `ctx.get` rather than `ctx.tools` on purpose: a missing
175
+ // service should leave the guard absent and silent, not throw inside the effect where
176
+ // the failure would look like a plugin that simply does nothing.
173
177
  ctx.effect(() => {
174
- const dispose = ctx.tools.guard((exec) => {
178
+ const tools = typeof ctx.get === 'function' ? ctx.get('tools') : ctx.tools
179
+ if (tools === undefined) {
180
+ trace('no tools service — guard not registered')
181
+ return () => {}
182
+ }
183
+ const dispose = tools.guard((exec) => {
175
184
  const header = exec?.agent?.session?.header
176
185
  // Only delegated sessions are fenced: the root owns the workspace and must be
177
186
  // able to write anywhere in it, or it could not integrate a child's artifacts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-root-planner-preset",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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",
package/verify-patch.mjs CHANGED
@@ -116,10 +116,15 @@ const delegation = plugins.find((row) => row.id === 'delegation');
116
116
  check(delegation?.group === true, 'delegation is a group row');
117
117
 
118
118
  // The workspace fence travels with the preset, so one install carries the protocol and
119
- // its enforcement. These assertions keep that promise honest: the row must exist, must
120
- // resolve to this package's own module, and must ship that module.
121
- const guardRow = plugins.find((row) => row.id === 'root-planner-guard');
122
- check(guardRow !== undefined, 'the preset mounts the workspace guard as its own row');
119
+ // its enforcement. It must be a TOP-LEVEL insert, not a row inside `config.plugins`.
120
+ //
121
+ // That distinction is measured, not stylistic: while this guard was declared as a preset
122
+ // row, a live six-step probe came back all-allowed — the row exists in the composed
123
+ // config yet registers nothing that sees a tool call. As a sibling entry (the shape the
124
+ // standalone guard bundle was proved in) one instance sees every call. The last check
125
+ // here keeps a future refactor from quietly moving it back.
126
+ const guardRow = inserted.find((row) => row.id === 'root-planner-guard');
127
+ check(guardRow !== undefined, 'the patch inserts the workspace guard as a top-level entry');
123
128
  check(
124
129
  guardRow?.name === 'dsh-root-planner-preset/guard',
125
130
  `the guard row resolves to this package's subpath (found ${JSON.stringify(guardRow?.name)})`,
@@ -127,6 +132,10 @@ check(
127
132
  check(guardRow?.disabled !== true, 'the guard row is enabled');
128
133
  check(guardRow?.config?.directoryName === '_agents', 'the guard fences the _agents/ directory');
129
134
  check(guardRow?.config?.guardShell === true, 'the guard also applies the shell rules');
135
+ check(
136
+ plugins.every((row) => row.id !== 'root-planner-guard'),
137
+ 'the guard is NOT declared as a preset plugin row (that mount registers nothing)',
138
+ );
130
139
  check(existsSync(join(here, 'guard.js')), 'guard.js ships inside the package');
131
140
  {
132
141
  const manifest = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'));