dsh-plugin-manager-companion 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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +144 -0
  3. package/README.md +142 -0
  4. package/cordis.patch.yml +9 -0
  5. package/dist/about.d.ts +77 -0
  6. package/dist/about.js +179 -0
  7. package/dist/cli.d.ts +226 -0
  8. package/dist/cli.js +856 -0
  9. package/dist/client/AboutPage.d.ts +75 -0
  10. package/dist/client/ConsolePage.d.ts +79 -0
  11. package/dist/client/KindsPage.d.ts +21 -0
  12. package/dist/client/MarketplacePage.d.ts +36 -0
  13. package/dist/client/OfficialSlots.d.ts +35 -0
  14. package/dist/client/UpgradeRow.d.ts +108 -0
  15. package/dist/client/index.d.ts +26 -0
  16. package/dist/client/locales.d.ts +475 -0
  17. package/dist/client/pmSelect.d.ts +38 -0
  18. package/dist/client/shared.d.ts +928 -0
  19. package/dist/client/upgradeView.d.ts +278 -0
  20. package/dist/client/wire.d.ts +401 -0
  21. package/dist/client.js +9194 -0
  22. package/dist/diagnostics.d.ts +332 -0
  23. package/dist/diagnostics.js +2631 -0
  24. package/dist/envManager.d.ts +1047 -0
  25. package/dist/envManager.js +3214 -0
  26. package/dist/fix.d.ts +60 -0
  27. package/dist/fix.js +168 -0
  28. package/dist/guard.d.ts +133 -0
  29. package/dist/guard.js +232 -0
  30. package/dist/index.d.ts +121 -0
  31. package/dist/index.js +1150 -0
  32. package/dist/installSession.d.ts +111 -0
  33. package/dist/installSession.js +150 -0
  34. package/dist/kinds.d.ts +464 -0
  35. package/dist/kinds.js +1029 -0
  36. package/dist/marketView.d.ts +261 -0
  37. package/dist/marketView.js +406 -0
  38. package/dist/marketplace.d.ts +248 -0
  39. package/dist/marketplace.js +500 -0
  40. package/dist/match.d.ts +67 -0
  41. package/dist/match.js +203 -0
  42. package/dist/net.d.ts +108 -0
  43. package/dist/net.js +163 -0
  44. package/dist/official.d.ts +145 -0
  45. package/dist/official.js +205 -0
  46. package/dist/paths.d.ts +108 -0
  47. package/dist/paths.js +236 -0
  48. package/dist/presets.d.ts +299 -0
  49. package/dist/presets.js +578 -0
  50. package/dist/qualityGate.d.ts +66 -0
  51. package/dist/qualityGate.js +247 -0
  52. package/dist/rank.d.ts +88 -0
  53. package/dist/rank.js +164 -0
  54. package/dist/registry.d.ts +295 -0
  55. package/dist/registry.js +686 -0
  56. package/dist/rest.d.ts +122 -0
  57. package/dist/rest.js +219 -0
  58. package/dist/scan.d.ts +134 -0
  59. package/dist/scan.js +396 -0
  60. package/dist/settings.d.ts +447 -0
  61. package/dist/settings.js +263 -0
  62. package/dist/tags.d.ts +119 -0
  63. package/dist/tags.js +166 -0
  64. package/dist/tools.d.ts +131 -0
  65. package/dist/tools.js +377 -0
  66. package/dist/types.d.ts +651 -0
  67. package/dist/types.js +13 -0
  68. package/dist/upgrade.d.ts +428 -0
  69. package/dist/upgrade.js +1100 -0
  70. package/dist/upgradeView.d.ts +313 -0
  71. package/dist/upgradeView.js +273 -0
  72. package/docs/images/readme/01-console-health.png +0 -0
  73. package/docs/images/readme/02-console-envs.png +0 -0
  74. package/docs/images/readme/03-marketplace.png +0 -0
  75. package/docs/images/readme/04-official-plugin-page.png +0 -0
  76. package/package.json +104 -0
package/dist/fix.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 分级修复:把诊断给出的 fix descriptor 变成实际动作。
3
+ *
4
+ * 归属:A 类·重写(旧仓库的 fixIssue/fixAll 在 index.ts 里,且直接写 patch 文件)。
5
+ * 官方复用:pluginManager.setPluginEnabled(行级启停)、installBundle(装包)、
6
+ * saveManifest + withFileLock(声明改写)。
7
+ * 前提检查:旧仓库有 A/B/C 三级修复,其中"删重复行"直接改 cordis.patch.yml。
8
+ * 前提已变:官方现在拥有组合的写权,而它**没有**删行能力。我们不再自己写那个文件,
9
+ * 改为如实回 needs-manual —— 见 applyFix 的 JSDoc。
10
+ */
11
+ import type { Context } from "@deepseek-ai/cordis";
12
+ /**
13
+ * 一次修复的执行结果。
14
+ *
15
+ * `needs-manual` 不是失败:它是"这条修复没有官方通道,我们拒绝自己动手写 profile 组合",
16
+ * 并把精确到行的操作步骤交回用户。与"执行失败"分开报,用户才知道该做什么。
17
+ */
18
+ export interface FixOutcome {
19
+ readonly ok: boolean;
20
+ readonly action: string;
21
+ readonly target?: string;
22
+ /** executed = 已执行;needs-manual = 需用户手工;failed = 执行失败。 */
23
+ readonly status: "executed" | "needs-manual" | "failed";
24
+ readonly output: string;
25
+ }
26
+ /** applyFix 的依赖(由入口注入,避免 fix.ts 反向依赖 index.ts)。 */
27
+ export interface FixDependencies {
28
+ readonly ctx: Context;
29
+ readonly environmentName: () => string | null;
30
+ /** 受质量门保护的安装(官方 add 通道);由入口提供(它需要 config 与官方 Remote 编排)。 */
31
+ readonly install: (spec: string) => Promise<{
32
+ readonly ok: boolean;
33
+ readonly output: string;
34
+ }>;
35
+ /**
36
+ * 修复安装(官方 install 通道):把 profile **已声明**的依赖真正装进 node_modules。
37
+ *
38
+ * @param target - 包名(用于文案;官方 install 按 package.json 全量收敛)。
39
+ * @returns 结果。
40
+ */
41
+ readonly repair: (target: string) => Promise<{
42
+ readonly ok: boolean;
43
+ readonly output: string;
44
+ }>;
45
+ }
46
+ /**
47
+ * 执行一条诊断给出的修复。
48
+ *
49
+ * 三条路,按"官方有没有现成通道"划分:
50
+ * 1. 行级启停 → 官方 setPluginEnabled(enable-row / disable-row)
51
+ * 2. 装包 → 入口提供的通道:install-provider 走受质量门保护的 add,
52
+ * install-dependency 走官方 install(声明已在、只是没装,add 必被 already-installed 拒绝)
53
+ * 3. 删重复官方包拷贝 → 官方 withFileLock + 官方 saveManifest(remove-official-copy)
54
+ *
55
+ * @param action - 修复动作(诊断给出的闭集取值)。
56
+ * @param target - 动作目标,语义随 action 不同。
57
+ * @param deps - 依赖。
58
+ * @returns 执行结果。
59
+ */
60
+ export declare function applyFix(action: string, target: string | undefined, deps: FixDependencies): Promise<FixOutcome>;
package/dist/fix.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * 分级修复:把诊断给出的 fix descriptor 变成实际动作。
3
+ *
4
+ * 归属:A 类·重写(旧仓库的 fixIssue/fixAll 在 index.ts 里,且直接写 patch 文件)。
5
+ * 官方复用:pluginManager.setPluginEnabled(行级启停)、installBundle(装包)、
6
+ * saveManifest + withFileLock(声明改写)。
7
+ * 前提检查:旧仓库有 A/B/C 三级修复,其中"删重复行"直接改 cordis.patch.yml。
8
+ * 前提已变:官方现在拥有组合的写权,而它**没有**删行能力。我们不再自己写那个文件,
9
+ * 改为如实回 needs-manual —— 见 applyFix 的 JSDoc。
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { rm } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { environmentDir } from "./paths.js";
15
+ import { requireManager } from "./official.js";
16
+ /**
17
+ * 需要用户手工处理的两条:它们都要求改 cordis.patch.yml 的**结构**(删行)。
18
+ *
19
+ * 官方只有行级 setPluginEnabled(改某一行的 disabled),没有删行能力。我们自己写
20
+ * 这个文件会引入"两个写者并发改同一份组合"——那正是本仓库要消除的事故形态。
21
+ */
22
+ const MANUAL_ACTIONS = new Set(["remove-duplicate-row", "remove-row"]);
23
+ /**
24
+ * 装包类动作分两条通道,**不能合并**:
25
+ *
26
+ * - install-provider:某个插件 import 了一个没被声明的包 → 要 `add` 它。
27
+ * - install-dependency:package.json **已经声明**了,但 node_modules 里没有 → 要修复安装。
28
+ *
29
+ * 为什么 install-dependency 不能走 `add`:官方 inspect 把「已声明」当作「已安装」
30
+ * (already-installed 是官方 PluginInspectProblem 闭集里的取值),于是 `add` 必然被拒绝,
31
+ * 输出「拒绝安装:already-installed」——与诊断结论直接矛盾,用户点多少次都不会成功
32
+ * (write-auditor 真机验证发现,见 docs/private/write-path-audit.md §3·P3)。
33
+ * 官方 `dsh plugin --profile X install` 是把参数转发给 pnpm 的官方通道,修复安装用它。
34
+ */
35
+ const ADD_ACTIONS = new Set(["install-provider"]);
36
+ /** 修复安装类动作:声明齐全但 node_modules 缺失,走官方 install 通道。 */
37
+ const REPAIR_ACTIONS = new Set(["install-dependency"]);
38
+ /**
39
+ * 执行一条诊断给出的修复。
40
+ *
41
+ * 三条路,按"官方有没有现成通道"划分:
42
+ * 1. 行级启停 → 官方 setPluginEnabled(enable-row / disable-row)
43
+ * 2. 装包 → 入口提供的通道:install-provider 走受质量门保护的 add,
44
+ * install-dependency 走官方 install(声明已在、只是没装,add 必被 already-installed 拒绝)
45
+ * 3. 删重复官方包拷贝 → 官方 withFileLock + 官方 saveManifest(remove-official-copy)
46
+ *
47
+ * @param action - 修复动作(诊断给出的闭集取值)。
48
+ * @param target - 动作目标,语义随 action 不同。
49
+ * @param deps - 依赖。
50
+ * @returns 执行结果。
51
+ */
52
+ export async function applyFix(action, target, deps) {
53
+ if (MANUAL_ACTIONS.has(action))
54
+ return manualOutcome(action, target);
55
+ if (ADD_ACTIONS.has(action) || REPAIR_ACTIONS.has(action)) {
56
+ if (target === undefined) {
57
+ return failed(action, ADD_ACTIONS.has(action) ? "缺少 target(要安装的 spec)" : "缺少 target(包名)");
58
+ }
59
+ const result = ADD_ACTIONS.has(action) ? await deps.install(target) : await deps.repair(target);
60
+ return {
61
+ ok: result.ok, action, target,
62
+ status: result.ok ? "executed" : "failed", output: result.output,
63
+ };
64
+ }
65
+ try {
66
+ switch (action) {
67
+ case "enable-row":
68
+ case "disable-row": {
69
+ if (target === undefined)
70
+ return failed(action, "缺少 target(loader 行 id)");
71
+ const manager = requireManager(deps.ctx);
72
+ const enabled = action === "enable-row";
73
+ const change = await manager.setPluginEnabled(target, enabled);
74
+ if (change.application === "failed") {
75
+ const code = change.error?.code ?? "unknown";
76
+ const detail = change.error?.diagnostic ?? "";
77
+ return failed(action, "执行失败:" + code + (detail === "" ? "" : " —— " + detail));
78
+ }
79
+ return {
80
+ ok: true, action, target, status: "executed",
81
+ output: "已" + (enabled ? "启用" : "禁用") + " " + target + restartNote(change.application),
82
+ };
83
+ }
84
+ case "remove-official-copy":
85
+ if (target === undefined)
86
+ return failed(action, "缺少 target(包名)");
87
+ return await removeOfficialCopy(target, deps);
88
+ default:
89
+ return { ok: false, action, status: "failed", output: "未知修复动作:" + action };
90
+ }
91
+ }
92
+ catch (error) {
93
+ return failed(action, error instanceof Error ? error.message : String(error), target);
94
+ }
95
+ }
96
+ /** 应用结果里"要不要重启"的补充说明。 */
97
+ function restartNote(application) {
98
+ if (application === "restart-required")
99
+ return "(需要重启环境才能生效)";
100
+ if (application === "overridden")
101
+ return "(改动已保存,但被更高层覆盖,当前不生效)";
102
+ return "";
103
+ }
104
+ /** 构造一条 needs-manual 结果。 */
105
+ function manualOutcome(action, target) {
106
+ const id = target ?? "(未知目标)";
107
+ return {
108
+ ok: false, action, ...(target === undefined ? {} : { target }), status: "needs-manual",
109
+ output: [
110
+ "这条修复要改 cordis.patch.yml 的结构,我们没有自己动手写这个文件。",
111
+ "",
112
+ "要修的是:" + action + ",目标 " + id,
113
+ "请在诊断报告里展开这条问题的证据 —— 它给了 cordis.patch.yml 的具体行号;",
114
+ "删掉多余那几行后保存即可,下一次启动生效。",
115
+ "",
116
+ "如果环境已经起不来:用 dsh --profile <name> --patch <空补丁.yml> 先拉起来,",
117
+ "或者交给 agent 用官方 plugin_manager 工具处理。",
118
+ ].join("\n"),
119
+ };
120
+ }
121
+ /** 构造一条 failed 结果。 */
122
+ function failed(action, output, target) {
123
+ return { ok: false, action, ...(target === undefined ? {} : { target }), status: "failed", output };
124
+ }
125
+ /**
126
+ * 删掉 profile 里重复的官方包拷贝,并把声明从 dependencies 挪到 peerDependencies。
127
+ *
128
+ * 为什么安全:重复拷贝才是模块身份分裂的根因(profile 里出现第二份官方包,loader 的
129
+ * 最近优先解析会劫持官方行)。删掉后该包由共享 fallback 目录提供,与其它官方包共用一份。
130
+ *
131
+ * 两个写操作都在官方通道上:目录删除是普通文件系统操作(不是配置文件);声明改写用
132
+ * 官方 saveManifest,并取官方同款的文件锁(与 pluginManager 的写用同一把),因此不会
133
+ * 与官方并发写 package.json 打架。
134
+ *
135
+ * @param packageName - 重复的官方包名。
136
+ * @param deps - 依赖。
137
+ * @returns 执行结果。
138
+ */
139
+ async function removeOfficialCopy(packageName, deps) {
140
+ const envName = deps.environmentName();
141
+ if (envName === null)
142
+ return failed("remove-official-copy", "无法确定当前环境", packageName);
143
+ const dir = environmentDir(envName);
144
+ const copy = join(dir, "node_modules", ...packageName.split("/"));
145
+ const removed = [];
146
+ if (existsSync(copy)) {
147
+ await rm(copy, { recursive: true, force: true });
148
+ removed.push(copy);
149
+ }
150
+ const { withFileLock } = await import("@deepseek-ai/dsh-atomic-write");
151
+ const { readProfileManifest } = await import("@deepseek-ai/dsh-app-boot");
152
+ const { saveManifest } = await import("@deepseek-ai/dsh-plugin-manager/operations");
153
+ await withFileLock(join(dir, "package.json"), async () => {
154
+ const manifest = readProfileManifest("dsh", dir);
155
+ const dependencies = { ...(manifest.dependencies ?? {}) };
156
+ if (!Object.hasOwn(dependencies, packageName))
157
+ return;
158
+ delete dependencies[packageName];
159
+ const peers = { ...(manifest["peerDependencies"] ?? {}) };
160
+ peers[packageName] = peers[packageName] ?? "*";
161
+ await saveManifest(dir, { ...manifest, dependencies, peerDependencies: peers });
162
+ });
163
+ const declared = removed.length === 0 ? "未发现重复拷贝(可能已被清理)" : "已删除 " + removed[0];
164
+ return {
165
+ ok: true, action: "remove-official-copy", target: packageName, status: "executed",
166
+ output: declared + ";声明已从 dependencies 移到 peerDependencies。重启环境后由共享目录提供同一份官方包。",
167
+ };
168
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 插件安装守卫:拦住 agent 走"裸命令"改插件状态,并把正确走法写进拒绝原因。
3
+ *
4
+ * 归属:A 类·重写(拦 agent 工具调用 + 注册常驻提示段;旧 src/guard.ts 仅作意图参考,未复制代码)。
5
+ * 旧实现参考:dsh-web-plugin-manager/src/guard.ts(194 行:按 shell 段判定、
6
+ * positional 词提取处理 --profile X 与 --profile=X 两种写法、npm 变体覆盖、
7
+ * 动词词边界避免误伤 install-assets 这类脚本名、denial reason 直接给正确命令)。
8
+ * 官方复用:官方 plugin_manager agent 工具(boot/plugin-manager/src/tools.ts,动作枚举
9
+ * list_plugins / list_bundles / set_plugin / set_bundle / install_bundle / remove_bundle)
10
+ * 是**当前环境**写操作的正确入口;ctx.tools.guard 是官方提供的单调守卫注册点
11
+ * (返回 string 即拒绝,没有 allow 结果,所以监听顺序无法把拒绝翻回放行);
12
+ * ctx.systemPrompt.section 是官方提示段注册点。
13
+ * 前提检查:旧实现引导到它自建的 plugin_install / plugin_uninstall / plugin_toggle
14
+ * 工具——那些工具在本仓库被**删除**了(用户决定:"agent 工具只留 plugin_search +
15
+ * 健康检查,其余交还官方")。所以这里的引导目标必须换成官方 plugin_manager;
16
+ * 引导到一个不存在的工具会让模型反复重试(比不拦更糟)。
17
+ *
18
+ * 范围与边界(如实声明,不夸大):
19
+ * - 守卫只作用于**本进程内的 agent 工具调用**(bash / run_code)。用户在终端
20
+ * 手工执行的裸命令拦不住,也不该拦——那是用户自己的机器。
21
+ * - 守卫是单调拒绝:它不能给别的守卫已拒绝的调用"放行",只能加一道拒绝。
22
+ * - 我们自己的 CLI(dshpmc)走的正是官方 runPluginCommand,与官方 plugin_manager
23
+ * 同一条 pnpm 通道,因此不被拦;DshPmc 自身也不在拦截词表里。
24
+ */
25
+ import type { Context } from '@deepseek-ai/cordis';
26
+ /** 一条工具执行的只读视图(结构式;不 import 官方内部类型,避免 peer 版本漂移)。 */
27
+ export interface GuardedExecution {
28
+ /** 工具名('bash' / 'run_code' / 其它)。 */
29
+ readonly name: string;
30
+ /** 解析后的入参:bash 用 command,run_code 用 code。 */
31
+ readonly arguments?: unknown;
32
+ }
33
+ /** 官方 CLI 的 plugin 子命令只读动词(显式列出,避免"不在写词表里就算只读"的默认放行)。 */
34
+ export declare const PLUGIN_READ_VERBS: Set<string>;
35
+ /**
36
+ * 从一条命令段里取出"位置词":去掉全局 flag 与其取值。
37
+ *
38
+ * 两种写法都要处理:--profile web(空格分隔,值是下一个词)与 --profile=web
39
+ * (内联)。解析器允许全局 flag 出现在任意位置,所以 flag 在子命令之前也要能
40
+ * 认出子命令——早先只匹配"子命令紧邻"的写法会漏掉 flag-first 的同一个变更。
41
+ *
42
+ * 取值槽只在下一个词自己不是子命令 plugin 时才吃掉它,否则 flag-first 的写法
43
+ * 会把 plugin 当成 --profile 的值丢掉。
44
+ *
45
+ * @param segment - 一条命令段(已按 ; | & 换行切开)。
46
+ * @returns 位置词序列。
47
+ */
48
+ export declare function positionalWords(segment: string): string[];
49
+ /**
50
+ * 一条命令段是否用官方 CLI 改了插件状态。
51
+ *
52
+ * 形如:dsh [--profile X] plugin <写动词> …(任意 flag 顺序)。只读动词(list /
53
+ * status / help / dump-config)放行:那不是变更,拦住它只会让模型无法诊断。
54
+ *
55
+ * @param segment - 一条命令段。
56
+ * @returns 是否是官方 CLI 的插件变更。
57
+ */
58
+ export declare function isDshPluginMutation(segment: string): boolean;
59
+ /**
60
+ * 一条命令段是否用包管理器改了 DSH 环境里的依赖。
61
+ *
62
+ * 两个条件同时成立才算:出现包管理器 + 写动词,且同一段里出现环境目录标记
63
+ * (profiles/、.dsh、DSH_HOME)。只在项目目录里跑 pnpm add 是正常开发行为,
64
+ * 拦它属于误伤。
65
+ *
66
+ * 动词用词边界判定:npm run install-assets 不是变更(脚本名以 install- 开头),
67
+ * 早先的 \binstall\b 会把它误判成变更。
68
+ *
69
+ * @param segment - 一条命令段。
70
+ * @returns 是否是对环境目录的包管理器变更。
71
+ */
72
+ export declare function isProfilePackageMutation(segment: string): boolean;
73
+ /**
74
+ * 整条命令是否属于被拦的裸变更。
75
+ *
76
+ * 逐段判定(; 换行 & | 切开):只读调用出现在另一段里不能豁免真正的变更,
77
+ * 环境目录标记出现在另一段里也不能牵连一条无关的包管理器命令。
78
+ *
79
+ * @param command - 完整的命令行或 run_code 源码。
80
+ * @returns 是否应拒绝。
81
+ */
82
+ export declare function isRawPluginMutation(command: string): boolean;
83
+ /**
84
+ * 拒绝原因:必须**可直接执行**。
85
+ *
86
+ * 只写"不要这么做"会让模型换个说法再试;写清"改用 X 的 Y 动作"才能一次纠正。
87
+ * 引导目标是官方 plugin_manager 工具(当前环境)与我们自己的 dshpmc CLI
88
+ * (跨环境 + 质量门 + 技能/预设直装),两者都是官方 pnpm 通道,不是自建写路径。
89
+ */
90
+ export declare const DENIAL_REASON: string;
91
+ /**
92
+ * 常驻提示段:在模型尝试裸命令之前就把规则说清楚。
93
+ *
94
+ * order 300 落在官方 SECTION_ORDERS 的 PERSONA_PREFIX(0) 与 PLAN_POLICY(500) 之间:
95
+ * 属于"操作约束",要排在策略文本之前。旧实现用了同一个位置。
96
+ */
97
+ export declare const PLUGIN_RULE_SECTION: {
98
+ name: string;
99
+ order: number;
100
+ text: string;
101
+ };
102
+ /**
103
+ * 创建守卫函数(每次装配一个实例)。
104
+ * @returns 守卫:命中裸变更时返回拒绝原因,否则 undefined。
105
+ */
106
+ export declare function createPluginGuard(): (execution: GuardedExecution) => string | undefined;
107
+ /**
108
+ * 把守卫注册到 tools 服务。
109
+ *
110
+ * tools 服务缺失时返回 null 而不是抛错:插件必须能在任何宿主上加载,
111
+ * 没有工具注册表的宿主里"没有守卫"是事实,由调用方决定要不要记账。
112
+ *
113
+ * @param ctx - host 上下文。
114
+ * @returns 注销函数;tools 服务不可用时 null。
115
+ */
116
+ export declare function registerPluginGuard(ctx: Context): (() => void) | null;
117
+ /**
118
+ * 注册常驻提示段。
119
+ *
120
+ * @param ctx - host 上下文。
121
+ * @returns 注销函数;systemPrompt 服务不可用时 null。
122
+ */
123
+ export declare function registerPluginRulePrompt(ctx: Context): (() => void) | null;
124
+ /**
125
+ * 同时注册守卫与提示段(装配入口用一次调用表达两件事)。
126
+ *
127
+ * @param ctx - host 上下文。
128
+ * @returns 两个注销函数;服务缺失时对应项为 null。
129
+ */
130
+ export declare function registerGuard(ctx: Context): {
131
+ readonly guard: (() => void) | null;
132
+ readonly prompt: (() => void) | null;
133
+ };
package/dist/guard.js ADDED
@@ -0,0 +1,232 @@
1
+ /**
2
+ * 插件安装守卫:拦住 agent 走"裸命令"改插件状态,并把正确走法写进拒绝原因。
3
+ *
4
+ * 归属:A 类·重写(拦 agent 工具调用 + 注册常驻提示段;旧 src/guard.ts 仅作意图参考,未复制代码)。
5
+ * 旧实现参考:dsh-web-plugin-manager/src/guard.ts(194 行:按 shell 段判定、
6
+ * positional 词提取处理 --profile X 与 --profile=X 两种写法、npm 变体覆盖、
7
+ * 动词词边界避免误伤 install-assets 这类脚本名、denial reason 直接给正确命令)。
8
+ * 官方复用:官方 plugin_manager agent 工具(boot/plugin-manager/src/tools.ts,动作枚举
9
+ * list_plugins / list_bundles / set_plugin / set_bundle / install_bundle / remove_bundle)
10
+ * 是**当前环境**写操作的正确入口;ctx.tools.guard 是官方提供的单调守卫注册点
11
+ * (返回 string 即拒绝,没有 allow 结果,所以监听顺序无法把拒绝翻回放行);
12
+ * ctx.systemPrompt.section 是官方提示段注册点。
13
+ * 前提检查:旧实现引导到它自建的 plugin_install / plugin_uninstall / plugin_toggle
14
+ * 工具——那些工具在本仓库被**删除**了(用户决定:"agent 工具只留 plugin_search +
15
+ * 健康检查,其余交还官方")。所以这里的引导目标必须换成官方 plugin_manager;
16
+ * 引导到一个不存在的工具会让模型反复重试(比不拦更糟)。
17
+ *
18
+ * 范围与边界(如实声明,不夸大):
19
+ * - 守卫只作用于**本进程内的 agent 工具调用**(bash / run_code)。用户在终端
20
+ * 手工执行的裸命令拦不住,也不该拦——那是用户自己的机器。
21
+ * - 守卫是单调拒绝:它不能给别的守卫已拒绝的调用"放行",只能加一道拒绝。
22
+ * - 我们自己的 CLI(dshpmc)走的正是官方 runPluginCommand,与官方 plugin_manager
23
+ * 同一条 pnpm 通道,因此不被拦;DshPmc 自身也不在拦截词表里。
24
+ */
25
+ /** 官方 CLI 的 plugin 子命令写动词。 */
26
+ const PLUGIN_WRITE_VERBS = new Set(['add', 'install', 'remove', 'rm', 'update', 'upgrade', 'uninstall', 'delete']);
27
+ /** 官方 CLI 的 plugin 子命令只读动词(显式列出,避免"不在写词表里就算只读"的默认放行)。 */
28
+ export const PLUGIN_READ_VERBS = new Set(['list', 'status', 'help', 'dump-config', 'view', 'ls']);
29
+ /** 包管理器变更动词(npm/yarn/bun/pnpm 家族)。 */
30
+ const PM_WRITE_VERBS = new Set(['add', 'install', 'i', 'remove', 'rm', 'uninstall', 'update', 'upgrade', 'link']);
31
+ /** 出现在命令里即视为"目标是 DSH 环境"的标记。 */
32
+ const PROFILE_DIR_MARKER = /profiles[\\/]|\.dsh|DSH_HOME/i;
33
+ /**
34
+ * 从一条命令段里取出"位置词":去掉全局 flag 与其取值。
35
+ *
36
+ * 两种写法都要处理:--profile web(空格分隔,值是下一个词)与 --profile=web
37
+ * (内联)。解析器允许全局 flag 出现在任意位置,所以 flag 在子命令之前也要能
38
+ * 认出子命令——早先只匹配"子命令紧邻"的写法会漏掉 flag-first 的同一个变更。
39
+ *
40
+ * 取值槽只在下一个词自己不是子命令 plugin 时才吃掉它,否则 flag-first 的写法
41
+ * 会把 plugin 当成 --profile 的值丢掉。
42
+ *
43
+ * @param segment - 一条命令段(已按 ; | & 换行切开)。
44
+ * @returns 位置词序列。
45
+ */
46
+ export function positionalWords(segment) {
47
+ // 命令行词:shell 标点(引号、括号、反引号、=、逗号)与空白同样分词,
48
+ // 因此嵌在引号里或 run_code 表达式里的同一个命令仍然能被认出来。
49
+ const tokens = segment.match(/[A-Za-z0-9_@.\/~-]+/g) ?? [];
50
+ const out = [];
51
+ for (let index = 0; index < tokens.length; index += 1) {
52
+ const token = tokens[index];
53
+ if (token.startsWith('-')) {
54
+ if (token.includes('='))
55
+ continue;
56
+ const next = tokens[index + 1];
57
+ if (next !== undefined && !next.startsWith('-') && next !== 'plugin')
58
+ index += 1;
59
+ continue;
60
+ }
61
+ out.push(token);
62
+ }
63
+ return out;
64
+ }
65
+ /** 一个位置词是否指 dsh 可执行文件(裸名或任意路径结尾)。 */
66
+ function isDshWord(word) {
67
+ return word.split('/').pop() === 'dsh';
68
+ }
69
+ /**
70
+ * 一条命令段是否用官方 CLI 改了插件状态。
71
+ *
72
+ * 形如:dsh [--profile X] plugin <写动词> …(任意 flag 顺序)。只读动词(list /
73
+ * status / help / dump-config)放行:那不是变更,拦住它只会让模型无法诊断。
74
+ *
75
+ * @param segment - 一条命令段。
76
+ * @returns 是否是官方 CLI 的插件变更。
77
+ */
78
+ export function isDshPluginMutation(segment) {
79
+ const words = positionalWords(segment);
80
+ for (let index = 0; index < words.length - 1; index += 1) {
81
+ if (!isDshWord(words[index]) || words[index + 1] !== 'plugin')
82
+ continue;
83
+ const verbs = words.slice(index + 2);
84
+ if (verbs.some(verb => PLUGIN_WRITE_VERBS.has(verb)))
85
+ return true;
86
+ // 只读动词(含无动词的裸 'dsh plugin')→ 不算变更。
87
+ if (verbs.some(verb => PLUGIN_READ_VERBS.has(verb)))
88
+ return false;
89
+ return false;
90
+ }
91
+ return false;
92
+ }
93
+ /**
94
+ * 一条命令段是否用包管理器改了 DSH 环境里的依赖。
95
+ *
96
+ * 两个条件同时成立才算:出现包管理器 + 写动词,且同一段里出现环境目录标记
97
+ * (profiles/、.dsh、DSH_HOME)。只在项目目录里跑 pnpm add 是正常开发行为,
98
+ * 拦它属于误伤。
99
+ *
100
+ * 动词用词边界判定:npm run install-assets 不是变更(脚本名以 install- 开头),
101
+ * 早先的 \binstall\b 会把它误判成变更。
102
+ *
103
+ * @param segment - 一条命令段。
104
+ * @returns 是否是对环境目录的包管理器变更。
105
+ */
106
+ export function isProfilePackageMutation(segment) {
107
+ if (!/\b(?:pnpm|npm|yarn|bun)\b/.test(segment))
108
+ return false;
109
+ if (!PROFILE_DIR_MARKER.test(segment))
110
+ return false;
111
+ const words = positionalWords(segment);
112
+ for (let index = 0; index < words.length; index += 1) {
113
+ const word = words[index];
114
+ if (word === 'pnpm' || word === 'npm' || word === 'yarn' || word === 'bun') {
115
+ const verbs = words.slice(index + 1);
116
+ if (verbs.some(verb => PM_WRITE_VERBS.has(verb)))
117
+ return true;
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+ /**
123
+ * 整条命令是否属于被拦的裸变更。
124
+ *
125
+ * 逐段判定(; 换行 & | 切开):只读调用出现在另一段里不能豁免真正的变更,
126
+ * 环境目录标记出现在另一段里也不能牵连一条无关的包管理器命令。
127
+ *
128
+ * @param command - 完整的命令行或 run_code 源码。
129
+ * @returns 是否应拒绝。
130
+ */
131
+ export function isRawPluginMutation(command) {
132
+ for (const segment of command.split(/[;\n&|]+/)) {
133
+ if (isDshPluginMutation(segment))
134
+ return true;
135
+ if (isProfilePackageMutation(segment))
136
+ return true;
137
+ }
138
+ return false;
139
+ }
140
+ /** 从一次工具执行里取出命令行文本;非 bash/run_code 返回 null。 */
141
+ function commandText(execution) {
142
+ if (execution.name !== 'bash' && execution.name !== 'run_code')
143
+ return null;
144
+ const args = execution.arguments;
145
+ if (typeof args?.command === 'string')
146
+ return args.command;
147
+ if (typeof args?.code === 'string')
148
+ return args.code;
149
+ return null;
150
+ }
151
+ /**
152
+ * 拒绝原因:必须**可直接执行**。
153
+ *
154
+ * 只写"不要这么做"会让模型换个说法再试;写清"改用 X 的 Y 动作"才能一次纠正。
155
+ * 引导目标是官方 plugin_manager 工具(当前环境)与我们自己的 dshpmc CLI
156
+ * (跨环境 + 质量门 + 技能/预设直装),两者都是官方 pnpm 通道,不是自建写路径。
157
+ */
158
+ export const DENIAL_REASON = 'Blocked: raw plugin mutations bypass the protected flow. Use the plugin_manager tool instead: '
159
+ + 'plugin_manager { action: "list_plugins" } to find an entry id, then '
160
+ + 'action: "set_plugin" | "set_bundle" (with target + enabled) to enable/disable, '
161
+ + 'action: "install_bundle" (target: package spec) to install, or '
162
+ + 'action: "remove_bundle" (target: package name) to uninstall — those actions require '
163
+ + 'danger-full-access permission or an approval, and the user must confirm the change. '
164
+ + 'For a quality-gated install, cross-environment management, or skill/agent-preset installs, '
165
+ + 'run the companion CLI instead: dshpmc install <spec> --profile <name>, '
166
+ + 'dshpmc remove <name>, dshpmc update <name>, dshpmc uninstall-kind <owner/repo>. '
167
+ + 'Do not run bare "dsh plugin add/remove/update" or pnpm add/remove against a profile directory.';
168
+ /**
169
+ * 常驻提示段:在模型尝试裸命令之前就把规则说清楚。
170
+ *
171
+ * order 300 落在官方 SECTION_ORDERS 的 PERSONA_PREFIX(0) 与 PLAN_POLICY(500) 之间:
172
+ * 属于"操作约束",要排在策略文本之前。旧实现用了同一个位置。
173
+ */
174
+ export const PLUGIN_RULE_SECTION = {
175
+ name: 'plugin-manager-companion:install-rule',
176
+ order: 300,
177
+ text: 'To install, remove, enable, disable, or update DSH plugins in the current profile, use the '
178
+ + 'plugin_manager tool (actions: list_plugins, list_bundles, set_plugin, set_bundle, install_bundle, '
179
+ + 'remove_bundle). It requires danger-full-access permission or approval. For a quality-gated install, '
180
+ + 'another profile, or skill / agent-preset installs, use the companion CLI: dshpmc install <spec>, '
181
+ + 'dshpmc remove <name>, dshpmc update <name>, dshpmc uninstall-kind <owner/repo>. '
182
+ + 'Never run bare "dsh plugin add/remove" or npm/yarn/bun/pnpm add/remove against a profile directory: '
183
+ + 'that path skips the quality gate and can leave the profile unable to boot.',
184
+ };
185
+ /**
186
+ * 创建守卫函数(每次装配一个实例)。
187
+ * @returns 守卫:命中裸变更时返回拒绝原因,否则 undefined。
188
+ */
189
+ export function createPluginGuard() {
190
+ return (execution) => {
191
+ const command = commandText(execution);
192
+ if (command === null)
193
+ return undefined;
194
+ return isRawPluginMutation(command) ? DENIAL_REASON : undefined;
195
+ };
196
+ }
197
+ /**
198
+ * 把守卫注册到 tools 服务。
199
+ *
200
+ * tools 服务缺失时返回 null 而不是抛错:插件必须能在任何宿主上加载,
201
+ * 没有工具注册表的宿主里"没有守卫"是事实,由调用方决定要不要记账。
202
+ *
203
+ * @param ctx - host 上下文。
204
+ * @returns 注销函数;tools 服务不可用时 null。
205
+ */
206
+ export function registerPluginGuard(ctx) {
207
+ const tools = ctx.get('tools');
208
+ if (tools === undefined || typeof tools.guard !== 'function')
209
+ return null;
210
+ return tools.guard(createPluginGuard());
211
+ }
212
+ /**
213
+ * 注册常驻提示段。
214
+ *
215
+ * @param ctx - host 上下文。
216
+ * @returns 注销函数;systemPrompt 服务不可用时 null。
217
+ */
218
+ export function registerPluginRulePrompt(ctx) {
219
+ const systemPrompt = ctx.get('systemPrompt');
220
+ if (systemPrompt === undefined || typeof systemPrompt.section !== 'function')
221
+ return null;
222
+ return systemPrompt.section({ ...PLUGIN_RULE_SECTION });
223
+ }
224
+ /**
225
+ * 同时注册守卫与提示段(装配入口用一次调用表达两件事)。
226
+ *
227
+ * @param ctx - host 上下文。
228
+ * @returns 两个注销函数;服务缺失时对应项为 null。
229
+ */
230
+ export function registerGuard(ctx) {
231
+ return { guard: registerPluginGuard(ctx), prompt: registerPluginRulePrompt(ctx) };
232
+ }