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 +8 -1
- package/README.md +22 -6
- package/README.zh.md +11 -2
- package/build-patch.mjs +15 -11
- package/cordis.patch.yml +15 -11
- package/guard.js +10 -1
- package/package.json +1 -1
- package/verify-patch.mjs +13 -4
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
|
@@ -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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
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.
|
|
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.
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
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'));
|