@joekytc/dsh-swarm 0.2.0 → 0.3.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 +176 -139
- package/README.zh-CN.md +172 -125
- package/client/ConfigSection.tsx +96 -5
- package/client/ConfigSelect.tsx +3 -1
- package/client/TaskDrawer.tsx +2 -0
- package/client/config-store.ts +48 -7
- package/client/kanban.css +69 -0
- package/client/timeline-model.ts +2 -0
- package/lib/client.js +172 -14
- package/lib/config.d.ts +18 -1
- package/lib/config.js +14 -0
- package/lib/dispatcher/agent-runner.d.ts +13 -5
- package/lib/dispatcher/agent-runner.js +78 -38
- package/lib/dispatcher/chain-auditor.d.ts +4 -4
- package/lib/dispatcher/chain-auditor.js +10 -8
- package/lib/dispatcher/dispatcher.d.ts +18 -18
- package/lib/dispatcher/dispatcher.js +59 -43
- package/lib/dispatcher/event-waker.d.ts +1 -1
- package/lib/dispatcher/event-waker.js +2 -2
- package/lib/dispatcher/git-credentials.d.ts +2 -2
- package/lib/dispatcher/git-credentials.js +2 -2
- package/lib/dispatcher/model-candidates.d.ts +1 -1
- package/lib/dispatcher/model-candidates.js +1 -1
- package/lib/dispatcher/session-events.d.ts +1 -1
- package/lib/dispatcher/session-events.js +1 -1
- package/lib/dispatcher/target-repo.d.ts +2 -2
- package/lib/dispatcher/target-repo.js +3 -3
- package/lib/dispatcher/v-orchestrator.d.ts +8 -8
- package/lib/dispatcher/v-orchestrator.js +41 -41
- package/lib/dispatcher/watchdog.d.ts +1 -1
- package/lib/dispatcher/watchdog.js +2 -2
- package/lib/domain/config-override.d.ts +14 -0
- package/lib/domain/config-override.js +38 -1
- package/lib/domain/delivery-contract.d.ts +2 -2
- package/lib/domain/delivery-contract.js +5 -5
- package/lib/domain/delivery-evidence.d.ts +1 -1
- package/lib/domain/delivery-evidence.js +1 -1
- package/lib/domain/event-store.js +1 -1
- package/lib/domain/im-message.d.ts +15 -0
- package/lib/domain/im-message.js +149 -0
- package/lib/domain/kanban-service.d.ts +13 -10
- package/lib/domain/kanban-service.js +36 -28
- package/lib/domain/memory.js +1 -1
- package/lib/domain/ocr-review.d.ts +47 -0
- package/lib/domain/ocr-review.js +105 -0
- package/lib/domain/permissions.js +5 -5
- package/lib/domain/projection.js +6 -6
- package/lib/domain/state-machine.js +1 -1
- package/lib/domain/task-parents.js +1 -1
- package/lib/domain/types.d.ts +4 -4
- package/lib/index.js +14 -9
- package/lib/roles/clean-fs-tools.js +3 -4
- package/lib/roles/preset-installer.js +3 -3
- package/lib/roles/skill-installer.js +1 -1
- package/lib/roles/toolsets.d.ts +60 -16
- package/lib/roles/toolsets.js +265 -47
- package/lib/roles/wiki-worker.js +3 -3
- package/lib/routes/kanban-http.d.ts +25 -2
- package/lib/routes/kanban-http.js +171 -11
- package/lib/routes/kanban-sse.d.ts +1 -1
- package/lib/routes/kanban-sse.js +1 -1
- package/lib/routes/planning-driver.d.ts +4 -2
- package/lib/routes/planning-driver.js +7 -3
- package/lib/routes/prefix-router.d.ts +3 -3
- package/lib/routes/prefix-router.js +3 -1
- package/lib/services/config-provider.d.ts +0 -4
- package/lib/services/config-provider.js +7 -9
- package/lib/services/im-delivery.d.ts +94 -0
- package/lib/services/im-delivery.js +263 -0
- package/lib/services/kanban-provider.d.ts +1 -1
- package/lib/services/kanban-provider.js +2 -2
- package/lib/services/ocr-cli.d.ts +35 -0
- package/lib/services/ocr-cli.js +128 -0
- package/lib/tools/kanban-tools.d.ts +2 -2
- package/lib/tools/kanban-tools.js +1 -1
- package/lib/tools/main-session-tools.d.ts +6 -2
- package/lib/tools/main-session-tools.js +58 -20
- package/lib/tools/ocr-review-tools.d.ts +11 -0
- package/lib/tools/ocr-review-tools.js +63 -0
- package/lib/tools/planning-tools.d.ts +5 -2
- package/lib/tools/planning-tools.js +11 -9
- package/lib/tools/wiki-tools.js +2 -2
- package/lib/wiki/kb-linkage.d.ts +1 -1
- package/lib/wiki/local-kb-client.js +3 -3
- package/lib/wiki/local-kb.d.ts +1 -1
- package/lib/wiki/local-kb.js +2 -2
- package/lib/wiki/page-path.d.ts +1 -1
- package/lib/wiki/page-path.js +6 -3
- package/lib/wiki/wiki-vault-client.d.ts +1 -1
- package/lib/wiki/wiki-vault-client.js +1 -1
- package/package.json +1 -1
- package/personas/kanban-d/agent.cordis.yml +5 -5
- package/personas/kanban-dt/agent.cordis.yml +23 -5
- package/personas/kanban-p/agent.cordis.yml +3 -3
- package/personas/kanban-pt/agent.cordis.yml +1 -1
- package/personas/kanban-v/agent.cordis.yml +3 -3
- package/personas/kanban-w/agent.cordis.yml +5 -5
- package/personas/persona-d.md +1 -1
- package/personas/persona-dt.md +12 -4
- package/personas/persona-p.md +1 -1
- package/personas/persona-v.md +2 -2
- package/personas/swarm/agent.cordis.yml +43 -0
- package/personas/swarm/preset.yml +5 -0
package/lib/roles/toolsets.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { KanbanService } from '../domain/kanban-service.js';
|
|
3
|
-
import {
|
|
3
|
+
import { WikiVaultClient } from '../wiki/wiki-vault-client.js';
|
|
4
|
+
import { isInsideKbRoot, ensureLocalKbRoot } from '../wiki/local-kb.js';
|
|
5
|
+
import { LocalWikiClient } from '../wiki/local-kb-client.js';
|
|
6
|
+
import { isStandaloneReviewNamespacePath } from '../domain/ocr-review.js';
|
|
7
|
+
import { isRoleComposed } from '../dispatcher/agent-runner.js';
|
|
8
|
+
import { ESCALATION_PARAM_KEYS } from './clean-fs-tools.js';
|
|
9
|
+
export { isRoleComposed };
|
|
4
10
|
import { buildKanbanTools } from '../tools/kanban-tools.js';
|
|
5
11
|
import { buildSpecCardTools } from '../tools/spec-card-tools.js';
|
|
6
12
|
import { buildWikiTools } from '../tools/wiki-tools.js';
|
|
7
13
|
import { buildPrefetchTools } from '../tools/prefetch-tools.js';
|
|
14
|
+
import { buildOcrReviewTool } from '../tools/ocr-review-tools.js';
|
|
8
15
|
import { WikiWorker } from './wiki-worker.js';
|
|
9
16
|
/** 直接写工具(无条件视为写能力;只读工具如 read/glob/grep 不算)。 */
|
|
10
17
|
const DIRECT_WRITE_TOOLS = new Set(['write', 'edit', 'rm', 'mv', 'cp', 'mkdir', 'mkfile']);
|
|
11
18
|
/** bash/run_code 命令中的写操作标记(写证据启发式;ls/cat/grep/git show 等只读不算)。
|
|
12
|
-
* 与 chain-auditor 同源;重定向标记用 \s>>?(要求 > 前有空白),避免 2>/dev/null 只读重定向误判。
|
|
13
|
-
|
|
19
|
+
* 与 chain-auditor 同源;重定向标记用 \s>>?(要求 > 前有空白),避免 2>/dev/null 只读重定向误判。
|
|
20
|
+
* git 细化(2026-09-08):merge(?!-base) 防 merge-base 查询被误判;tag 只认带非 -l/-n 参数的
|
|
21
|
+
* 变更形态(tag -l/-n/裸 为列表查询),两份正则(toolsets/chain-auditor)改动须同步。 */
|
|
22
|
+
const BASH_WRITE_RE = /(?:\b(?:touch|mkdir|rm|rmdir|mv|cp|tee|truncate|install|ln|dd|chmod|chown|make|cmake)\b|\bgit\s+(?:-C\s+\S+\s+)*(?:add|commit|push|mv|rm|checkout\s+-b|switch\s+-c|worktree\s+add|merge(?!-base)|rebase|reset|clean|restore|tag\s+(?!-l\b|-n\b)\S+|remote\s+add|apply)\b|\bpnpm\s+(?:add|install|remove|update|link)\b|\bnpm\s+(?:i|install|add|remove|uninstall|update)\b|\byarn\s+(?:add|remove)\b|\bbun\s+(?:add|install|remove)\b|\bsed\s+-i\b|\bperl\s+-i\b|\s>>?)/i;
|
|
14
23
|
/** run_code(JS/TS/Python 程序)中的写操作标记:文件写 API / 命令派发写工具。
|
|
15
|
-
* 含 Python 写标记(
|
|
24
|
+
* 含 Python 写标记(DT run_code 盲区闭环):open() 写模式精确版('w'/'a'/'w+'/'a+' 及
|
|
16
25
|
* wb/ab 等变体,读模式 'r' 不命中)、os. 模块写、pathlib Path 写、shutil 复制/移动/删除。 */
|
|
17
26
|
const CODE_WRITE_RE = /(?:\b(?:writeFileSync|writeFile|appendFileSync|appendFile|createWriteStream|unlinkSync|unlink|rmSync|rm|mkdirSync|mkdir|cpSync|renameSync)\b|\bwriteFile\(|\bfs\s*\.\s*(?:write|append|createWrite)|\bopen\(\s*['"][^'"\n]*['"]\s*,\s*(?:(?:mode|encoding|errors|buffering|newline|closefd|opener|text)\s*=\s*)?['"][wa][^'"\n]*['"]|\bos\s*\.\s*(?:remove|unlink|write|rmdir|makedirs|rename)\b|\.(?:write_text|write_bytes|unlink|mkdir|rename)\(|shutil\s*\.\s*(?:copy|move|rmtree))/i;
|
|
18
27
|
/** git 只读动词白名单:命中则 git 命令放行;其余 git 动词(含 checkout/branch/stash/merge/commit/push 等)一律拒绝。
|
|
@@ -33,10 +42,10 @@ const GUARD_WRITE_INTENT_RE = /(?:\b(?:touch|mkdir|rm|rmdir|mv|cp|tee|truncate|i
|
|
|
33
42
|
* 误判致 P 会话 openspec 裸 edit 随机被拒(15 次误报实证,2026-09-03);
|
|
34
43
|
* 2) GUARD 写动词(touch/mkdir/rm/…)与解释器 -c/-e、原地编辑器 -i 移除——真文件写由
|
|
35
44
|
* CODE_WRITE_RE 补回覆盖(writeFile/mkdir/unlink/open 写模式/os./pathlib/shutil),
|
|
36
|
-
* 内嵌 shell 裸动词由 CODE_SHELL_VERB_RE
|
|
45
|
+
* 内嵌 shell 裸动词由 CODE_SHELL_VERB_RE 补回覆盖,内嵌 shell 重定向由
|
|
37
46
|
* 本正则覆盖。三层分工与 accepted-risk 见 CODE_SHELL_VERB_RE 注释。 */
|
|
38
47
|
const CODE_REDIRECT_WRITE_RE = /\s>>?/;
|
|
39
|
-
/** run_code 内嵌 shell
|
|
48
|
+
/** run_code 内嵌 shell 裸动词写意图:CODE_WRITE_RE 只含文件写
|
|
40
49
|
* API,不含裸 shell 动词——run_code 字符串派发的 shell 命令(`child_process.exec('cp /tmp/x
|
|
41
50
|
* src/foo.ts')`)会绕过写护栏。词表对齐 GUARD_WRITE_INTENT_RE 动词清单中 CODE_WRITE_RE 未覆盖
|
|
42
51
|
* 的部分(rm/mkdir 已由 CODE_WRITE_RE 覆盖不重复;touch/rmdir/make/cmake 按评审定版不纳入)。
|
|
@@ -49,7 +58,7 @@ const CODE_REDIRECT_WRITE_RE = /\s>>?/;
|
|
|
49
58
|
* run_code 带空格比较 `a > b` 且同 code 含 openspec/changes 路径时仍可能误拒(残余面小,
|
|
50
59
|
* 失败可恢复——模型改用 write 工具重试即可)。 */
|
|
51
60
|
const CODE_SHELL_VERB_RE = /\b(?:cp|mv|tee|dd|chmod|chown|ln|install|truncate)\b/i;
|
|
52
|
-
/** 剥去 shell 重定向目标 token 外壳的一层引号(' "
|
|
61
|
+
/** 剥去 shell 重定向目标 token 外壳的一层引号(' " `)(修复引号包裹 plan 路径的误拒)。
|
|
53
62
|
* 仅剥对称外壳一层;剥完仍走 resolve+isPlanPath,.. / 绝对路径逃逸不被削弱。 */
|
|
54
63
|
function stripShellQuotes(tok) {
|
|
55
64
|
const q = tok[0];
|
|
@@ -58,7 +67,7 @@ function stripShellQuotes(tok) {
|
|
|
58
67
|
}
|
|
59
68
|
return tok;
|
|
60
69
|
}
|
|
61
|
-
/**
|
|
70
|
+
/** 从 bash/run_code 写意图命令提取实际写目标路径。
|
|
62
71
|
* redirectRe 按入口分流:bash 传 BASH_REDIRECT_TARGET_RE(无空格形态),run_code 传
|
|
63
72
|
* CODE_REDIRECT_TARGET_RE(带空格形态)——两形态各自命名,禁止共享开关参数。
|
|
64
73
|
* 另提取 writeFileSync(/appendFileSync( 首个字符串实参;提取不到返回空数组(调用方据此
|
|
@@ -75,8 +84,8 @@ function extractWriteTargets(cmd, redirectRe) {
|
|
|
75
84
|
if (m[2])
|
|
76
85
|
out.push(m[2]);
|
|
77
86
|
}
|
|
78
|
-
//
|
|
79
|
-
// `mkdir -p <kb>/x /repo/y` 漏检第二个目标 →
|
|
87
|
+
// 双模式:动词目标提取——必须捕获动词后【全部】路径实参(只取首个会让
|
|
88
|
+
// `mkdir -p <kb>/x /repo/y` 漏检第二个目标 → 护栏越权放行)。
|
|
80
89
|
// cp/mv/rm 的源+目标全部入列:任一在库根外即整体拒绝(fail-closed,accepted-risk:
|
|
81
90
|
// 库根内合法 cp/mv 改用 write 工具完成)。
|
|
82
91
|
const verbRe = /\b(?:touch|mkdir|tee|cp|mv|rm|install)\b([^;&|<>]*)/g;
|
|
@@ -107,18 +116,6 @@ export function isReviewNamespacePath(pagePath, chainId) {
|
|
|
107
116
|
const chain = chainId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
108
117
|
return new RegExp(`^projects\\/[a-z0-9-]+\\/${chain}\\/review\\/`).test(p);
|
|
109
118
|
}
|
|
110
|
-
/**
|
|
111
|
-
* 评审引擎决策(DT):ocr(open-code-review Delegation 模式)优先;
|
|
112
|
-
* 不可用 fallback superpowers code-review;两者都不可用 → review-tool-unavailable(阻塞)。
|
|
113
|
-
* 纯函数便于单测;真实可用性探测在 agent-runner 装配(探活 ocr 二进制/失败)。
|
|
114
|
-
*/
|
|
115
|
-
export function resolveReviewEngine(available) {
|
|
116
|
-
if (available.ocr)
|
|
117
|
-
return 'ocr';
|
|
118
|
-
if (available.codeReview)
|
|
119
|
-
return 'code-review';
|
|
120
|
-
return 'review-tool-unavailable';
|
|
121
|
-
}
|
|
122
119
|
/**
|
|
123
120
|
* DT 写护栏 = PT 只读护栏(源码/git/写标记 bash 拒绝)+ wiki_write 仅 review namespace 收窄。
|
|
124
121
|
* repoRoot 为 D 目标仓库;chainId 用于 wiki 评审命名空间校验。
|
|
@@ -140,7 +137,7 @@ export function buildReadOnlyWriteGuard(_repoRoot) {
|
|
|
140
137
|
return (execution) => {
|
|
141
138
|
const name = String(execution?.name ?? '');
|
|
142
139
|
const args = execution?.arguments ?? {};
|
|
143
|
-
//
|
|
140
|
+
// 全名拦截——只读会话(W/PT/DT)fs 写无条件拒绝,不再依赖 repoRoot 子串 / hitsRepo。
|
|
144
141
|
// W 是 danger-full-access(无 workspace-write sandbox 兜底),可写 repo 外任意路径(~/x、/tmp/x、
|
|
145
142
|
// 其他项目源码),故必须工具级全名拦截封死。非写工具(read/glob/grep/wiki_* 等)照常放行。
|
|
146
143
|
if (DIRECT_WRITE_TOOLS.has(name))
|
|
@@ -162,10 +159,10 @@ export function buildReadOnlyWriteGuard(_repoRoot) {
|
|
|
162
159
|
return undefined;
|
|
163
160
|
};
|
|
164
161
|
}
|
|
165
|
-
/** 本地模式 KB
|
|
162
|
+
/** 本地模式 KB 写护栏:全名只读拦截(base)之上,仅对「实际写目标全部为
|
|
166
163
|
* 库根内绝对路径」的写操作豁免。目标提取不到 / 相对路径 / 越界 → 维持 base 拒绝(fail-closed)。
|
|
167
164
|
* 仅 local 模式对 W/DT 装配;remote 模式仍用 buildReadOnlyWriteGuard(库根写也被拒)。
|
|
168
|
-
*
|
|
165
|
+
* 审查修订:extractWriteTargets 保持既有双参签名(cmd, redirectRe),按入口分流
|
|
169
166
|
* 传 BASH_REDIRECT_TARGET_RE / CODE_REDIRECT_TARGET_RE(与 buildPlanWriteGuard 同款)——
|
|
170
167
|
* 单参调用会在首个写意图命令上 TypeError。 */
|
|
171
168
|
export function buildKbWriteGuard(kbRoot) {
|
|
@@ -188,7 +185,7 @@ export function buildKbWriteGuard(kbRoot) {
|
|
|
188
185
|
const cmd = String(a['command'] ?? a['code'] ?? '');
|
|
189
186
|
if (!cmd)
|
|
190
187
|
return baseReason;
|
|
191
|
-
// 双参签名 + 按入口分流 redirect
|
|
188
|
+
// 双参签名 + 按入口分流 redirect 正则(审查修订);targets 为空 = 写意图但提取不到目标 → fail-closed 拒绝
|
|
192
189
|
const targets = extractWriteTargets(cmd, name === 'bash' ? BASH_REDIRECT_TARGET_RE : CODE_REDIRECT_TARGET_RE);
|
|
193
190
|
if (targets.length > 0 && targets.every(isKbTarget))
|
|
194
191
|
return undefined;
|
|
@@ -197,23 +194,23 @@ export function buildKbWriteGuard(kbRoot) {
|
|
|
197
194
|
return baseReason;
|
|
198
195
|
};
|
|
199
196
|
}
|
|
200
|
-
/** P
|
|
197
|
+
/** P 专用写护栏:读全放行;git mutation 一律拒绝;写仅允许目标仓库 openspec/changes 目录。
|
|
201
198
|
* 直接 fs 写工具 → 路径经 resolve 归一化后须落在 <workspaceRoot>/openspec/changes/ 之下(相邻段对判定);
|
|
202
199
|
* bash/run_code 写标记命令 → 命令文本须含 `openspec/changes` 子串,且提取出的实际写目标(重定向
|
|
203
|
-
* 目标 / writeFileSync 实参)逐条经 resolve+isPlanPath
|
|
200
|
+
* 目标 / writeFileSync 实参)逐条经 resolve+isPlanPath 校验(杀 openspec/changes/../.. 穿越写源码)。
|
|
204
201
|
* 源码/src/lib/tests 等写不入(不含该子串)——"禁止改动源码"为工具级硬约束,非 prompt 软约束。
|
|
205
202
|
* execution 以 dsh-tools 形态 { name, arguments } 传入(与 buildReadOnlyWriteGuard 一致)。 */
|
|
206
203
|
export function buildPlanWriteGuard(workspaceRoot) {
|
|
207
204
|
const wsRoot = workspaceRoot.replace(/\/+$/, '');
|
|
208
205
|
/** 目标路径须解析后落在 wsRoot 之下、且含相邻 openspec→changes 段对。
|
|
209
|
-
* resolve 折叠 ../ 与重复斜杠(杀
|
|
210
|
-
* 相对路径(openspec/changes/x.md)经 resolve 归到 wsRoot 之下 →
|
|
206
|
+
* resolve 折叠 ../ 与重复斜杠(杀 .. 穿越);wsRoot + '/' 边界前缀(杀 /ws/main2 前缀逃逸);
|
|
207
|
+
* 相对路径(openspec/changes/x.md)经 resolve 归到 wsRoot 之下 → 允许。 */
|
|
211
208
|
const isPlanPath = (p) => {
|
|
212
209
|
if (!p)
|
|
213
210
|
return false;
|
|
214
211
|
const resolved = resolve(wsRoot, p);
|
|
215
212
|
// POSIX 上 \ 是合法文件名字符(如目录名 openspec\changes),反斜杠归一化仅 win32 需要,
|
|
216
|
-
// 否则会把它折叠成相邻 openspec→changes
|
|
213
|
+
// 否则会把它折叠成相邻 openspec→changes 段对而被误放行。
|
|
217
214
|
const norm = process.platform === 'win32' ? resolved.replace(/\\/g, '/') : resolved;
|
|
218
215
|
if (!norm.startsWith(wsRoot + '/'))
|
|
219
216
|
return false;
|
|
@@ -244,11 +241,11 @@ export function buildPlanWriteGuard(workspaceRoot) {
|
|
|
244
241
|
const cmd = String(a['command'] ?? a['code'] ?? '');
|
|
245
242
|
if (!cmd)
|
|
246
243
|
return undefined;
|
|
247
|
-
// FIRST git
|
|
244
|
+
// FIRST git 判定(仅命令文本,不扫写入内容——修复内容误拒):
|
|
248
245
|
// 按 && / || / ; / | / 换行 分段,逐段提取首个 git 动词(先跳过 git 全局选项
|
|
249
246
|
// --no-pager/-p/-v/--bare/--literal-pathspecs/--no-replace-objects/-C <path>/-c <kv>/
|
|
250
247
|
// --git-dir=/--work-tree=/--namespace=)。任何一段含 git 却提取不到动词(如 git --version
|
|
251
|
-
// 或裸 git)→ fail-closed 拒绝;动词非只读白名单 →
|
|
248
|
+
// 或裸 git)→ fail-closed 拒绝;动词非只读白名单 → 拒绝。修复链式绕过与全局选项前缀 fall-through。
|
|
252
249
|
const segments = cmd.split(/\s*(?:&&|\|\||;|\||\n)\s*/);
|
|
253
250
|
for (const seg of segments) {
|
|
254
251
|
if (!/\bgit\b/.test(seg))
|
|
@@ -260,7 +257,7 @@ export function buildPlanWriteGuard(workspaceRoot) {
|
|
|
260
257
|
}
|
|
261
258
|
// THEN 写意图:bash 用增强写标记(含无空格重定向与解释器 -c/-e)且不含 plan 路径 → 拒绝。
|
|
262
259
|
// run_code 与 bash 分离(planguard-falsepositive 根治,2026-09-03):CODE_WRITE_RE(文件
|
|
263
|
-
// 写 API)∪ CODE_SHELL_VERB_RE(内嵌 shell
|
|
260
|
+
// 写 API)∪ CODE_SHELL_VERB_RE(内嵌 shell 裸动词)∪ CODE_REDIRECT_WRITE_RE
|
|
264
261
|
// (带空格重定向)——无空格 > 在 JS 里是运算符(=>、比较 remaining>0),
|
|
265
262
|
// GUARD_WRITE_INTENT_RE 复用致 openspec 裸 edit 随机误拒(15 次实证);GUARD 动词/
|
|
266
263
|
// 解释器对 run_code 移除的论证见 CODE_REDIRECT_WRITE_RE / CODE_SHELL_VERB_RE 注释。
|
|
@@ -270,8 +267,8 @@ export function buildPlanWriteGuard(workspaceRoot) {
|
|
|
270
267
|
if (isWrite) {
|
|
271
268
|
if (!isPlanCmd(cmd))
|
|
272
269
|
return 'plan-guard: P 写仅允许 openspec/changes/ 目录(禁止改动源码)';
|
|
273
|
-
//
|
|
274
|
-
// 首个字符串实参,逐条 resolve + isPlanPath(与 write/edit
|
|
270
|
+
// 含 plan 标记仍须验证实际写目标——重定向 >/>> 目标与 writeFileSync(/appendFileSync(
|
|
271
|
+
// 首个字符串实参,逐条 resolve + isPlanPath(与 write/edit 入口同款判定,杀同款
|
|
275
272
|
// openspec/changes/../.. 穿越写源码,补齐 bash/run_code 入口)。任一条目标不通过 → 拒绝。
|
|
276
273
|
// 重定向形态按入口分流:bash 无空格(>f 合法写),run_code 带空格(无空格 > 是运算符)。
|
|
277
274
|
const targets = extractWriteTargets(cmd, name === 'run_code' ? CODE_REDIRECT_TARGET_RE : BASH_REDIRECT_TARGET_RE);
|
|
@@ -313,8 +310,8 @@ function safeKeys(ctx) {
|
|
|
313
310
|
return '<unkeyed>';
|
|
314
311
|
}
|
|
315
312
|
}
|
|
316
|
-
/** 按角色在 agent scope
|
|
317
|
-
* 所有 kanban
|
|
313
|
+
/** 按角色在 agent scope 注册工具面(统一注册策略):
|
|
314
|
+
* 所有 kanban 工具从工具工厂选取 + getCaller 闭包(actor=role、boundTaskId=taskId)。
|
|
318
315
|
* can() 权限兜底仍保留在工具 execute 内(纵深防御第二道)。 */
|
|
319
316
|
export async function installRoleTools(agentCtx, role, deps) {
|
|
320
317
|
const kbMode = deps.kbMode ?? 'remote';
|
|
@@ -322,14 +319,14 @@ export async function installRoleTools(agentCtx, role, deps) {
|
|
|
322
319
|
const caller = () => ({ actor: role, boundTaskId: deps.taskId });
|
|
323
320
|
const allKanban = buildKanbanTools(deps.kanban, caller);
|
|
324
321
|
// 每角色可用的 kanban 工具名(V 额外编排、P/W/D 任务工具)
|
|
325
|
-
//
|
|
322
|
+
// 设计表另有 V 专属 kanban_link/chain_show,src/tools/kanban-tools.ts 未实现这两项,
|
|
326
323
|
// 故保持不注册(不为实现而实现多余工具),与设计表的差异以此注释声明。
|
|
327
324
|
const namesFor = {
|
|
328
325
|
v: ['kanban_create', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment', 'kanban_show', 'kanban_chain', 'kanban_list'],
|
|
329
326
|
p: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
330
327
|
w: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
331
328
|
d: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
332
|
-
//
|
|
329
|
+
// 评审角色(正式装配工具面):PT/DT 任务工具 + 只读(spec 视图等)
|
|
333
330
|
pt: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
334
331
|
dt: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
335
332
|
};
|
|
@@ -349,8 +346,8 @@ export async function installRoleTools(agentCtx, role, deps) {
|
|
|
349
346
|
for (const tool of buildWikiTools(deps.wiki, caller))
|
|
350
347
|
registry.register(tool);
|
|
351
348
|
}
|
|
352
|
-
// local
|
|
353
|
-
//
|
|
349
|
+
// local:wiki 三原语不注册,W 经 skill 工具(preset 提供)自治查写;prefetch/spec 视图保留
|
|
350
|
+
// 设计表:W 对规格卡只读(spec_card_view)
|
|
354
351
|
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
355
352
|
if (tool.name === 'spec_card_view')
|
|
356
353
|
registry.register(tool);
|
|
@@ -395,7 +392,7 @@ export async function installRoleTools(agentCtx, role, deps) {
|
|
|
395
392
|
}
|
|
396
393
|
}
|
|
397
394
|
else if (role === 'dt') {
|
|
398
|
-
// DT:只读评审——spec_card_view + wiki 只读(评审区写由 ToolGuard
|
|
395
|
+
// DT:只读评审——spec_card_view + wiki 只读(评审区写由 ToolGuard 收窄)
|
|
399
396
|
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
400
397
|
if (tool.name === 'spec_card_view')
|
|
401
398
|
registry.register(tool);
|
|
@@ -407,6 +404,8 @@ export async function installRoleTools(agentCtx, role, deps) {
|
|
|
407
404
|
registry.register(tool);
|
|
408
405
|
}
|
|
409
406
|
}
|
|
407
|
+
// ocr_review:ocr CLI 三子命令(preview/rule/managed);未安装/托管未配置在 execute 内降级引导,注册无条件
|
|
408
|
+
registry.register(buildOcrReviewTool({ cwd: () => process.cwd() }));
|
|
410
409
|
}
|
|
411
410
|
else if (role === 'v') {
|
|
412
411
|
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
@@ -415,11 +414,11 @@ export async function installRoleTools(agentCtx, role, deps) {
|
|
|
415
414
|
}
|
|
416
415
|
}
|
|
417
416
|
}
|
|
418
|
-
// ── 0.1.0 delegation
|
|
419
|
-
//
|
|
420
|
-
// scoped
|
|
417
|
+
// ── 0.1.0 delegation:全局子代理写护栏────────────────────────────
|
|
418
|
+
// 框架事实:agent.ctx guard 不被子代理继承;toolFilter 对 preset
|
|
419
|
+
// scoped 工具零作用。故 DT 子代理只读防线只能在插件全局 ctx 注册 guard(全局
|
|
421
420
|
// 生效),按发起 agent 会话 header 的 agentPreset 精准判定(childSessionMeta 在子代理
|
|
422
|
-
// join 父组合时记录 preset id
|
|
421
|
+
// join 父组合时记录 preset id):仅 kanban-dt 系收紧,其余(kanban-d 系、主会话
|
|
423
422
|
// 及其子代理)一律放行。
|
|
424
423
|
/** DT 任务运行期 taskId → chainId 同步缓存(guard 内 wiki review namespace 校验的
|
|
425
424
|
* 解析源;AgentRunner 在 DT 任务 runTask 生命周期内维护,与 permissionBlockedTasks
|
|
@@ -462,3 +461,222 @@ export function buildSubagentTreeGuard(deps = {}) {
|
|
|
462
461
|
return buildDTWriteGuard(repoRoot, chainId)(execution);
|
|
463
462
|
};
|
|
464
463
|
}
|
|
464
|
+
/** 蜂群模式主会话硬闸:全局 guard,按 header.agentPreset==='swarm'
|
|
465
|
+
* 精准判定(先例 buildSubagentTreeGuard)。swarm 会话 = 扩权参数教学拦截(宿主 bash/write/edit
|
|
466
|
+
* schema 广播 sandbox_permissions/justification,模型带参重试会触发天花板会话 approveEscalation
|
|
467
|
+
* 死循环——独立 DT 同款拦截)+ git 反选(与独立 DT 共用 GIT_MUTATION_VERBS/dualVerbGitDenyReason,
|
|
468
|
+
* buildPlanWriteGuard 同款分段提取判定,跳过 git 全局选项、提取不到动词 fail-closed,先行判定——
|
|
469
|
+
* git 变更动词多数同时命中只读基座的写标记,须以 swarm-guard 文案优先返回)+ 只读基座
|
|
470
|
+
* (buildReadOnlyWriteGuard:直接写工具全名拦截 + bash/run_code 写标记)。其余会话恒放行
|
|
471
|
+
* (角色会话自有 agent scope 护栏兜底,双保险不叠加)。 */
|
|
472
|
+
export function buildSwarmSessionGuard() {
|
|
473
|
+
return (execution) => {
|
|
474
|
+
const header = extractSessionHeader(execution?.agent);
|
|
475
|
+
if (!header || header.agentPreset !== 'swarm')
|
|
476
|
+
return undefined;
|
|
477
|
+
const name = String(execution?.name ?? '');
|
|
478
|
+
const args = execution?.arguments ?? {};
|
|
479
|
+
if (name === 'bash' || name === 'run_code' || name === 'write' || name === 'edit') {
|
|
480
|
+
// 扩权参数教学拦截:swarm 直聊会话是 danger-full-access 天花板(无更宽可升),
|
|
481
|
+
// 模型带参会被宿主 approveEscalation 拒(2026-09-02 P 会话 30 连败同根;swarm 会话
|
|
482
|
+
// 宿主直建、无 agent-runner setup 钩子装不了剥参 shadow,guard 拦截是唯一硬面)。
|
|
483
|
+
if (args && typeof args === 'object' && ESCALATION_PARAM_KEYS.some((k) => args[k] !== undefined)) {
|
|
484
|
+
return 'swarm-guard: 本会话为只读蜂群会话——不要传 sandbox_permissions/justification 扩权参数(天花板会话无可升级权限,带参必被宿主拒绝);去掉该参数后重试';
|
|
485
|
+
}
|
|
486
|
+
if (name === 'bash' || name === 'run_code') {
|
|
487
|
+
const cmd = String(args && typeof args === 'object' ? (args['command'] ?? args['code'] ?? '') : '');
|
|
488
|
+
if (cmd) {
|
|
489
|
+
const segments = cmd.split(/\s*(?:&&|\|\||;|\||\n)\s*/);
|
|
490
|
+
for (const seg of segments) {
|
|
491
|
+
if (!/\bgit\b/.test(seg))
|
|
492
|
+
continue;
|
|
493
|
+
const m = seg.match(GIT_VERB_RE);
|
|
494
|
+
const verb = m?.[1];
|
|
495
|
+
// 反选:仅禁变更动词与双态动词的变更子形态;提取不到动词 fail-closed 拒
|
|
496
|
+
if (!verb || GIT_MUTATION_VERBS.has(verb))
|
|
497
|
+
return SWARM_GIT_DENY;
|
|
498
|
+
const sub = dualVerbGitDenyReason(verb, seg.slice((m.index ?? 0) + m[0].length), SWARM_GIT_DENY);
|
|
499
|
+
if (sub)
|
|
500
|
+
return sub;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const baseReason = buildReadOnlyWriteGuard(header.cwd || '/')(execution);
|
|
506
|
+
if (baseReason)
|
|
507
|
+
return baseReason;
|
|
508
|
+
return undefined;
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// ── 独立评审(standalone DT):全局 guard + 评审工具全局注册 ───────────────
|
|
512
|
+
// 独立评审 = 用户在 dsh web 以「交付评审官 (DT)」preset 直接对话:无绑定任务、不经
|
|
513
|
+
// agent-runner 角色组合(无角色工具面、无 agent-scope guard)。本节在插件全局 ctx
|
|
514
|
+
// 注册 buildStandaloneDtGuard(仅对独立 DT 会话收紧)与 registerStandaloneReviewerTools
|
|
515
|
+
// (ocr_review + wiki 三原语);链上组合 DT 会话不受影响(自有 agent-scope guard 兜底)。
|
|
516
|
+
/** 只读评审类会话(独立 DT / 蜂群主会话共用)禁用的 git 变更动词(反选:只枚举变更面,其余动词放行)。
|
|
517
|
+
* 决策(2026-09-08):查询/clone/fetch/裸 checkout·switch 放开,仅禁改动代码与分支状态的
|
|
518
|
+
* git 操作;双态动词(checkout/switch/branch/tag/stash/remote/worktree/config/reflog/notes)
|
|
519
|
+
* 的变更子形态由 dualVerbGitDenyReason 单独判定,此处只列整词禁用项。 */
|
|
520
|
+
const GIT_MUTATION_VERBS = new Set([
|
|
521
|
+
'push', 'merge', 'rebase', 'revert', 'cherry-pick', 'filter-branch', 'filter-repo', 'replace',
|
|
522
|
+
'reset', 'restore', 'clean', 'commit', 'add', 'am', 'apply', 'mv', 'rm',
|
|
523
|
+
'gc', 'prune', 'repack', 'bisect',
|
|
524
|
+
]);
|
|
525
|
+
/** 独立评审 git 变更拒绝文案(错误文案即模型的 prompt:写明放开面,教模型改用合法形态)。 */
|
|
526
|
+
const STANDALONE_GIT_DENY = 'standalone-dt: 独立评审禁止 git 变更操作(push/merge/rebase/reset/commit 等);查询、clone/fetch 与 checkout/switch 裸切换放行';
|
|
527
|
+
/** 蜂群会话 git 变更拒绝文案(同款反选语义;文案点名 swarm-guard 便于测试与日志归因)。 */
|
|
528
|
+
const SWARM_GIT_DENY = 'swarm-guard: 蜂群会话禁止 git 变更操作(push/merge/rebase/reset/commit 等,执行由工作流 D 角色完成);查询与 clone/fetch 放行';
|
|
529
|
+
/** git 动词提取:跳过全局选项(与 swarm-guard 同款);提取不到动词 fail-closed 拒。 */
|
|
530
|
+
const GIT_VERB_RE = /\bgit(?:\s+(?:--no-pager|-p|-v|--bare|--literal-pathspecs|--no-replace-objects|-C\s+\S+|-c\s+\S+|--git-dir=\S+|--work-tree=\S+|--namespace=\S+))*\s+([a-zA-Z][\w-]*)/;
|
|
531
|
+
/** 双态 git 动词的变更子形态判定(rest = 动词之后的命令原文,deny = 命中时的拒绝文案)。
|
|
532
|
+
* 只认旗标语义不做路径解析;`checkout <path>` 与裸切分支同形(无 `--` 分隔)按放行处理
|
|
533
|
+
* (评审常在临时 clone 中进行,残留风险可接受)。undefined = 放行。 */
|
|
534
|
+
function dualVerbGitDenyReason(verb, rest, deny) {
|
|
535
|
+
switch (verb) {
|
|
536
|
+
case 'checkout': // 建分支(-b/-B/--orphan)与 `-- <path>` 丢弃工作区改动拒;裸切换放行
|
|
537
|
+
if (/^\s+(?:-\S+\s+)*(?:-b|-B|--orphan)\b/.test(rest))
|
|
538
|
+
return deny;
|
|
539
|
+
if (/^\s+--(?:\s|$)/.test(rest))
|
|
540
|
+
return deny;
|
|
541
|
+
return undefined;
|
|
542
|
+
case 'switch': // 建/替换分支拒;裸切换放行
|
|
543
|
+
return /^\s+(?:-\S+\s+)*(?:-c|-C|--create|--orphan)\b/.test(rest) ? deny : undefined;
|
|
544
|
+
case 'branch': { // 仅裸/任意非管理旗标放行;建删改(管理旗标 -m/-M/-c/-C/-d/-D 或位置参数命名)拒
|
|
545
|
+
const flagsOnly = /^(?:\s+-\S+)*\s*$/.test(rest);
|
|
546
|
+
const mgmt = /\s-(?:m|M|c|C|d|D)\b/.test(rest);
|
|
547
|
+
return !mgmt && flagsOnly ? undefined : deny;
|
|
548
|
+
}
|
|
549
|
+
case 'tag': // 仅裸/-l/-n 列表放行
|
|
550
|
+
return /^\s*$|^\s+(?:-l\b|-n\b)/.test(rest) ? undefined : deny;
|
|
551
|
+
case 'stash': // 仅 list/show/find 放行(裸 = 打印帮助)
|
|
552
|
+
return /^\s*$|^\s+(?:list|show|find)\b/.test(rest) ? undefined : deny;
|
|
553
|
+
case 'remote': // 仅裸/-v/--verbose/get-url 放行
|
|
554
|
+
return /^\s*$|^\s+(?:-v\b|--verbose\b)|^\s+get-url\b/.test(rest) ? undefined : deny;
|
|
555
|
+
case 'worktree': // 仅裸/list 放行
|
|
556
|
+
return /^\s*$|^\s+list\b/.test(rest) ? undefined : deny;
|
|
557
|
+
case 'config': // 仅读旗标放行;单/双位置参数写入(含 --global)与 --unset 一律拒
|
|
558
|
+
return /^\s*$|^\s+(?:--get\b|--get-all\b|--get-regexp\b|--list\b|-l\b)/.test(rest) ? undefined : deny;
|
|
559
|
+
case 'reflog': // 仅 delete/expire 拒,其余(裸/show)放行
|
|
560
|
+
return /^\s+(?:delete|expire)\b/.test(rest) ? deny : undefined;
|
|
561
|
+
case 'notes': // 仅裸/list/show 放行
|
|
562
|
+
return /^\s*$|^\s+(?:list|show)\b/.test(rest) ? undefined : deny;
|
|
563
|
+
default:
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/** 独立评审(standalone DT)全局硬闸。独立会话判定(同时成立):
|
|
568
|
+
* header.agentPreset === 'kanban-dt'、未经角色组合标记(!isRoleComposed)、parentSession
|
|
569
|
+
* 非 kbn- 前缀(与 buildSubagentTreeGuard 判据对齐:有 kbn- parentSession 的是链上系
|
|
570
|
+
* DT 子代理,交由该 guard 管——其 incarnation 可能无组合标记、宿主 id 也可能非 kbn-
|
|
571
|
+
* 前缀,漏判会把链评审写入误当独立模式拒掉)、且(能从 execution.agent 取到 session
|
|
572
|
+
* id 时)id 非 kbn- 前缀。不满足 → 不走独立规则。
|
|
573
|
+
* 独立模式规则(按序):
|
|
574
|
+
* 1. kanban 写工具(complete/block/comment/heartbeat/create)→ 拒(独立评审不挂任务链);
|
|
575
|
+
* 2. bash/run_code/write/edit → 扩权参数教学拦截(sandbox_permissions/justification 任一
|
|
576
|
+
* 出现即拒并教模型去参——只读会话无扩权场景,宿主沙箱校验报错文案晦涩);bash/run_code
|
|
577
|
+
* 再过 git 反选(swarm-guard 同款分段提取:&&/||/;/|/换行 分段、跳过全局选项、提取不到
|
|
578
|
+
* 动词 fail-closed 拒):仅禁变更动词与双态动词的变更子形态(见 GIT_STANDALONE_DENY_VERBS
|
|
579
|
+
* /standaloneGitDenyReason),查询、clone/fetch、裸 checkout/switch 放行;非 git 段不因
|
|
580
|
+
* git 规则拒绝;最后挂只读基座 buildReadOnlyWriteGuard(写动词/重定向照拒;clone/fetch
|
|
581
|
+
* 落盘是 git 自身行为,BASH_WRITE_RE 无此二动词天然放行,无需特判);
|
|
582
|
+
* 3. wiki_write(独立模式)→ 仅放行 projects/<repo>/reviews/<主题>-<日期>/ 命名空间;
|
|
583
|
+
* 4. wiki_write(非独立会话)→ 链上组合会话(isRoleComposed)放行(自有 agent-scope
|
|
584
|
+
* guard 管,链命名空间写入绝不被本 guard 误拦);其余(main/swarm/未知 GUI)拒绝
|
|
585
|
+
* ——评审工具全局注册后 wiki_write 收紧到评审会话;
|
|
586
|
+
* 5. 其余工具(read/glob/grep/ocr_review/wiki_read/wiki_search 等)→ 放行。 */
|
|
587
|
+
export function buildStandaloneDtGuard() {
|
|
588
|
+
return (execution) => {
|
|
589
|
+
const header = extractSessionHeader(execution?.agent);
|
|
590
|
+
const name = String(execution?.name ?? '');
|
|
591
|
+
const agent = execution?.agent;
|
|
592
|
+
const sessionId = typeof agent?.id === 'string' ? agent.id : undefined;
|
|
593
|
+
// 链上系 DT 子代理(parentSession 为 kbn-<taskId> 前缀):交由 buildSubagentTreeGuard 管,
|
|
594
|
+
// 本 guard 不按独立模式收紧、也不在非独立分支拒其 wiki_write(链评审目录写入归该 guard 校验)。
|
|
595
|
+
const isDtChainSubagent = !!header
|
|
596
|
+
&& header.agentPreset === 'kanban-dt'
|
|
597
|
+
&& typeof header.parentSession === 'string'
|
|
598
|
+
&& header.parentSession.startsWith('kbn-');
|
|
599
|
+
const standalone = !!header
|
|
600
|
+
&& header.agentPreset === 'kanban-dt'
|
|
601
|
+
&& !isRoleComposed(execution?.agent)
|
|
602
|
+
&& !isDtChainSubagent
|
|
603
|
+
&& !(sessionId !== undefined && sessionId.startsWith('kbn-'));
|
|
604
|
+
if (standalone) {
|
|
605
|
+
if (name === 'kanban_complete' || name === 'kanban_block' || name === 'kanban_comment' || name === 'kanban_heartbeat' || name === 'kanban_create') {
|
|
606
|
+
return 'standalone-dt: 独立评审模式不使用看板工具';
|
|
607
|
+
}
|
|
608
|
+
if (name === 'bash' || name === 'run_code' || name === 'write' || name === 'edit') {
|
|
609
|
+
const args = execution?.arguments ?? {};
|
|
610
|
+
// 扩权参数教学拦截:独立评审只读无扩权场景,宿主 bash/write/edit schema 仍广播
|
|
611
|
+
// sandbox_permissions/justification(组合层行为,会话层改不掉),模型重试惯性带参
|
|
612
|
+
// 会触发宿主沙箱校验错误(invalid justification)——在此拦下并教模型去参重试。
|
|
613
|
+
if (args && typeof args === 'object' && ESCALATION_PARAM_KEYS.some((k) => args[k] !== undefined)) {
|
|
614
|
+
return 'standalone-dt: 独立评审为只读会话——不要传 sandbox_permissions/justification 扩权参数(会触发宿主沙箱校验错误);去掉该参数后重试';
|
|
615
|
+
}
|
|
616
|
+
if (name === 'bash' || name === 'run_code') {
|
|
617
|
+
const cmd = String(args['command'] ?? args['code'] ?? '');
|
|
618
|
+
if (cmd) {
|
|
619
|
+
const segments = cmd.split(/\s*(?:&&|\|\||;|\||\n)\s*/);
|
|
620
|
+
for (const seg of segments) {
|
|
621
|
+
if (!/\bgit\b/.test(seg))
|
|
622
|
+
continue;
|
|
623
|
+
const m = seg.match(GIT_VERB_RE);
|
|
624
|
+
const verb = m?.[1];
|
|
625
|
+
// 反选:仅禁变更动词与双态动词的变更子形态;提取不到动词 fail-closed 拒
|
|
626
|
+
if (!verb || GIT_MUTATION_VERBS.has(verb))
|
|
627
|
+
return STANDALONE_GIT_DENY;
|
|
628
|
+
const sub = dualVerbGitDenyReason(verb, seg.slice((m.index ?? 0) + m[0].length), STANDALONE_GIT_DENY);
|
|
629
|
+
if (sub)
|
|
630
|
+
return sub;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const baseReason = buildReadOnlyWriteGuard(header.cwd || '/')(execution);
|
|
634
|
+
if (baseReason)
|
|
635
|
+
return baseReason;
|
|
636
|
+
}
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
if (name === 'wiki_write') {
|
|
640
|
+
const args = execution?.arguments ?? {};
|
|
641
|
+
const pagePath = String(args && typeof args === 'object' ? args['pagePath'] ?? '' : '');
|
|
642
|
+
if (!isStandaloneReviewNamespacePath(pagePath))
|
|
643
|
+
return 'wiki-write-outside-reviews-namespace: 独立评审仅可写 projects/<repo>/reviews/<主题>-<日期>/';
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
if (name === 'wiki_write') {
|
|
649
|
+
if (isRoleComposed(execution?.agent) || isDtChainSubagent)
|
|
650
|
+
return undefined;
|
|
651
|
+
return 'wiki-write-restricted-to-reviewer-sessions: wiki_write 仅限交付评审官(DT)评审会话使用';
|
|
652
|
+
}
|
|
653
|
+
return undefined;
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/** 独立评审工具全局注册:ocr_review + wiki 三原语(wiki_read/wiki_search/wiki_write)。
|
|
657
|
+
* 独立评审不经 agent-runner 角色组合,角色工具面不存在,故在插件全局 ctx 注册;
|
|
658
|
+
* caller 固定 actor='dt'(can('wiki-write','dt')=true),wiki_write 的会话级收紧由
|
|
659
|
+
* buildStandaloneDtGuard 完成(链上组合会话放行,main/swarm/未知 GUI 拒绝)。
|
|
660
|
+
* wiki 客户端按 kbMode 构造(与 registerMainSessionTools 同源:remote 优先 ctx.get('wiki')
|
|
661
|
+
* 注入(测试 mock),生产热读取 getEffective().wikiVault;local 走 LocalWikiClient)。
|
|
662
|
+
* local 模式不注册 wiki_write:LocalWikiClient.abs 只接受 wiki/** 形态,而 wiki_write
|
|
663
|
+
* 工具边界只放行 projects/... 命名空间——双锁死(任何路径都写不进)。与链上 DT local
|
|
664
|
+
* 行为一致(走 skill/fs 工具而非 wiki 原语);wiki_read/wiki_search 不受写路径白名单
|
|
665
|
+
* 约束,照常注册。
|
|
666
|
+
* registry 缺失(测试裸 Context)→ return,与 registerMainSessionTools 同款防御。 */
|
|
667
|
+
export function registerStandaloneReviewerTools(ctx, configProvider) {
|
|
668
|
+
const registry = ctx.get('tools');
|
|
669
|
+
if (!registry)
|
|
670
|
+
return; // 测试裸 Context 无 tools 服务,跳过注册
|
|
671
|
+
registry.register(buildOcrReviewTool({ cwd: () => process.cwd() }));
|
|
672
|
+
const kbMode = configProvider.mode;
|
|
673
|
+
const wiki = (kbMode === 'local'
|
|
674
|
+
? new LocalWikiClient(ensureLocalKbRoot())
|
|
675
|
+
: (ctx.get('wiki') ?? new WikiVaultClient(() => configProvider.getEffective().wikiVault)));
|
|
676
|
+
const caller = () => ({ actor: 'dt' });
|
|
677
|
+
for (const tool of buildWikiTools(wiki, caller)) {
|
|
678
|
+
if (kbMode === 'local' && tool.name === 'wiki_write')
|
|
679
|
+
continue;
|
|
680
|
+
registry.register(tool);
|
|
681
|
+
}
|
|
682
|
+
}
|
package/lib/roles/wiki-worker.js
CHANGED
|
@@ -12,7 +12,7 @@ export class WikiWorker {
|
|
|
12
12
|
constructor(kanban, wiki, cfg) { this.kanban = kanban; this.wiki = wiki; this.cfg = cfg; }
|
|
13
13
|
async executePrefetch(task, mode, source) {
|
|
14
14
|
const ws = workspaceOf(task.chainId, task.id);
|
|
15
|
-
//
|
|
15
|
+
// 三模式全部落地;产物一律写入任务工作区(原汁原味,禁压缩/蒸馏)
|
|
16
16
|
if (mode === 'file') {
|
|
17
17
|
// 只读命令由角色 agent 的 terminal 白名单执行;此处仅登记产物引用
|
|
18
18
|
if (!source.startsWith(ws))
|
|
@@ -27,7 +27,7 @@ export class WikiWorker {
|
|
|
27
27
|
throw new Error('prefetch source outside workspace: ' + source);
|
|
28
28
|
return { ref: source || ref };
|
|
29
29
|
}
|
|
30
|
-
// kb 模式:知识库查询产物。local
|
|
30
|
+
// kb 模式:知识库查询产物。local 模式不走 wiki 工具——明确指引改用 skill 加载 llm-wiki。
|
|
31
31
|
if (this.cfg.kbMode === 'local') {
|
|
32
32
|
throw new Error('prefetch_kb unavailable in local KB mode: use the skill tool to load llm-wiki for KB search instead');
|
|
33
33
|
}
|
|
@@ -47,6 +47,6 @@ export class WikiWorker {
|
|
|
47
47
|
return { kb_url: this.wikiBase() + '/#/page/' + pagePath, page_path: pagePath };
|
|
48
48
|
}
|
|
49
49
|
wikiBase() {
|
|
50
|
-
return this.wiki.baseUrl; //
|
|
50
|
+
return this.wiki.baseUrl; // 用 WikiVaultClient 公开 getter,不做类型强转
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -3,7 +3,30 @@ import type { KanbanConfig } from '../config.js';
|
|
|
3
3
|
import type { KanbanProvider } from '../services/kanban-provider.js';
|
|
4
4
|
import type { ConfigProvider } from '../services/config-provider.js';
|
|
5
5
|
import type { LlmRuntimeLike } from '../services/llm-catalog.js';
|
|
6
|
+
import { managedProviderReady, probeOcr } from '../services/ocr-cli.js';
|
|
7
|
+
/** ocr 全局安装器:回调式 npm install -g,onSpawn 转交子进程句柄供取消。 */
|
|
8
|
+
export type OcrInstaller = (onSpawn?: (child: {
|
|
9
|
+
kill(sig?: string): boolean;
|
|
10
|
+
}) => void) => Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
version?: string;
|
|
13
|
+
log: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** ocr HTTP 分支可注入依赖(测试用);缺省走真实现。 */
|
|
16
|
+
export interface KanbanOcrDeps {
|
|
17
|
+
installer?: OcrInstaller;
|
|
18
|
+
probeFn?: typeof probeOcr;
|
|
19
|
+
managedReadyFn?: typeof managedProviderReady;
|
|
20
|
+
wirer?: (a: {
|
|
21
|
+
provider: string;
|
|
22
|
+
model: string;
|
|
23
|
+
}) => Promise<{
|
|
24
|
+
ok: boolean;
|
|
25
|
+
log: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
6
28
|
/** 看板 HTTP 桥(Web GUI 浏览器半消费):GET /kanban/board 读快照;POST /kanban/action 执行状态操作;
|
|
7
|
-
* GET/PUT /kanban/config
|
|
29
|
+
* GET/PUT /kanban/config 配置读写(无 reset:各用户模型配置不同,无公共默认值);GET /kanban/llm-catalog 模型目录;
|
|
30
|
+
* GET /kanban/ocr/status + POST /kanban/ocr/install(/cancel) + GET install/state + POST /kanban/ocr/wire ocr 评审引擎运维。
|
|
8
31
|
* 仅在 webServer 服务存在时挂载(CLI/headless/测试裸 Context 不挂)。 */
|
|
9
|
-
export declare function registerKanbanHttp(ctx: Context, provider: KanbanProvider, configProvider: ConfigProvider, llm: LlmRuntimeLike, config?: Pick<KanbanConfig, 'ui'
|
|
32
|
+
export declare function registerKanbanHttp(ctx: Context, provider: KanbanProvider, configProvider: ConfigProvider, llm: LlmRuntimeLike, config?: Pick<KanbanConfig, 'ui'>, ocrDeps?: KanbanOcrDeps): void;
|