@joekytc/dsh-swarm 0.1.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/LICENSE +21 -0
- package/README.md +504 -0
- package/README.zh-CN.md +452 -0
- package/client/BoardCard.tsx +41 -0
- package/client/ConnectionBanner.tsx +16 -0
- package/client/KanbanBoard.tsx +168 -0
- package/client/KanbanTab.tsx +26 -0
- package/client/RenameModal.tsx +38 -0
- package/client/TaskDrawer.tsx +212 -0
- package/client/WorkflowRail.tsx +175 -0
- package/client/board-store.ts +196 -0
- package/client/css.d.ts +4 -0
- package/client/index.ts +31 -0
- package/client/kanban.css +653 -0
- package/client/useKanbanBoard.ts +7 -0
- package/client/workflow-model.ts +229 -0
- package/cordis.patch.yml +88 -0
- package/lib/client.js +1205 -0
- package/lib/config.d.ts +46 -0
- package/lib/config.js +43 -0
- package/lib/dispatcher/agent-runner.d.ts +23 -0
- package/lib/dispatcher/agent-runner.js +429 -0
- package/lib/dispatcher/chain-auditor.d.ts +47 -0
- package/lib/dispatcher/chain-auditor.js +194 -0
- package/lib/dispatcher/dispatcher.d.ts +50 -0
- package/lib/dispatcher/dispatcher.js +280 -0
- package/lib/dispatcher/event-waker.d.ts +13 -0
- package/lib/dispatcher/event-waker.js +24 -0
- package/lib/dispatcher/git-credentials.d.ts +33 -0
- package/lib/dispatcher/git-credentials.js +78 -0
- package/lib/dispatcher/merge-gate.d.ts +28 -0
- package/lib/dispatcher/merge-gate.js +74 -0
- package/lib/dispatcher/model-candidates.d.ts +13 -0
- package/lib/dispatcher/model-candidates.js +31 -0
- package/lib/dispatcher/session-events.d.ts +28 -0
- package/lib/dispatcher/session-events.js +33 -0
- package/lib/dispatcher/target-repo.d.ts +15 -0
- package/lib/dispatcher/target-repo.js +42 -0
- package/lib/dispatcher/v-orchestrator.d.ts +74 -0
- package/lib/dispatcher/v-orchestrator.js +452 -0
- package/lib/dispatcher/watchdog.d.ts +15 -0
- package/lib/dispatcher/watchdog.js +30 -0
- package/lib/dispatcher/workspace-attach.d.ts +40 -0
- package/lib/dispatcher/workspace-attach.js +112 -0
- package/lib/domain/delivery-contract.d.ts +18 -0
- package/lib/domain/delivery-contract.js +80 -0
- package/lib/domain/delivery-evidence.d.ts +12 -0
- package/lib/domain/delivery-evidence.js +41 -0
- package/lib/domain/event-store.d.ts +20 -0
- package/lib/domain/event-store.js +53 -0
- package/lib/domain/kanban-service.d.ts +94 -0
- package/lib/domain/kanban-service.js +430 -0
- package/lib/domain/permissions.d.ts +7 -0
- package/lib/domain/permissions.js +54 -0
- package/lib/domain/planning-checklist.d.ts +22 -0
- package/lib/domain/planning-checklist.js +81 -0
- package/lib/domain/prefetch-manifest.d.ts +21 -0
- package/lib/domain/prefetch-manifest.js +73 -0
- package/lib/domain/projection.d.ts +3 -0
- package/lib/domain/projection.js +165 -0
- package/lib/domain/review-evidence.d.ts +13 -0
- package/lib/domain/review-evidence.js +76 -0
- package/lib/domain/state-machine.d.ts +4 -0
- package/lib/domain/state-machine.js +32 -0
- package/lib/domain/task-parents.d.ts +17 -0
- package/lib/domain/task-parents.js +39 -0
- package/lib/domain/tdd-classify.d.ts +5 -0
- package/lib/domain/tdd-classify.js +20 -0
- package/lib/domain/types.d.ts +142 -0
- package/lib/domain/types.js +2 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +56 -0
- package/lib/roles/preset-installer.d.ts +8 -0
- package/lib/roles/preset-installer.js +53 -0
- package/lib/roles/toolsets.d.ts +62 -0
- package/lib/roles/toolsets.js +332 -0
- package/lib/roles/wiki-worker.d.ts +20 -0
- package/lib/roles/wiki-worker.js +44 -0
- package/lib/routes/kanban-http.d.ts +6 -0
- package/lib/routes/kanban-http.js +159 -0
- package/lib/routes/kanban-sse.d.ts +8 -0
- package/lib/routes/kanban-sse.js +58 -0
- package/lib/routes/planning-driver.d.ts +16 -0
- package/lib/routes/planning-driver.js +52 -0
- package/lib/routes/prefix-router.d.ts +29 -0
- package/lib/routes/prefix-router.js +28 -0
- package/lib/services/kanban-provider.d.ts +16 -0
- package/lib/services/kanban-provider.js +14 -0
- package/lib/tools/kanban-tools.d.ts +11 -0
- package/lib/tools/kanban-tools.js +169 -0
- package/lib/tools/main-session-tools.d.ts +20 -0
- package/lib/tools/main-session-tools.js +170 -0
- package/lib/tools/planning-tools.d.ts +32 -0
- package/lib/tools/planning-tools.js +102 -0
- package/lib/tools/prefetch-tools.d.ts +5 -0
- package/lib/tools/prefetch-tools.js +59 -0
- package/lib/tools/spec-card-tools.d.ts +4 -0
- package/lib/tools/spec-card-tools.js +75 -0
- package/lib/tools/wiki-tools.d.ts +4 -0
- package/lib/tools/wiki-tools.js +57 -0
- package/lib/wiki/kb-linkage.d.ts +9 -0
- package/lib/wiki/kb-linkage.js +87 -0
- package/lib/wiki/page-path.d.ts +6 -0
- package/lib/wiki/page-path.js +28 -0
- package/lib/wiki/wiki-vault-client.d.ts +28 -0
- package/lib/wiki/wiki-vault-client.js +48 -0
- package/package.json +83 -0
- package/personas/kanban-d/agent.cordis.yml +157 -0
- package/personas/kanban-d/preset.yml +2 -0
- package/personas/kanban-dt/agent.cordis.yml +71 -0
- package/personas/kanban-dt/preset.yml +2 -0
- package/personas/kanban-p/agent.cordis.yml +66 -0
- package/personas/kanban-p/preset.yml +2 -0
- package/personas/kanban-pt/agent.cordis.yml +47 -0
- package/personas/kanban-pt/preset.yml +2 -0
- package/personas/kanban-v/agent.cordis.yml +47 -0
- package/personas/kanban-v/preset.yml +2 -0
- package/personas/kanban-w/agent.cordis.yml +47 -0
- package/personas/kanban-w/preset.yml +2 -0
- package/personas/persona-d.md +26 -0
- package/personas/persona-dt.md +18 -0
- package/personas/persona-p.md +13 -0
- package/personas/persona-pt.md +13 -0
- package/personas/persona-v.md +18 -0
- package/personas/persona-w.md +12 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { KanbanProvider } from './services/kanban-provider.js';
|
|
2
|
+
import { Config } from './config.js';
|
|
3
|
+
import { registerMainSessionTools } from './tools/main-session-tools.js';
|
|
4
|
+
import { installRolePresets } from './roles/preset-installer.js';
|
|
5
|
+
import { registerKanbanHttp } from './routes/kanban-http.js';
|
|
6
|
+
import { startDispatcher } from './dispatcher/dispatcher.js';
|
|
7
|
+
export const name = 'dsh-swarm';
|
|
8
|
+
export { Config };
|
|
9
|
+
// P1-9:无 inject——KanbanProvider 只用文件系统(FileEventStore),不依赖 ctx.storage 等任何服务。
|
|
10
|
+
/** 可选服务延迟装配:插件 apply 时各服务可能尚未提供(cordis 注入顺序),轮询 ctx.get 直至可用再接线。 */
|
|
11
|
+
function wireWhenAvailable(ctx, name, fn, timeoutMs = 60000) {
|
|
12
|
+
if (ctx.get(name)) {
|
|
13
|
+
fn();
|
|
14
|
+
return () => { };
|
|
15
|
+
}
|
|
16
|
+
const timer = setInterval(() => {
|
|
17
|
+
if (ctx.get(name)) {
|
|
18
|
+
clearInterval(timer);
|
|
19
|
+
clearTimeout(to);
|
|
20
|
+
fn();
|
|
21
|
+
}
|
|
22
|
+
}, 500);
|
|
23
|
+
const to = setTimeout(() => clearInterval(timer), timeoutMs);
|
|
24
|
+
return () => { clearInterval(timer); clearTimeout(to); };
|
|
25
|
+
}
|
|
26
|
+
/** 等待多个服务同时可用后再接线(如 tools 先于 kanban 出现时,单服务轮询会过早执行)。 */
|
|
27
|
+
function wireAllAvailable(ctx, names, fn, timeoutMs = 60000) {
|
|
28
|
+
const ready = () => names.every((name) => Boolean(ctx.get(name)));
|
|
29
|
+
if (ready()) {
|
|
30
|
+
fn();
|
|
31
|
+
return () => { };
|
|
32
|
+
}
|
|
33
|
+
const timer = setInterval(() => {
|
|
34
|
+
if (ready()) {
|
|
35
|
+
clearInterval(timer);
|
|
36
|
+
clearTimeout(to);
|
|
37
|
+
fn();
|
|
38
|
+
}
|
|
39
|
+
}, 500);
|
|
40
|
+
const to = setTimeout(() => clearInterval(timer), timeoutMs);
|
|
41
|
+
return () => { clearInterval(timer); clearTimeout(to); };
|
|
42
|
+
}
|
|
43
|
+
export function apply(ctx, config) {
|
|
44
|
+
// cordis 4:Service 构造即注册(super(ctx,'kanban') 调 ctx.reflect.provide),无需手动 provide。
|
|
45
|
+
const provider = new KanbanProvider(ctx, config);
|
|
46
|
+
// D22:把包内角色裁剪 preset 组合安装到 $DSH_HOME/.agent-presets/(真实 API 下唯一可发现的自定义根)。
|
|
47
|
+
const installed = installRolePresets();
|
|
48
|
+
console.info('[dsh-swarm] role presets installed: ' + (installed.length ? installed.join(',') : 'none'));
|
|
49
|
+
// 可选服务接线均延迟到服务可用后:
|
|
50
|
+
// - Web GUI 数据桥(GET /kanban/board + POST /kanban/action,仅 webServer 存在时挂载)
|
|
51
|
+
wireWhenAvailable(ctx, 'webServer', () => registerKanbanHttp(ctx, provider, config));
|
|
52
|
+
// - P1-3 主会话工具面(spec_card_view/edit/approve + kanban 只读子集 + 前缀路由工具)
|
|
53
|
+
wireAllAvailable(ctx, ['tools', 'kanban'], () => registerMainSessionTools(ctx, config));
|
|
54
|
+
// - 调度层:事件唤醒 V(R20)+ 每任务 agent runner + 看门狗(仅 agents 可用时启动)
|
|
55
|
+
wireAllAvailable(ctx, ['agents', 'kanban'], () => startDispatcher(ctx, config));
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 包内组合目录(随包分发)。src/roles/ 与 lib/roles/ 深度一致,均经 ../../ 回到包根。 */
|
|
2
|
+
export declare function packagePresetsDir(): string;
|
|
3
|
+
/** $DSH_HOME/.agent-presets(agent-presets 服务 includeUserRoot 派生根;DSH_HOME 缺省 ~/.dsh)。 */
|
|
4
|
+
export declare function userPresetsRoot(): string;
|
|
5
|
+
/** 安装角色裁剪 preset 到用户预设根;返回成功安装的 preset id 列表(空=无随包组合)。
|
|
6
|
+
* 尽力而为:单 preset 写失败(用户预设根不可写等)仅告警不抛出,
|
|
7
|
+
* 后续 agentPresets.mount('kanban-<role>') 失败由 runner 降级日志兜底。 */
|
|
8
|
+
export declare function installRolePresets(): string[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/roles/preset-installer.ts
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
/**
|
|
7
|
+
* D22 角色裁剪 preset 的运行时安装(2026-08-15 取舍记录):
|
|
8
|
+
* - 组合文件随包分发(personas/kanban-{v,p,w,d}/agent.cordis.yml,随 npm 包分发)。
|
|
9
|
+
* kanban-v 于 2026-08-17 补充(R21 对齐):V=butler·orchestrator 零执行能力,组合仅 persona+instructions。
|
|
10
|
+
* - 官方 dsh-agent-presets 的 roots 在 web 启动的 composeProfile 中被强制覆盖为
|
|
11
|
+
* 官方 shipped root 单一路径(profile-boot 内 SHIPPED_PRESET_ROOT 硬编码),
|
|
12
|
+
* 因此 cordis.patch.yml 追加 roots 指向包内目录在真实 API 下无效;
|
|
13
|
+
* 唯一可发现的自定义根是 includeUserRoot 派生的 `$DSH_HOME/.agent-presets`。
|
|
14
|
+
* - 故插件 apply 时把包内组合复制到 `$DSH_HOME/.agent-presets/<id>/`,
|
|
15
|
+
* agentPresets.mount(agentCtx, 'kanban-v'|'kanban-p'|'kanban-w'|'kanban-d') 即可解析。
|
|
16
|
+
* - 幂等:重复安装覆盖同名文件;缺组合文件(未随包)则跳过并让 runner 降级日志。
|
|
17
|
+
*/
|
|
18
|
+
const PRESET_IDS = ['kanban-v', 'kanban-p', 'kanban-w', 'kanban-d', 'kanban-pt', 'kanban-dt'];
|
|
19
|
+
/** 包内组合目录(随包分发)。src/roles/ 与 lib/roles/ 深度一致,均经 ../../ 回到包根。 */
|
|
20
|
+
export function packagePresetsDir() {
|
|
21
|
+
return fileURLToPath(new URL('../../personas/', import.meta.url));
|
|
22
|
+
}
|
|
23
|
+
/** $DSH_HOME/.agent-presets(agent-presets 服务 includeUserRoot 派生根;DSH_HOME 缺省 ~/.dsh)。 */
|
|
24
|
+
export function userPresetsRoot() {
|
|
25
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), '.agent-presets');
|
|
26
|
+
}
|
|
27
|
+
/** 安装角色裁剪 preset 到用户预设根;返回成功安装的 preset id 列表(空=无随包组合)。
|
|
28
|
+
* 尽力而为:单 preset 写失败(用户预设根不可写等)仅告警不抛出,
|
|
29
|
+
* 后续 agentPresets.mount('kanban-<role>') 失败由 runner 降级日志兜底。 */
|
|
30
|
+
export function installRolePresets() {
|
|
31
|
+
const src = packagePresetsDir();
|
|
32
|
+
const dstRoot = userPresetsRoot();
|
|
33
|
+
const installed = [];
|
|
34
|
+
for (const id of PRESET_IDS) {
|
|
35
|
+
const srcFile = join(src, id, 'agent.cordis.yml');
|
|
36
|
+
if (!existsSync(srcFile))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
const dstDir = join(dstRoot, id);
|
|
40
|
+
mkdirSync(dstDir, { recursive: true });
|
|
41
|
+
copyFileSync(srcFile, join(dstDir, 'agent.cordis.yml'));
|
|
42
|
+
const meta = join(src, id, 'preset.yml');
|
|
43
|
+
if (existsSync(meta))
|
|
44
|
+
copyFileSync(meta, join(dstDir, 'preset.yml'));
|
|
45
|
+
installed.push(id);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// 尽力而为:写入失败(如沙箱/权限拒绝)仅告警,不阻断插件启动
|
|
49
|
+
console.warn('[dsh-swarm] role preset install skipped ' + id + ' -> ' + dstRoot + ': ' + String(err));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return installed;
|
|
53
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
3
|
+
import type { WikiVaultClient } from '../wiki/wiki-vault-client.js';
|
|
4
|
+
import type { Role } from '../domain/types.js';
|
|
5
|
+
/** 判定 wiki 路径是否位于 DT 评审命名空间 projects/<chain>/review/(拒绝 ../、绝对路径、非 review 前缀)。 */
|
|
6
|
+
export declare function isReviewNamespacePath(pagePath: string, chainId: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* 评审引擎决策(DT):ocr(open-code-review Delegation 模式)优先;
|
|
9
|
+
* 不可用 fallback superpowers code-review;两者都不可用 → review-tool-unavailable(阻塞)。
|
|
10
|
+
* 纯函数便于单测;真实可用性探测在 agent-runner 装配(探活 ocr 二进制/失败)。
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveReviewEngine(available: {
|
|
13
|
+
ocr: boolean;
|
|
14
|
+
codeReview: boolean;
|
|
15
|
+
}): 'ocr' | 'code-review' | 'review-tool-unavailable';
|
|
16
|
+
/**
|
|
17
|
+
* DT 写护栏 = PT 只读护栏(源码/git/写标记 bash 拒绝)+ wiki_write 仅 review namespace 收窄。
|
|
18
|
+
* repoRoot 为 D 目标仓库;chainId 用于 wiki 评审命名空间校验。
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildDTWriteGuard(repoRoot: string, chainId: string): (execution: {
|
|
21
|
+
name?: string;
|
|
22
|
+
arguments?: unknown;
|
|
23
|
+
}) => string | undefined;
|
|
24
|
+
export declare function buildReadOnlyWriteGuard(_repoRoot: string): (execution: {
|
|
25
|
+
name?: string;
|
|
26
|
+
arguments?: unknown;
|
|
27
|
+
}) => string | undefined;
|
|
28
|
+
/** P 专用写护栏(Q3):读全放行;git mutation 一律拒绝;写仅允许目标仓库 openspec/changes 目录。
|
|
29
|
+
* 直接 fs 写工具 → 路径经 resolve 归一化后须落在 <workspaceRoot>/openspec/changes/ 之下(相邻段对判定);
|
|
30
|
+
* bash/run_code 写标记命令 → 命令文本须含 `openspec/changes` 子串,且提取出的实际写目标(重定向
|
|
31
|
+
* 目标 / writeFileSync 实参)逐条经 resolve+isPlanPath 校验(I1:杀 openspec/changes/../.. 穿越写源码)。
|
|
32
|
+
* 源码/src/lib/tests 等写不入(不含该子串)——"禁止改动源码"为工具级硬约束,非 prompt 软约束。
|
|
33
|
+
* execution 以 dsh-tools 形态 { name, arguments } 传入(与 buildReadOnlyWriteGuard 一致)。 */
|
|
34
|
+
export declare function buildPlanWriteGuard(workspaceRoot: string): (execution: {
|
|
35
|
+
name?: string;
|
|
36
|
+
arguments?: unknown;
|
|
37
|
+
}) => string | undefined;
|
|
38
|
+
/** 按角色在 agent scope 注册工具面(P1-3 统一注册策略):
|
|
39
|
+
* 所有 kanban 工具从 T9 工厂选取 + getCaller 闭包(actor=role、boundTaskId=taskId)。
|
|
40
|
+
* can() 权限兜底仍保留在工具 execute 内(纵深防御第二道)。 */
|
|
41
|
+
export declare function installRoleTools(agentCtx: Context, role: Role, deps: {
|
|
42
|
+
kanban: KanbanService;
|
|
43
|
+
wiki: WikiVaultClient;
|
|
44
|
+
taskId?: string;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
export declare function registerDtTaskChain(taskId: string, chainId: string): void;
|
|
47
|
+
export declare function unregisterDtTaskChain(taskId: string): void;
|
|
48
|
+
export interface SubagentGuardDeps {
|
|
49
|
+
/** kbn-<taskId> → chainId 同步解析(缺省用 module 缓存;测试注入用)。 */
|
|
50
|
+
getTaskChainId?(taskId: string): string | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** 全局子代理写护栏:仅 DT 角色会话的"子代理"(agentPreset === 'kanban-dt' 且
|
|
53
|
+
* header.parentSession 为 kbn-<taskId> 前缀)应用 buildDTWriteGuard。判据:parentSession
|
|
54
|
+
* 缺失或非 kbn- 前缀 → 放行(DT 父会话自身或无关会话;DT 父会话只读由 agent.ctx guard
|
|
55
|
+
* 兜底,双保险)。repoRoot 取子代理 header.cwd(继承 DT 会话 cwd=评审目标仓库);缺省
|
|
56
|
+
* '/'(写标记全拦的保守形态)。chainId 从 parentSession(kbn-<taskId>)解析;解析不到
|
|
57
|
+
* → 空(wiki_write fail-closed 全拒,源码写拦截不受影响)。 */
|
|
58
|
+
export declare function buildSubagentTreeGuard(deps?: SubagentGuardDeps): (execution: {
|
|
59
|
+
name?: string;
|
|
60
|
+
arguments?: unknown;
|
|
61
|
+
agent?: unknown;
|
|
62
|
+
}) => string | undefined;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
3
|
+
import { buildKanbanTools } from '../tools/kanban-tools.js';
|
|
4
|
+
import { buildSpecCardTools } from '../tools/spec-card-tools.js';
|
|
5
|
+
import { buildWikiTools } from '../tools/wiki-tools.js';
|
|
6
|
+
import { buildPrefetchTools } from '../tools/prefetch-tools.js';
|
|
7
|
+
import { WikiWorker } from './wiki-worker.js';
|
|
8
|
+
/** 直接写工具(无条件视为写能力;只读工具如 read/glob/grep 不算)。 */
|
|
9
|
+
const DIRECT_WRITE_TOOLS = new Set(['write', 'edit', 'rm', 'mv', 'cp', 'mkdir', 'mkfile']);
|
|
10
|
+
/** bash/run_code 命令中的写操作标记(写证据启发式;ls/cat/grep/git show 等只读不算)。
|
|
11
|
+
* 与 chain-auditor 同源;重定向标记用 \s>>?(要求 > 前有空白),避免 2>/dev/null 只读重定向误判。 */
|
|
12
|
+
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|rebase|reset|clean|restore|tag|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;
|
|
13
|
+
/** run_code(JS/TS/Python 程序)中的写操作标记:文件写 API / 命令派发写工具。
|
|
14
|
+
* 含 Python 写标记(F2,DT run_code 盲区闭环):open() 写模式精确版('w'/'a'/'w+'/'a+' 及
|
|
15
|
+
* wb/ab 等变体,读模式 'r' 不命中)、os. 模块写、pathlib Path 写、shutil 复制/移动/删除。 */
|
|
16
|
+
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;
|
|
17
|
+
/** git 只读动词白名单:命中则 git 命令放行;其余 git 动词(含 checkout/branch/stash/merge/commit/push 等)一律拒绝。
|
|
18
|
+
* 反选比枚举 mutation 更全:新增 mutation 动词无需维护。config/remote 兼具读写语义但仅改 .git 元数据不改源码,
|
|
19
|
+
* 故不列入白名单(拒绝);纯读子命令(status/log/show/diff/rev-parse/ls-files/ls-tree/grep/blame/describe/
|
|
20
|
+
* shortlog/help/version/count-objects/fsck)放行。 */
|
|
21
|
+
const GIT_READ_VERBS = new Set(['status', 'log', 'show', 'diff', 'rev-parse', 'ls-files', 'ls-tree', 'grep', 'blame', 'describe', 'shortlog', 'help', 'version', 'count-objects', 'fsck']);
|
|
22
|
+
/** 护栏用增强写意图:BASH_WRITE_RE(写动词 + 带空格重定向)∪ 无空格重定向(fd2 stderr 豁免用 lookbehind:
|
|
23
|
+
* 重定向符前(首个 > 前)字符非 '2' 才算写意图,且禁止从 >> 的第二个 > 起匹配;2>/dev/null、2>&1、
|
|
24
|
+
* 2>>err.log 放行,1>/0>/>file/x> 一律命中)∪ 解释器 -c/-e 单行(node/python3/perl/ruby/sh/bash 等
|
|
25
|
+
* 可直接执行任意文件写 API)∪ 原地编辑器 -i(sed -i / perl -i / awk -i inplace:可越过重定向直达
|
|
26
|
+
* 源码原地改写,P 写护栏硬约束须识别,与 BASH_WRITE_RE 同源)。 */
|
|
27
|
+
const GUARD_WRITE_INTENT_RE = /(?:\b(?:touch|mkdir|rm|rmdir|mv|cp|tee|truncate|install|ln|dd|chmod|chown|make|cmake)\b|\b(?:sed|perl|awk)\s+-i\b|\b(?:node|python|python3|perl|ruby|php|sh|bash)\s+-[ec]\b|(?<!2)>>|(?<!2)(?<!>)>)/i;
|
|
28
|
+
/** 剥去 shell 重定向目标 token 外壳的一层引号(' " `)(F3:引号包裹的 plan 路径被 I1 误拒修复)。
|
|
29
|
+
* 仅剥对称外壳一层;剥完仍走 resolve+isPlanPath,.. / 绝对路径逃逸不被削弱。 */
|
|
30
|
+
function stripShellQuotes(tok) {
|
|
31
|
+
const q = tok[0];
|
|
32
|
+
if (tok.length >= 2 && (q === "'" || q === '"' || q === '`') && tok[tok.length - 1] === q) {
|
|
33
|
+
return tok.slice(1, -1);
|
|
34
|
+
}
|
|
35
|
+
return tok;
|
|
36
|
+
}
|
|
37
|
+
/** I1:从 bash/run_code 写意图命令提取实际写目标路径。
|
|
38
|
+
* 返回重定向(>/>> 后首个非重定向 token,fd2 以 lookbehind 豁免)与 writeFileSync(/appendFileSync(
|
|
39
|
+
* 首个字符串实参;提取不到返回空数组(调用方据此 fail-closed 或放行)。 */
|
|
40
|
+
function extractWriteTargets(cmd) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const redirectRe = /(?:(?<!2)>>|(?<!2)(?<!>)>)\s*([^\s;&|<>]+)/g;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = redirectRe.exec(cmd)) !== null) {
|
|
45
|
+
if (m[1])
|
|
46
|
+
out.push(stripShellQuotes(m[1]));
|
|
47
|
+
}
|
|
48
|
+
const apiRe = /\b(?:writeFileSync|appendFileSync)\s*\(\s*(['"`])(.*?)\1/g;
|
|
49
|
+
while ((m = apiRe.exec(cmd)) !== null) {
|
|
50
|
+
if (m[2])
|
|
51
|
+
out.push(m[2]);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** 判定 wiki 路径是否位于 DT 评审命名空间 projects/<chain>/review/(拒绝 ../、绝对路径、非 review 前缀)。 */
|
|
56
|
+
export function isReviewNamespacePath(pagePath, chainId) {
|
|
57
|
+
const p = String(pagePath ?? '');
|
|
58
|
+
if (!p || p.startsWith('/') || p.includes('..'))
|
|
59
|
+
return false;
|
|
60
|
+
const prefix = `projects/${chainId}/review/`;
|
|
61
|
+
return p.startsWith(prefix);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 评审引擎决策(DT):ocr(open-code-review Delegation 模式)优先;
|
|
65
|
+
* 不可用 fallback superpowers code-review;两者都不可用 → review-tool-unavailable(阻塞)。
|
|
66
|
+
* 纯函数便于单测;真实可用性探测在 agent-runner 装配(探活 ocr 二进制/失败)。
|
|
67
|
+
*/
|
|
68
|
+
export function resolveReviewEngine(available) {
|
|
69
|
+
if (available.ocr)
|
|
70
|
+
return 'ocr';
|
|
71
|
+
if (available.codeReview)
|
|
72
|
+
return 'code-review';
|
|
73
|
+
return 'review-tool-unavailable';
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* DT 写护栏 = PT 只读护栏(源码/git/写标记 bash 拒绝)+ wiki_write 仅 review namespace 收窄。
|
|
77
|
+
* repoRoot 为 D 目标仓库;chainId 用于 wiki 评审命名空间校验。
|
|
78
|
+
*/
|
|
79
|
+
export function buildDTWriteGuard(repoRoot, chainId) {
|
|
80
|
+
const base = buildReadOnlyWriteGuard(repoRoot);
|
|
81
|
+
return (execution) => {
|
|
82
|
+
const name = String(execution?.name ?? '');
|
|
83
|
+
if (name === 'wiki_write') {
|
|
84
|
+
const args = execution?.arguments ?? {};
|
|
85
|
+
const pagePath = String(args && typeof args === 'object' ? args['pagePath'] ?? '' : '');
|
|
86
|
+
if (!isReviewNamespacePath(pagePath, chainId))
|
|
87
|
+
return 'wiki-write-outside-review-namespace: DT may only write projects/<chain>/review/';
|
|
88
|
+
}
|
|
89
|
+
return base(execution);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function buildReadOnlyWriteGuard(_repoRoot) {
|
|
93
|
+
return (execution) => {
|
|
94
|
+
const name = String(execution?.name ?? '');
|
|
95
|
+
const args = execution?.arguments ?? {};
|
|
96
|
+
// I2:全名拦截——只读会话(W/PT/DT)fs 写无条件拒绝,不再依赖 repoRoot 子串 / hitsRepo。
|
|
97
|
+
// W 是 danger-full-access(无 workspace-write sandbox 兜底),可写 repo 外任意路径(~/x、/tmp/x、
|
|
98
|
+
// 其他项目源码),故必须工具级全名拦截封死。非写工具(read/glob/grep/wiki_* 等)照常放行。
|
|
99
|
+
if (DIRECT_WRITE_TOOLS.has(name))
|
|
100
|
+
return 'write-to-repo-source-denied: read-only reviewer must not modify repo sources';
|
|
101
|
+
if (name === 'bash' || name === 'run_code') {
|
|
102
|
+
const cmd = String(args && typeof args === 'object' ? (args['command'] ?? args['code'] ?? '') : '');
|
|
103
|
+
// bash 用 BASH_WRITE_RE(写动词 + git mutation + 带空格重定向)∪ GUARD_WRITE_INTENT_RE
|
|
104
|
+
// (无空格重定向 + 解释器 -c/-e)——全名拦截,写标记即拒,无论目标是否在 repo 内。
|
|
105
|
+
// run_code 用 JS/Python 文件写 API 标记(CODE_WRITE_RE)。
|
|
106
|
+
const writeRe = name === 'run_code' ? CODE_WRITE_RE : BASH_WRITE_RE;
|
|
107
|
+
if (cmd && (writeRe.test(cmd) || GUARD_WRITE_INTENT_RE.test(cmd)))
|
|
108
|
+
return 'write-to-repo-source-denied: ' + name + ' with write marker';
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** P 专用写护栏(Q3):读全放行;git mutation 一律拒绝;写仅允许目标仓库 openspec/changes 目录。
|
|
114
|
+
* 直接 fs 写工具 → 路径经 resolve 归一化后须落在 <workspaceRoot>/openspec/changes/ 之下(相邻段对判定);
|
|
115
|
+
* bash/run_code 写标记命令 → 命令文本须含 `openspec/changes` 子串,且提取出的实际写目标(重定向
|
|
116
|
+
* 目标 / writeFileSync 实参)逐条经 resolve+isPlanPath 校验(I1:杀 openspec/changes/../.. 穿越写源码)。
|
|
117
|
+
* 源码/src/lib/tests 等写不入(不含该子串)——"禁止改动源码"为工具级硬约束,非 prompt 软约束。
|
|
118
|
+
* execution 以 dsh-tools 形态 { name, arguments } 传入(与 buildReadOnlyWriteGuard 一致)。 */
|
|
119
|
+
export function buildPlanWriteGuard(workspaceRoot) {
|
|
120
|
+
const wsRoot = workspaceRoot.replace(/\/+$/, '');
|
|
121
|
+
/** 目标路径须解析后落在 wsRoot 之下、且含相邻 openspec→changes 段对。
|
|
122
|
+
* resolve 折叠 ../ 与重复斜杠(杀 M1 .. 穿越);wsRoot + '/' 边界前缀(杀 M2 /ws/main2 前缀逃逸);
|
|
123
|
+
* 相对路径(openspec/changes/x.md)经 resolve 归到 wsRoot 之下 → 允许(m2 回归)。 */
|
|
124
|
+
const isPlanPath = (p) => {
|
|
125
|
+
if (!p)
|
|
126
|
+
return false;
|
|
127
|
+
const resolved = resolve(wsRoot, p);
|
|
128
|
+
// POSIX 上 \ 是合法文件名字符(如目录名 openspec\changes),反斜杠归一化仅 win32 需要,
|
|
129
|
+
// 否则会把它折叠成相邻 openspec→changes 段对而被误放行(R2-F7)。
|
|
130
|
+
const norm = process.platform === 'win32' ? resolved.replace(/\\/g, '/') : resolved;
|
|
131
|
+
if (!norm.startsWith(wsRoot + '/'))
|
|
132
|
+
return false;
|
|
133
|
+
const segs = norm.split('/');
|
|
134
|
+
const i = segs.indexOf('openspec');
|
|
135
|
+
return i >= 0 && segs[i + 1] === 'changes';
|
|
136
|
+
};
|
|
137
|
+
const isPlanCmd = (cmd) => cmd.includes('openspec/changes');
|
|
138
|
+
return (execution) => {
|
|
139
|
+
const name = String(execution?.name ?? '');
|
|
140
|
+
const args = execution?.arguments ?? {};
|
|
141
|
+
const a = args && typeof args === 'object' ? args : {};
|
|
142
|
+
if (DIRECT_WRITE_TOOLS.has(name)) {
|
|
143
|
+
const target = String(a['path'] ?? a['file_path'] ?? '');
|
|
144
|
+
if (isPlanPath(target))
|
|
145
|
+
return undefined;
|
|
146
|
+
return 'plan-guard: P 写仅允许 openspec/changes/ 目录(禁止改动源码)';
|
|
147
|
+
}
|
|
148
|
+
if (name === 'bash' || name === 'run_code') {
|
|
149
|
+
const cmd = String(a['command'] ?? a['code'] ?? '');
|
|
150
|
+
if (!cmd)
|
|
151
|
+
return undefined;
|
|
152
|
+
// FIRST git 判定(仅命令文本,不扫写入内容——修 m1 内容误拒):
|
|
153
|
+
// 按 && / || / ; / | / 换行 分段,逐段提取首个 git 动词(先跳过 git 全局选项
|
|
154
|
+
// --no-pager/-p/-v/--bare/--literal-pathspecs/--no-replace-objects/-C <path>/-c <kv>/
|
|
155
|
+
// --git-dir=/--work-tree=/--namespace=)。任何一段含 git 却提取不到动词(如 git --version
|
|
156
|
+
// 或裸 git)→ fail-closed 拒绝;动词非只读白名单 → 拒绝。修 R2 链式绕过与全局选项前缀 fall-through。
|
|
157
|
+
const segments = cmd.split(/\s*(?:&&|\|\||;|\||\n)\s*/);
|
|
158
|
+
for (const seg of segments) {
|
|
159
|
+
if (!/\bgit\b/.test(seg))
|
|
160
|
+
continue;
|
|
161
|
+
const verbMatch = seg.match(/\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-]*)/);
|
|
162
|
+
const verb = verbMatch ? verbMatch[1] : undefined;
|
|
163
|
+
if (!verb || !GIT_READ_VERBS.has(verb))
|
|
164
|
+
return 'plan-guard: P 禁止 git 操作';
|
|
165
|
+
}
|
|
166
|
+
// THEN 写意图:增强写标记(含无空格重定向与解释器 -c/-e 单行)且不含 plan 路径 → 拒绝。
|
|
167
|
+
// run_code 额外用文件写 API 标记(CODE_WRITE_RE,fs.writeFileSync 等);保留 GUARD 动词/解释器
|
|
168
|
+
// 覆盖(touch/mkdir/rm/… 与 python -c 内联单行)不回归。
|
|
169
|
+
const isWrite = name === 'run_code'
|
|
170
|
+
? CODE_WRITE_RE.test(cmd) || GUARD_WRITE_INTENT_RE.test(cmd)
|
|
171
|
+
: GUARD_WRITE_INTENT_RE.test(cmd);
|
|
172
|
+
if (isWrite) {
|
|
173
|
+
if (!isPlanCmd(cmd))
|
|
174
|
+
return 'plan-guard: P 写仅允许 openspec/changes/ 目录(禁止改动源码)';
|
|
175
|
+
// I1:含 plan 标记仍须验证实际写目标——重定向 >/>> 目标与 writeFileSync(/appendFileSync(
|
|
176
|
+
// 首个字符串实参,逐条 resolve + isPlanPath(与 write/edit 入口同款判定,杀 M1 同款
|
|
177
|
+
// openspec/changes/../.. 穿越写源码,补齐 bash/run_code 入口)。任一条目标不通过 → 拒绝。
|
|
178
|
+
const targets = extractWriteTargets(cmd);
|
|
179
|
+
if (targets.length === 0) {
|
|
180
|
+
// 快速防线:命令同时含 openspec/changes 与 .. 但目标解析不出(无法提取)→ fail-closed 拒绝
|
|
181
|
+
if (cmd.includes('..'))
|
|
182
|
+
return 'plan-guard: P 写仅允许 openspec/changes/ 目录(禁止改动源码)';
|
|
183
|
+
return undefined; // 无重定向/写 API 目标(如 touch openspec/changes/x)→ 放行(保留 allow 标记语义)
|
|
184
|
+
}
|
|
185
|
+
for (const t of targets) {
|
|
186
|
+
if (!isPlanPath(t))
|
|
187
|
+
return 'plan-guard: P 写仅允许 openspec/changes/ 目录(禁止改动源码)';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** 按角色在 agent scope 注册工具面(P1-3 统一注册策略):
|
|
195
|
+
* 所有 kanban 工具从 T9 工厂选取 + getCaller 闭包(actor=role、boundTaskId=taskId)。
|
|
196
|
+
* can() 权限兜底仍保留在工具 execute 内(纵深防御第二道)。 */
|
|
197
|
+
export async function installRoleTools(agentCtx, role, deps) {
|
|
198
|
+
console.error('[dsh-swarm][debug] installRoleTools role=' + role + ' task=' + deps.taskId);
|
|
199
|
+
const caller = () => ({ actor: role, boundTaskId: deps.taskId });
|
|
200
|
+
const allKanban = buildKanbanTools(deps.kanban, caller);
|
|
201
|
+
// 每角色可用的 kanban 工具名(V 额外编排、P/W/D 任务工具)
|
|
202
|
+
// 设计表(§3)另有 V 专属 kanban_link/chain_show,src/tools/kanban-tools.ts 未实现这两项,
|
|
203
|
+
// 故保持不注册(不为实现而实现多余工具),与设计表的差异以此注释声明。
|
|
204
|
+
const namesFor = {
|
|
205
|
+
v: ['kanban_create', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment', 'kanban_show', 'kanban_chain', 'kanban_list'],
|
|
206
|
+
p: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
207
|
+
w: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
208
|
+
d: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
209
|
+
// 评审角色(Task 8/9 正式装配工具面):PT/DT 任务工具 + 只读(spec 视图等)
|
|
210
|
+
pt: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
211
|
+
dt: ['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_complete', 'kanban_block', 'kanban_heartbeat', 'kanban_comment'],
|
|
212
|
+
};
|
|
213
|
+
const want = new Set(namesFor[role]);
|
|
214
|
+
const registry = agentCtx.tools;
|
|
215
|
+
if (!registry)
|
|
216
|
+
return; // 无工具服务(测试桩)跳过
|
|
217
|
+
for (const tool of allKanban) {
|
|
218
|
+
const name = tool.name;
|
|
219
|
+
if (name && want.has(name))
|
|
220
|
+
registry.register(tool);
|
|
221
|
+
}
|
|
222
|
+
if (role === 'w') {
|
|
223
|
+
for (const tool of buildWikiTools(deps.wiki, caller))
|
|
224
|
+
registry.register(tool);
|
|
225
|
+
// 设计表 §3:W 对规格卡只读(spec_card_view)
|
|
226
|
+
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
227
|
+
if (tool.name === 'spec_card_view')
|
|
228
|
+
registry.register(tool);
|
|
229
|
+
}
|
|
230
|
+
const worker = new WikiWorker(deps.kanban, deps.wiki, { pagePrefix: 'projects/' });
|
|
231
|
+
const getTask = async (taskId) => {
|
|
232
|
+
const state = await deps.kanban.snapshot();
|
|
233
|
+
const t = state.tasks.get(taskId);
|
|
234
|
+
if (!t)
|
|
235
|
+
throw new Error('unknown task: ' + taskId);
|
|
236
|
+
return t;
|
|
237
|
+
};
|
|
238
|
+
for (const tool of buildPrefetchTools(worker, getTask, caller))
|
|
239
|
+
registry.register(tool);
|
|
240
|
+
}
|
|
241
|
+
else if (role === 'd') {
|
|
242
|
+
// D:只读 KB——注册 wiki_read + wiki_search(均走 can('wiki-read')=w/d 只读兜底);规格卡只读
|
|
243
|
+
for (const tool of buildWikiTools(deps.wiki, caller)) {
|
|
244
|
+
const name = tool.name;
|
|
245
|
+
if (name === 'wiki_read' || name === 'wiki_search')
|
|
246
|
+
registry.register(tool);
|
|
247
|
+
}
|
|
248
|
+
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
249
|
+
if (tool.name === 'spec_card_view')
|
|
250
|
+
registry.register(tool);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (role === 'p') {
|
|
254
|
+
// P:spec_card_view(只读)+ openspec 写工具由 base 提供
|
|
255
|
+
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
256
|
+
if (tool.name === 'spec_card_view')
|
|
257
|
+
registry.register(tool);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (role === 'pt') {
|
|
261
|
+
// PT:只读评审——spec_card_view + 任务工具(无 create/wiki/执行);写护栏在 agent-runner 装配
|
|
262
|
+
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
263
|
+
if (tool.name === 'spec_card_view')
|
|
264
|
+
registry.register(tool);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (role === 'dt') {
|
|
268
|
+
// DT:只读评审——spec_card_view + wiki 只读(评审区写由 ToolGuard 收窄);Task 9 完整实现
|
|
269
|
+
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
270
|
+
if (tool.name === 'spec_card_view')
|
|
271
|
+
registry.register(tool);
|
|
272
|
+
}
|
|
273
|
+
for (const tool of buildWikiTools(deps.wiki, caller)) {
|
|
274
|
+
const n = tool.name;
|
|
275
|
+
if (n === 'wiki_read' || n === 'wiki_search' || n === 'wiki_write')
|
|
276
|
+
registry.register(tool);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (role === 'v') {
|
|
280
|
+
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
281
|
+
if (tool.name === 'spec_card_view')
|
|
282
|
+
registry.register(tool);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ── 0.1.0 delegation:全局子代理写护栏(spec FR2)────────────────────────────
|
|
287
|
+
// 框架事实(spec §3):agent.ctx guard 不被子代理继承(F1);toolFilter 对 preset
|
|
288
|
+
// scoped 工具零作用(F2)。故 DT 子代理只读防线只能在插件全局 ctx 注册 guard(全局
|
|
289
|
+
// 生效),按发起 agent 会话 header 的 agentPreset 精准判定(childSessionMeta 在子代理
|
|
290
|
+
// join 父组合时记录 preset id,F4):仅 kanban-dt 系收紧,其余(kanban-d 系、主会话
|
|
291
|
+
// 及其子代理)一律放行。
|
|
292
|
+
/** DT 任务运行期 taskId → chainId 同步缓存(guard 内 wiki review namespace 校验的
|
|
293
|
+
* 解析源;AgentRunner 在 DT 任务 runTask 生命周期内维护,与 permissionBlockedTasks
|
|
294
|
+
* 同为 module-level 进程内记忆)。 */
|
|
295
|
+
const dtTaskChainIds = new Map();
|
|
296
|
+
export function registerDtTaskChain(taskId, chainId) {
|
|
297
|
+
dtTaskChainIds.set(taskId, chainId);
|
|
298
|
+
}
|
|
299
|
+
export function unregisterDtTaskChain(taskId) {
|
|
300
|
+
dtTaskChainIds.delete(taskId);
|
|
301
|
+
}
|
|
302
|
+
/** 从 execution.agent 提取 session header(真实形态 agent.session.header,dsh-agent
|
|
303
|
+
* Session.header;取不到返回 undefined → 放行,DT 父会话 agent.ctx guard 兜底)。 */
|
|
304
|
+
function extractSessionHeader(agent) {
|
|
305
|
+
const a = agent;
|
|
306
|
+
const h = a?.session?.header;
|
|
307
|
+
return h && typeof h === 'object' ? h : undefined;
|
|
308
|
+
}
|
|
309
|
+
/** 全局子代理写护栏:仅 DT 角色会话的"子代理"(agentPreset === 'kanban-dt' 且
|
|
310
|
+
* header.parentSession 为 kbn-<taskId> 前缀)应用 buildDTWriteGuard。判据:parentSession
|
|
311
|
+
* 缺失或非 kbn- 前缀 → 放行(DT 父会话自身或无关会话;DT 父会话只读由 agent.ctx guard
|
|
312
|
+
* 兜底,双保险)。repoRoot 取子代理 header.cwd(继承 DT 会话 cwd=评审目标仓库);缺省
|
|
313
|
+
* '/'(写标记全拦的保守形态)。chainId 从 parentSession(kbn-<taskId>)解析;解析不到
|
|
314
|
+
* → 空(wiki_write fail-closed 全拒,源码写拦截不受影响)。 */
|
|
315
|
+
export function buildSubagentTreeGuard(deps = {}) {
|
|
316
|
+
return (execution) => {
|
|
317
|
+
const header = extractSessionHeader(execution?.agent);
|
|
318
|
+
if (!header || header.agentPreset !== 'kanban-dt')
|
|
319
|
+
return undefined;
|
|
320
|
+
// 仅真实子代理(parentSession 为 kbn- 前缀)受全局护栏约束;DT 父会话自身
|
|
321
|
+
// parentSession 是主会话或缺失(非 kbn- 前缀),chainId 解析不到 → 空,若误拦
|
|
322
|
+
// 会把 DT 评审写入(wiki_write projects/<chain>/review/...)拒掉 → 直接放行。
|
|
323
|
+
const parent = header.parentSession;
|
|
324
|
+
if (typeof parent !== 'string' || !parent.startsWith('kbn-'))
|
|
325
|
+
return undefined;
|
|
326
|
+
const repoRoot = header.cwd || '/';
|
|
327
|
+
let chainId = '';
|
|
328
|
+
const taskId = parent.slice('kbn-'.length);
|
|
329
|
+
chainId = deps.getTaskChainId?.(taskId) ?? dtTaskChainIds.get(taskId) ?? '';
|
|
330
|
+
return buildDTWriteGuard(repoRoot, chainId)(execution);
|
|
331
|
+
};
|
|
332
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Task } from '../domain/types.js';
|
|
2
|
+
import type { KanbanService } from '../domain/kanban-service.js';
|
|
3
|
+
import type { WikiVaultClient } from '../wiki/wiki-vault-client.js';
|
|
4
|
+
/** W 角色的领域执行:预取(原汁原味只读)与 KB 同步。 */
|
|
5
|
+
export declare class WikiWorker {
|
|
6
|
+
private readonly kanban;
|
|
7
|
+
private readonly wiki;
|
|
8
|
+
private readonly cfg;
|
|
9
|
+
constructor(kanban: KanbanService, wiki: WikiVaultClient, cfg: {
|
|
10
|
+
pagePrefix: string;
|
|
11
|
+
});
|
|
12
|
+
executePrefetch(task: Task, mode: 'file' | 'external' | 'kb', source: string): Promise<{
|
|
13
|
+
ref: string;
|
|
14
|
+
}>;
|
|
15
|
+
syncToWiki(task: Task, sourceRef: string): Promise<{
|
|
16
|
+
kb_url: string;
|
|
17
|
+
page_path: string;
|
|
18
|
+
}>;
|
|
19
|
+
private wikiBase;
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
function workspaceOf(chainId, taskId) {
|
|
4
|
+
return join(process.env.DSH_HOME ?? process.cwd(), 'storages', 'kanban', 'workspaces', chainId, taskId);
|
|
5
|
+
}
|
|
6
|
+
/** W 角色的领域执行:预取(原汁原味只读)与 KB 同步。 */
|
|
7
|
+
export class WikiWorker {
|
|
8
|
+
kanban;
|
|
9
|
+
wiki;
|
|
10
|
+
cfg;
|
|
11
|
+
constructor(kanban, wiki, cfg) { this.kanban = kanban; this.wiki = wiki; this.cfg = cfg; }
|
|
12
|
+
async executePrefetch(task, mode, source) {
|
|
13
|
+
const ws = workspaceOf(task.chainId, task.id);
|
|
14
|
+
// P1-6:三模式全部落地;产物一律写入任务工作区(原汁原味,禁压缩/蒸馏)
|
|
15
|
+
if (mode === 'file') {
|
|
16
|
+
// 只读命令由角色 agent 的 terminal 白名单执行;此处仅登记产物引用
|
|
17
|
+
if (!source.startsWith(ws))
|
|
18
|
+
throw new Error('prefetch source outside workspace: ' + source);
|
|
19
|
+
return { ref: source };
|
|
20
|
+
}
|
|
21
|
+
if (mode === 'external') {
|
|
22
|
+
// 外网资料查询(multi-search-engine 语义由角色 agent 的 web 工具执行);
|
|
23
|
+
// 此处登记产物引用:agent 完成检索后把原汁原味结果写入 ws/prefetch-external.md
|
|
24
|
+
const ref = join(ws, 'prefetch-external.md');
|
|
25
|
+
if (!source.startsWith(ws) && source !== '')
|
|
26
|
+
throw new Error('prefetch source outside workspace: ' + source);
|
|
27
|
+
return { ref: source || ref };
|
|
28
|
+
}
|
|
29
|
+
// kb 模式:知识库查询产物(wiki-vault search/read 由 wiki 工具执行,agent 落盘 ws/prefetch-kb.md)
|
|
30
|
+
const ref = join(ws, 'prefetch-kb.md');
|
|
31
|
+
if (!source.startsWith(ws) && source !== '')
|
|
32
|
+
throw new Error('prefetch source outside workspace: ' + source);
|
|
33
|
+
return { ref: source || ref };
|
|
34
|
+
}
|
|
35
|
+
async syncToWiki(task, sourceRef) {
|
|
36
|
+
const content = readFileSync(sourceRef, 'utf8'); // 原汁原味:不压缩不蒸馏
|
|
37
|
+
const pagePath = this.cfg.pagePrefix + task.chainId + '/' + task.id + '.md';
|
|
38
|
+
await this.wiki.write(pagePath, content);
|
|
39
|
+
return { kb_url: this.wikiBase() + '/#/page/' + pagePath, page_path: pagePath };
|
|
40
|
+
}
|
|
41
|
+
wikiBase() {
|
|
42
|
+
return this.wiki.baseUrl; // P2:用 WikiVaultClient 公开 getter,不做类型强转
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { KanbanConfig } from '../config.js';
|
|
3
|
+
import type { KanbanProvider } from '../services/kanban-provider.js';
|
|
4
|
+
/** 看板 HTTP 桥(Web GUI 浏览器半消费):GET /kanban/board 读快照;POST /kanban/action 执行状态操作。
|
|
5
|
+
* 仅在 webServer 服务存在时挂载(CLI/headless/测试裸 Context 不挂)。 */
|
|
6
|
+
export declare function registerKanbanHttp(ctx: Context, provider: KanbanProvider, config?: Pick<KanbanConfig, 'ui'>): void;
|