@caesarloo/dsh-skill-audit 0.1.1 → 0.2.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/skill/SKILL.md ADDED
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: skill-audit
3
+ category: quality
4
+ description: "DSH 技能的审核通道:以确定性脚本对技能做静态体检——frontmatter 契约、脚本可用性(UTF-8 BOM + PowerShell 5.1 可解析)、SKILL.md 引用完整性、敏感信息与凭据泄漏、机器专属路径、危险命令模式;并提供技能被修改或从备份仓库恢复后自动执行审核的通道(结论作为上下文回传给模型)。当技能被新建/修改/从备份恢复、技能里的脚本改过、多机同步技能之后,或需要回答『这个技能是否可用、有没有把密钥写进去、引用的脚本还在不在』时使用。触发词:技能审核、skill audit、审核技能、技能体检、技能改动后检查、恢复技能后检查、技能脚本没BOM、技能引用失效、技能泄漏密钥。"
5
+ whenToUse: "技能新增或修改之后(尤其改了 SKILL.md 或技能内脚本);从备份仓库恢复技能之后;跨机同步技能之后;排查『技能加载了却不管用』(脚本跑不起来、引用文件缺失)时;提交技能进备份仓库之前做入库前体检。"
6
+ last_updated: 2026-09-17
7
+ version: 1.1.0
8
+ created_by: agent
9
+ metadata:
10
+ hermes:
11
+ tags: [skill, audit, quality, 技能审核, 静态检查, BOM, frontmatter, 凭据泄漏, 豁免, 扩展点]
12
+ ---
13
+
14
+ # 技能审核(skill-audit)
15
+
16
+ 技能是**给模型看的指令 + 给机器跑的脚本**。它坏掉的方式和插件不同:插件崩了会报错,技能坏掉往往**静默**——frontmatter 写错导致模型根本看不到它、脚本丢了 BOM 导致 5.1 下跑不起来、SKILL.md 引用了已删除的脚本、密钥被顺手写进技能正文随备份进了 git。本技能就是把这些"没人报错的坏"变成**报错的检查**。
17
+
18
+ > **本技能是「核心面」**:只放判据、契约与流程——也就是**换一台机器仍然成立**的内容。**你本机的接线、路径约定、实测证据与机器专属规则,不要写进本技能正文**,而应放在你自己的**本地扩展技能**里(见 §4.1 的扩展点,或另立一个本机运维技能)。判定尺度一句话:**「这条内容换一台机器还成立吗?」** 成立 → 核心;不成立 → 本地扩展。切分的收益是两头都受益:核心只随判据演进(不必为本机琐事重建重发),本机知识改完即生效且不进公开包。
19
+
20
+ ## 一、边界:本技能管什么、不管什么
21
+
22
+ | 问题 | 归属 |
23
+ |---|---|
24
+ | 技能是否**可用、自洽、安全** | **本技能** |
25
+ | 插件是否**恶意 / 可信**(静态评分 + 源码调查 + 健康档案) | 插件安全审计类技能 |
26
+ | 插件能否**装配成功**(bundle 装配、registry 可达性、boot 验证) | 插件装配核查类技能 |
27
+ | 技能 / 插件**在多机间怎么搬**(backup / restore、分叉合并) | 多机同步类技能 |
28
+ | 技能**该不该转成插件** | 插件开发类技能 |
29
+ | **本机怎么接线**(装在哪、档案落哪、机器专属规则放哪) | 你自己的本地运维技能 |
30
+
31
+ 一句话:**本技能只回答「这个技能能不能用、干不干净」**。插件坏不坏、装不装得上、资产怎么搬,各是另一条通道的事——本技能不越界,也不替它们下结论。
32
+
33
+ ## 二、自动通道
34
+
35
+ 技能被修改、或被恢复之后**自动执行**,无需任何人记得去跑。
36
+
37
+ ### 2.0 三层分工(加判据前先看这张表,别放错层)
38
+
39
+ | 层 | 手里有什么 | 该放什么 | 改动代价 |
40
+ |---|---|---|---|
41
+ | `AGENTS.md`(全局指令) | 每次会话**无条件加载** | **写之前**的约定:该怎么写技能、什么时候必须审核 | 立即生效,无需重启 |
42
+ | 审核引擎 `scripts/audit-skills.ps1` | 只有**文件系统**(技能目录里的字节) | 一切**静态可判定**的判据(frontmatter、BOM、引用、凭据、危险命令) | 改完即生效;所有通道同时受益(引擎随本技能一起发布,实际用哪一份由 §2.1 的解析优先级决定) |
43
+ | 审核插件 | **进程内活状态**(已解析的技能目录、触发时机、上下文预算) | 只放**需要活状态**的检查,以及"触发 + 回传"编排 | 要重建 `dist/` + 重装 + 重启 dsh |
44
+
45
+ **为什么判据不写进插件**:插件刻意不含任何规则(只做 `parseReport` 与格式化回传)。规则若在插件里也存一份,就有了**两个真源**——改 `.ps1` 立即生效、改插件要重建重发重启,两者必然漂移。
46
+
47
+ **什么才该进插件**:静态层**拿不到**的信息。典型是"这个技能名到底能不能解析"——技能名可以由插件在运行时注册(磁盘上**没有** SKILL.md,只在进程内可见),`.ps1` 查不到,只有进程内的技能目录知道。F2 因此**刻意不查存在性**。
48
+
49
+ ### 2.1 主通道:审核插件(覆盖"技能被修改")
50
+
51
+ 插件在 harness 进程内监听 `tools/post-execute`,用 **`ctx.subprocess`(host 层)** 跑本技能的审核引擎,把结论作为上下文回传给模型。
52
+
53
+ - **触发规则**:**写入类文件工具**(`write`/`edit`/`multi_edit`/`notebook_edit`/`apply_patch` …)命中 `<DSH_HOME>/skills/<技能>/` → 只审该技能;但命中的目录**没有 `SKILL.md`**(即它不是技能)→ **全量**(只审一个不是技能的东西毫无意义;这类写入通常意味着技能目录还没成形);**任何以 `mode: restore` / `mode: backup` 调用的工具** → 全量(判据是**调用形态**而不是工具名,因此不绑定任何具体备份插件——没有装备份插件的用户也不会因此产生死逻辑);`pwsh` 等 shell 的命令行同时含 `skills` 与写操作迹象(`Set-Content`/`Copy-Item`/`Remove-Item`/`robocopy` …)→ 全量;其它 → 静默。**只读工具(`read`/`glob`/`grep`)刻意不触发**——它们同样携带 `file_path` 却不改内容;不加这条白名单,每读一次技能文件就会注入一次审核上下文。
54
+ - **审的是新内容**:在 `post-execute`(**写入之后**)执行——`pre-execute` 只能审到旧文件(第三方 `dsh-skill-authoring` 的 pre-execute + 跳过 edit 就是这个缺陷)。
55
+ - **上下文分级**:定向单技能详列 fail + warn;**全量场景**只详列 fail、warn 压成一行汇总(warn 多时逐条列会把上下文挤爆);**全量且只有 warn 时完全不注入**(背景噪音不打断写入);全部通过同样保持安静,只写审核日志。
56
+ - **不阻塞**:任何异常都被吞掉并委托 `next()`,绝不影响工具调用本身。
57
+ - **主动调用**:`skill_audit` 工具(不带参数 = 全量;`skill: 'a,b'` = 定向)。
58
+ - **引擎单一真源**:插件不含审核规则,只负责"触发 + 回传";规则始终在 `scripts/audit-skills.ps1`——**引擎与本技能正文一起随包发布**,按 `config.auditScript` > 用户态技能目录 > 包内副本 的优先级解析。你可以在自己的技能根里放一份引擎来覆盖包内那份(改完即生效、不必发版),不放就用包内的。实际用的是哪一个,以 §三 手工通道给出的路径为准。
59
+
60
+ ## 三、手工通道(agent 执行)
61
+
62
+ 引擎只做**确定性静态检查**;下列情况要人(agent)读内容判断:
63
+
64
+ 1. 技能语义是否正确、步骤是否最新(脚本已改但 SKILL.md 没跟上);
65
+ 2. 触发词是否覆盖真实说法(模型是否会在对的场景加载它);
66
+ 3. 记录是否过期(引用了已卸载的插件、已改名的工具、已废弃的路径)。
67
+
68
+ ```powershell
69
+ # 全量体检(人读)
70
+ powershell -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.dsh\skills\skill-audit\scripts\audit-skills.ps1"
71
+
72
+ # 只审某几个技能(名字是**真技能**的目录名,即该目录含 SKILL.md;逗号分隔)
73
+ ... -Skill my-skill-a,my-skill-b
74
+
75
+ # 机器消费(JSON)
76
+ ... -Json
77
+ ```
78
+
79
+ 退出码:`0` 无 fail;`1` 有 fail;`2` 参数/路径错误。
80
+
81
+ 深度审核做完后应把结论落成档案,内容包含:审核时间、静态结论、人工核对的语义项、结论(可用 / 需修 / 建议重写)、遗留风险。**落档位置由你本机的约定决定**(通常放你自己的笔记或备份仓库)——别把本机路径写进技能正文。
82
+
83
+ ## 四、审核项与判据
84
+
85
+ | 代码 | 检查 | 级别 | 判据 |
86
+ |---|---|---|---|
87
+ | F1 | frontmatter 契约 | fail/warn | `name`、`description` 必填;`name` 必须 kebab-case 且与目录名一致;缺 `whenToUse`/`version`/`last_updated` → warn |
88
+ | S1 | 脚本可用性 | fail | 技能内每个 `.ps1` 必须 UTF-8 **with BOM**,且 `Parser::ParseFile` 报错数为 0。无 BOM 的中文脚本在 Windows PowerShell 5.1 下按 GBK 解码 → 解析失败(`Missing closing ')'`),**更新过来即不可用** |
89
+ | R1 | 引用完整性 | fail/warn | SKILL.md 里 `scripts/xxx.ps1` 这类相对引用必须真实存在。**子目录存在而文件缺失 → fail**(真断裂);**引用落在同根下的另一个技能里 → warn「跨技能引用」**(应改为点名技能名 + `related_skills`,见 §5.1);**连子目录都没有 → warn**(运行时生成或外部来源) |
90
+ | R2 | 脚本被引用 | info | 技能内脚本未被 SKILL.md 提及(可能是死资产,也可能是刻意留的工具) |
91
+ | F2 | 依赖声明 | warn | `metadata.hermes.related_skills` 的**自依赖 / 重复项**。**存在性刻意不查**:技能名可由插件运行时注册(磁盘无 SKILL.md),静态脚本查不到 → 查了必误报。悬空声明检测需活的技能目录,属插件侧(§2.0) |
92
+ | E1 | 审核扩展 | warn | `audit_extension` 声明的扩展缺失 / 无 BOM / 解析失败 / 执行抛错 → 记在**声明该扩展的技能**上;抛过错的扩展立即停用且只报一次(见 §4.1) |
93
+ | M1 | 豁免标记契约 | warn | `audit:ignore` 标记缺代码、或理由不足 8 字符 → 标记不生效并报 M1(防止"随手加个标记消音") |
94
+ | X1 | 敏感信息 | fail | 命中 OpenAI/GitHub/npm/AWS 特征串、私钥块、明文口令或 token 赋值 |
95
+ | X2 | 机器专属路径 | info | 硬编码 `C:\Users\<具体用户名>`(通用写法 `$env:USERPROFILE`/`%TEMP%` 不算) |
96
+ | X3 | 危险命令 | info | 递归强删、`rm -rf`、`reg delete`、格式化等模式(确认用途,常见于快照清理) |
97
+
98
+ **为什么 X2/X3 只记 info**:技能按约定会写自己的绝对路径(钩子、快照目录),危险命令也确有正当用途(清理恢复快照)。把它们当 fail 会让审核天天报警,最后被无视——**审核一旦有噪音就会失去意义**。
99
+
100
+ **F1 与"这个目录其实不是技能"**:**全量**扫描只挑含 `SKILL.md` 的目录,所以"只有脚本、没有 SKILL.md"的目录不会进入全量范围。但**显式定向**指定它(`-Skill <目录名>`)会得到 `F1 缺少 SKILL.md`——这是**正确**的报错,不是缺陷:它确实不是技能。引擎**刻意不做**"有 scripts/ 就免除 F1"这类豁免,一是引擎是泛用层、不该知道"某个目录只用来放判据引擎"这种本地约定,二是豁免会连"真技能的 SKILL.md 被误删"一起放过。要验证这类目录里的脚本,跑**全量**或按脚本自身方式验证。
101
+
102
+ ### 4.1 扩展点:由本地其他技能补充审核(`audit_extension`)
103
+
104
+ 核心判据的真源永远是本技能的 `scripts/audit-skills.ps1`;但**本地其他技能可以追加自己的检查**——这既是"领域专属规则"的落点,也是**稳定性边界**:会随本机/本组织频繁变动的规则放这里,核心引擎与插件包就不必跟着改。声明写在**提供扩展的那个技能**的 frontmatter 里,脚本路径相对该技能目录:
105
+
106
+ ```yaml
107
+ metadata:
108
+ hermes:
109
+ audit_extension: "scripts/<扩展脚本名>.ps1"
110
+ ```
111
+
112
+ 扩展脚本需定义 `Get-SkillAuditFindings`,引擎会对**每个被审技能**调用它一次:
113
+
114
+ ```powershell
115
+ function Get-SkillAuditFindings {
116
+ param([string]$SkillName, [string]$SkillDir, [string]$SkillsRoot)
117
+ if ($SkillName -ne 'my-skill') { return @() }
118
+ return @([pscustomobject]@{
119
+ code = 'MY1'; level = 'warn'
120
+ message = '自定义检查未通过'
121
+ file = 'SKILL.md'; target = 'my-skill'
122
+ })
123
+ }
124
+ ```
125
+
126
+ 三条硬约束:
127
+
128
+ 1. **只增不减**:扩展只能追加发现,不能移除或降级核心判据——「判据单一真源」不因扩展点而松动,新判据各有各的家。
129
+ 2. **出错不拖垮审核**:缺文件 / 无 BOM / 解析失败 / 执行抛错 → 记一条 `E1` warn 到**声明它的技能**上,审核照常跑完;抛过错的扩展会被**立即停用且只报一次**(否则一个坏扩展会作用于每个被审技能,把报告刷爆)。
130
+ 3. **零影响**:没有任何技能声明 `audit_extension` 时本机制完全不参与,行为与引入前一致。
131
+
132
+ 扩展返回的 `level` 只认 `fail` / `warn` / `info`,其它值降级为 `warn`——不允许自造级别绕过 status 判定。扩展产物照常参与豁免(形式与理由要求见 §5.1)。审核输出会标出来源(形如 `MY1 @my-skill`),表头列出本次加载的扩展,JSON 报告含 `extensions` 字段。
133
+
134
+ > **本机专属规则放哪**:找个你自己的技能当宿主即可(哪个技能由你定)——但这类承载本机知识的技能**不要**随公开包分发。核心判据的追加检查是"只对本机 / 本组织成立"的典型,正该走这个扩展点。
135
+
136
+ ## 五、处置流程
137
+
138
+ 1. `fail` **必须修**:S1 用 **BOM 安全编辑法**补 BOM——`ReadAllText` → `Replace` → `WriteAllText($path, $text, UTF8Encoding($true))`,别让编辑工具剥掉 BOM;F1 补 frontmatter;R1 改引用或补文件;X1 先把密钥**移出**技能、**再轮换**(顺序不能反:仓库 / 云盘可能已有副本,先轮换才是止血)。
139
+ 2. `warn` 评估后处理:多为缺元数据,补上即可;确属误报的走下面的豁免标记。
140
+ 3. 修完重跑审核直到 `fail 0`(warn 也应为 0,否则噪音会掩盖将来真出现的问题)。
141
+ 4. 技能改动**入库并同步**(若你的技能目录在同步面内,两侧必须一致)。
142
+
143
+ ### 5.1 误报处置(先解耦,豁免是最后手段)
144
+
145
+ R1 的 warn 分支针对的是"引用了**不属于本技能**、或**尚未生成**的文件"。按下面顺序处置:
146
+
147
+ 1. **属于别的技能 → 只写技能名 + 声明依赖**。正文提到对方时只出现技能名(如"见 **那个本地技能**"),并在 frontmatter 的 `metadata.hermes.related_skills` 里登记。**绝不要在正文抄别的技能的内部文件路径**——对方一改脚本名,你的引用就悄悄过期,而且 R1 会为此长期报警。**这条已由引擎自动检查**:引用落在同根下别的技能里时,R1 直接报「跨技能引用」并给出修法(§四 R1 行);反向的悬空声明由 F2 检查。
148
+ 2. **属于外部来源 / 运行时生成 → 用文字描述,不留路径 token**。例如"本技能目录下的 `references` 缓存(文件名固定 `core.md`)"——描述照旧精确,但不再是一个会被误判成"本技能资产"的引用。
149
+ 3. **前两条都不适用 → 才用 `audit:ignore` 标记**,就地写明例外与理由:
150
+
151
+ ```markdown
152
+ <!-- audit:ignore R1 references/<文件名>.md 该文件属于 xxx 技能 / 由某 CLI 运行时生成,非本技能资产 -->
153
+ ```
154
+
155
+ (示例刻意用占位符写:R1 对含 `xxx`、`<…>` 的 token 主动跳过,所以这段示例不会自己制造 warn。)
156
+
157
+ - **形式**:`<!-- audit:ignore <代码> <目标> <理由> -->`;代码为 `F1/S1/R1/X1/X2/X3` 之一,或 `*`(全部)。
158
+ - **目标**:`R1` 填相对引用(如 `references/xxx.md`,斜杠两种写法等价);**其它代码**填文件名(`SKILL.md` 或脚本名)。
159
+ - **`fail` 级不可豁免**:§五「fail 必须修」是硬线,标记只用来消解 `warn`/`info` 的误报。引擎里 `Test-Waived` 对 fail 直接返回 false——否则"真断裂仍是 fail"这条保证会被一个标记悄悄绕过。
160
+ - **理由不足 8 字符**(或缺代码)→ 标记**不生效**并报 `M1`,避免随手消音。
161
+ - **为什么不是"放宽脚本"**:放宽会同时放过所有技能;豁免标记是**逐条、带理由、随技能进 git 可审计**的,正好落实"例外被显式记录"。此前"写明例外与理由"写了并不生效,规则与实现脱节,结果只剩两条歪路:忍受常驻噪音(久了审核被无视),或改写措辞回避正则(那是掩盖检测,更糟)。豁免标记把"写明例外"变成可执行的正解。
162
+
163
+ > **⚠️ 反例一:新建空目录消 warn(会自伤)**。为了消掉 R1 的 warn 而**新建空的 `scripts/`、`references/` 目录**或放个 `.gitkeep`,会命中 R1 的第二分支"子目录存在但文件缺失",把 warn **直接升级成 fail**(实测:`references/` 一建,`fail=0` → `fail=1`)。空目录不是豁免。
164
+ >
165
+ > **⚠️ 反例二:改写措辞回避正则**。把 `references/xxx.md` 这类真实引用改写成"references 子目录下的 xxx.md"确实能让正则不匹配,但那是**掩盖检测**——文本变含糊,且将来真断裂也一起被藏起来。判据存在的意义是发现断裂,不是让文本绕过它。
166
+ >
167
+ > (自查约定:本文件正文举例一律用 `xxx` / `<名>` 占位符——R1 对这些占位符**主动跳过**,所以文档里的示例不会自己制造 warn;真实引用才写全路径。)
168
+
169
+ ## 六、脚本资产
170
+
171
+ | 脚本 | 用途 | 主要参数 |
172
+ |---|---|---|
173
+ | `scripts/audit-skills.ps1` | 静态审核引擎(本技能唯一的判据真源,插件与其它通道都调它) | `-Skill <名,...>`、`-SkillsRoot <目录>`、`-Json`、`-NoLog` |
174
+
175
+ 脚本必须带 UTF-8 BOM(由 `powershell` 5.1 执行且含中文)。审核引擎另在开头把 `[Console]::OutputEncoding` 设为 UTF-8——否则 5.1 按控制台代码页(GBK)写 stdout,消费方(插件侧 Node 按 UTF-8 解码)会拿到乱码。
176
+
177
+ **自动触发的实现**不在本技能目录里(避免与 dsh 同步面耦合):它是一个独立发布的插件,与本技能正文、引擎一起分发,使"装了就能用"。安装与配置见插件自身的 README。
178
+
179
+ ## 七、故障排查
180
+
181
+ | 症状 | 原因 / 处理 |
182
+ |---|---|
183
+ | 改了技能但没收到审核上下文 | ① 通过时**本来就不输出**;② 路径没落在 `skills\<技能>\` 下(改技能内的脚本同样命中;改技能外的文件不命中);③ 插件未装或**装/改 patch 后没重启 dsh** |
184
+ | 插件到底有没有被装载 | 用 profile 的 dump-config 找该插件的工具 id;没有条目就是没装/没装配 |
185
+ | 审核结论中文乱码 | 引擎已显式设 `[Console]::OutputEncoding = UTF8`;若仍有乱码,检查是否用了旧版脚本 |
186
+ | 审核脚本跑不起来(BOM/语法) | 引擎自己的 BOM 被编辑工具剥了——用 §五 的 BOM 安全编辑法补回 |
187
+ | 工具报"审核脚本不存在" | 引擎解析不到。优先级是 `config.auditScript` > 用户态技能目录 > 包内;正常应落到包内那份,报错说明包不完整、或配置项指错了路径 |
188
+ | 想审的技能不在默认根 | 用 `-SkillsRoot`(手工)或插件的 `skillsRoot` 配置(项目级技能在 `<项目>/.dsh/skills`,不是用户根) |
189
+ | 又想去用 Claude Code 钩子桥接 | **别走回头路**:钩子经 `ctx.shell` 执行命令,沙箱后端不可用时执行器按设计 fail-closed(`SANDBOX_UNAVAILABLE`),配得再对也不生效;审核插件走 `ctx.subprocess`(host 层)才不受此限 |