@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/README.md +194 -30
- package/dist/index.js +174 -23
- package/package.json +7 -4
- package/skill/SKILL.md +189 -0
- package/skill/scripts/audit-skills.ps1 +596 -0
package/README.md
CHANGED
|
@@ -1,33 +1,183 @@
|
|
|
1
1
|
# @caesarloo/dsh-skill-audit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](#english) | [中文](#中文)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## English
|
|
8
|
+
|
|
9
|
+
A DeepSeek Harness plugin that **audits your skills the moment they change**: after a skill file is written or edited, or after skills are restored from a backup repository, it runs the skill audit and feeds the findings back to the model. It also registers a `skill_audit` tool for on-demand runs.
|
|
10
|
+
|
|
11
|
+
### What it does
|
|
12
|
+
|
|
13
|
+
The audit rules live in the `skill-audit` skill's engine: frontmatter contract (`name` / `description` / `whenToUse` / `version`), script usability (UTF-8 BOM + parseable by Windows PowerShell 5.1), `SKILL.md` reference integrity, credential leakage, machine-specific paths and dangerous command patterns.
|
|
14
|
+
|
|
15
|
+
- **Automatic** — audits after skill files are written or edited, and after a bulk restore / backup (any tool invoked with `mode: 'restore'` or `mode: 'backup'`);
|
|
16
|
+
- **Speaks only when there is something to act on** — findings are injected as context only when `fail` / `warn` exist; a clean run stays silent and just writes a log;
|
|
17
|
+
- **Never blocks** — an audit cannot fail your tool call. The file is already written, so surfacing the problem to the model is the right move;
|
|
18
|
+
- **On demand** — the `skill_audit` tool.
|
|
19
|
+
|
|
20
|
+
### Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
dsh plugin --profile web add @caesarloo/dsh-skill-audit
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**No prerequisite** — the package ships the audit engine *and* the skill body, so a bare install is enough. If your skills root already holds a full `skill-audit` skill, the plugin leaves it alone and uses that one instead.
|
|
27
|
+
|
|
28
|
+
Restart dsh afterwards (bundle-level change, not hot-reloaded). Verify:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
dsh --profile web --dump-config | Select-String tool-skill-audit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Engine resolution
|
|
35
|
+
|
|
36
|
+
| # | Source | Path | Notes |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| 1 | `config.auditScript` | whatever you set | Explicit. **If it is set but missing this is an error** — the plugin will not silently run a different engine. |
|
|
39
|
+
| 2 | an engine in your skills root | `<skillsRoot>/skill-audit/scripts/audit-skills.ps1` | Optional override slot, normally absent. Put one here to use your own rules instead of the shipped ones. |
|
|
40
|
+
| 3 | the package | `<package>/skill/scripts/audit-skills.ps1` | **The source of truth**, shipped with the plugin. One copy only — nothing to keep in sync. |
|
|
41
|
+
|
|
42
|
+
The plugin registers the `skill-audit` skill at runtime **unless your skills root already holds a full copy (i.e. a `SKILL.md`)**. That guard is mandatory rather than polite: DSH ranks `project > runtime > user`, and a skills root is the *user* layer (`source: 'user-dsh'`) — an unconditional runtime registration would **shadow your own skill**.
|
|
43
|
+
|
|
44
|
+
The same split decides what ships and what stays yours:
|
|
45
|
+
|
|
46
|
+
| Content | Lives in | Cost of a change |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Skill body (boundaries, trigger rules, finding table, waiver & extension contracts) + the audit **engine** | **the package** — the body is registered at runtime, the engine is resolved from the package too | immediate on a linked install; **republish** for everyone else |
|
|
49
|
+
| **Machine-specific rules** — an `audit_extension` declared by one of your own skills | your skills root | none — live on the next audit, never needs a release |
|
|
50
|
+
|
|
51
|
+
So a bare install gets the whole thing, and the one part that genuinely keeps changing — your own local rules — stays on your side, editable without a rebuild or a restart.
|
|
52
|
+
|
|
53
|
+
Keeping the engine in the package is what makes a **single copy** possible: there is no second copy in your skills root that could silently drift out of date. You can still put one there to override the shipped engine — that slot is respected — but nothing needs it.
|
|
54
|
+
|
|
55
|
+
A registered skill is given the bundled directory as its resource base, so the relative script paths inside the skill body still resolve.
|
|
56
|
+
|
|
57
|
+
### Usage
|
|
58
|
+
|
|
59
|
+
Nothing to do after installing — audits run automatically whenever you change skills. To re-check on demand:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
skill_audit() # audit every skill
|
|
63
|
+
skill_audit({ skill: 'a,b' }) # audit specific skills (comma separated)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Trigger scope:
|
|
67
|
+
|
|
68
|
+
| Your action | Audit scope |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `write` / `edit` on a skill file | that skill only (fast) |
|
|
71
|
+
| any tool invoked with `mode: 'restore'` or `mode: 'backup'` | everything (bulk overwrite / pre-commit check) |
|
|
72
|
+
| a shell command rewriting the skills directory | everything |
|
|
73
|
+
|
|
74
|
+
Bulk detection keys on the **call shape**, not on a tool name — so the plugin works with any backup plugin, and with none. If yours names its mode argument differently, list its tool name in `fullAuditTools`.
|
|
75
|
+
|
|
76
|
+
Reporting is tiered by scenario:
|
|
77
|
+
|
|
78
|
+
- single-skill edit → lists `fail` and `warn` in detail;
|
|
79
|
+
- bulk scope → lists only `fail`, with `warn` collapsed into one summary line;
|
|
80
|
+
- bulk scope with no `fail` → **completely silent** (background noise should not interrupt you).
|
|
4
81
|
|
|
5
|
-
|
|
82
|
+
Logs go to `<DSH_HOME>/vet/skill-audits/` (`latest.json` plus timestamped files, last 40 kept).
|
|
6
83
|
|
|
7
|
-
|
|
84
|
+
### Configuration
|
|
8
85
|
|
|
9
|
-
|
|
10
|
-
- **有发现才提示**:只在存在 `fail`/`warn` 时把结论作为上下文回传给模型;全部通过时保持安静,只写审核日志;
|
|
11
|
-
- **不阻塞**:审核不会让工具调用失败——文件已经写入,把问题摆到模型面前才是正确做法;
|
|
12
|
-
- **可主动调用**:`skill_audit` 工具。
|
|
86
|
+
Usually none. To customize, add `config` to the entry in the profile's `cordis.patch.yml`:
|
|
13
87
|
|
|
14
|
-
|
|
88
|
+
| Key | Default | Meaning |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `skillsRoot` | `<DSH_HOME>/skills` | Skills root directory |
|
|
91
|
+
| `auditScript` | auto: your skills root → the package | Audit engine script; set it to pin one (a missing explicit path is an error) |
|
|
92
|
+
| `autoAudit` | `true` | `false` disables automatic audits (the `skill_audit` tool stays available) |
|
|
93
|
+
| `powershell` | Windows PowerShell / `pwsh` | PowerShell executable |
|
|
94
|
+
| `timeoutMs` | `120000` | Per-audit timeout |
|
|
95
|
+
| `maxContextChars` | `2000` | Cap on injected context length |
|
|
96
|
+
| `fullAuditTools` | `[]` | Extra tool names to treat as bulk rewrites of the skills tree. Usually unnecessary: the generic rule already covers calls whose `mode` is `restore` or `backup`. |
|
|
15
97
|
|
|
16
|
-
|
|
98
|
+
### Dependencies
|
|
99
|
+
|
|
100
|
+
`@deepseek-ai/dsh-tools`, `@deepseek-ai/dsh-subprocess` and `@deepseek-ai/dsh-llm` are declared as **optional peerDependencies** and provided by the host; the package bundles none of them. This is deliberate: a second copy inside the profile would create two module instances and break tool registration. `dsh-llm` is used only to build the context message — if it is unavailable, the audit still runs and only the context injection is skipped.
|
|
101
|
+
|
|
102
|
+
The **skills service** is a different kind of dependency: a service of the host process rather than a package, and it must be declared in `inject`. That declaration is mandatory rather than cosmetic — cordis **throws** on an undeclared service property instead of returning `undefined`, so reading `ctx.skills` without it crashes plugin loading (observed 2026-09-17: `cannot get property "skills" without inject` → `plugin tree failed to load`), and a defensive `undefined` check never gets the chance to run. The service itself is always present: `@deepseek-ai/dsh-base` provides it, and every profile is built on that.
|
|
103
|
+
|
|
104
|
+
### Boundaries
|
|
105
|
+
|
|
106
|
+
- Contains no audit logic itself: it ships the skill body and the engine, and relays the engine's output. The editable sources of truth are the package (skill body + engine) and machine-specific rules in a local skill's `audit_extension`;
|
|
107
|
+
- Does not rewrite tool input and never blocks a tool call;
|
|
108
|
+
- Does not hook non-tool events (`SessionStart` / `Stop`);
|
|
109
|
+
- Does not watch the skills directory — it triggers after tool calls only.
|
|
110
|
+
|
|
111
|
+
### Maintaining `skill/` (maintainers)
|
|
112
|
+
|
|
113
|
+
Both halves of `skill/` — `SKILL.md` (the skill body) and `scripts/audit-skills.ps1` (the engine) — are **the source of truth**, edited right here in this repository. There is no snapshot step any more: the engine used to live in the author's skills root and had to be copied into the package, which meant a second copy that could silently drift out of date. That copy is gone, and with it the whole class of problem.
|
|
114
|
+
|
|
115
|
+
Two checks enforce the contract instead:
|
|
116
|
+
|
|
117
|
+
- `npm test` runs the **real** engine against the bundled skill — laid out exactly like a real skill — and asserts the engine kept its UTF-8 BOM. So the engine cannot ship unparseable and the body cannot ship broken;
|
|
118
|
+
- `prepublishOnly` runs `build && test`, so nothing ships without those checks.
|
|
119
|
+
|
|
120
|
+
One timing difference is worth remembering: the **engine** is read fresh on every audit, so an edit applies immediately; the **skill body** is read once at plugin startup, so an edit needs a dsh restart.
|
|
121
|
+
|
|
122
|
+
### License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 中文
|
|
129
|
+
|
|
130
|
+
让 **DSH 技能一改就自动被审核** 的插件:技能文件被改动、或技能从备份仓库恢复之后,自动跑一遍技能审核,并把结论回传给模型;另提供 `skill_audit` 工具供随时复验。
|
|
131
|
+
|
|
132
|
+
### 功能
|
|
133
|
+
|
|
134
|
+
审核规则由 `skill-audit` 技能的引擎执行,包含:frontmatter 契约(`name` / `description` / `whenToUse` / `version`)、技能内脚本的可用性(UTF-8 BOM + Windows PowerShell 5.1 可解析)、`SKILL.md` 引用完整性、凭据泄漏、机器专属路径与危险命令模式。
|
|
135
|
+
|
|
136
|
+
- **自动** —— 技能文件被写入 / 编辑之后,或任何整批 restore / backup(以 `mode: 'restore'` 或 `mode: 'backup'` 调用的工具)之后自动审核;
|
|
137
|
+
- **有发现才提示** —— 只在存在 `fail` / `warn` 时把结论作为上下文回传给模型;全部通过时保持安静,只写审核日志;
|
|
138
|
+
- **不阻塞** —— 审核不会让你的工具调用失败。文件已经写入,把问题摆到模型面前才是正确做法;
|
|
139
|
+
- **可主动调用** —— `skill_audit` 工具。
|
|
140
|
+
|
|
141
|
+
### 安装
|
|
17
142
|
|
|
18
143
|
```sh
|
|
19
144
|
dsh plugin --profile web add @caesarloo/dsh-skill-audit
|
|
20
145
|
```
|
|
21
146
|
|
|
147
|
+
**无前置条件** —— 包内自带审核引擎与技能正文,装上即可用。若你的技能根里已有完整的 `skill-audit` 技能,插件不打扰它、直接用你那份。
|
|
148
|
+
|
|
22
149
|
装完**重启 dsh**(插件属于 bundle 层变更,不随热重载生效)。验证:
|
|
23
150
|
|
|
24
151
|
```powershell
|
|
25
152
|
dsh --profile web --dump-config | Select-String tool-skill-audit
|
|
26
153
|
```
|
|
27
154
|
|
|
28
|
-
|
|
155
|
+
### 引擎解析顺序
|
|
156
|
+
|
|
157
|
+
| 优先级 | 来源 | 路径 | 说明 |
|
|
158
|
+
|---|---|---|---|
|
|
159
|
+
| 1 | `config.auditScript` | 你指定的路径 | 显式指定。**指定了却不存在会直接报错**——不会偷偷换一个引擎跑。 |
|
|
160
|
+
| 2 | 你技能根里的一份 | `<skillsRoot>/skill-audit/scripts/audit-skills.ps1` | 可选覆盖位,默认不存在。放一份在这就能用你自己的判据替代包里那份。 |
|
|
161
|
+
| 3 | 包内 | `<package>/skill/scripts/audit-skills.ps1` | **真源**,随插件发布。只有一份,没有需要同步的东西。 |
|
|
162
|
+
|
|
163
|
+
插件**只在你的技能根里还没有完整副本(即没有 `SKILL.md`)时**才在运行时注册 `skill-audit`。这个保护是硬性要求而非客气:DSH 的层级是 `project > runtime > user`,而技能根属于 **user 层**(`source: 'user-dsh'`)——无条件注册会**遮蔽你自己的技能**。
|
|
164
|
+
|
|
165
|
+
同一条切分决定什么进包、什么留给你:
|
|
166
|
+
|
|
167
|
+
| 内容 | 住在哪 | 改一次的代价 |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| 技能正文(边界、触发规则、判据表、豁免与扩展点契约)+ 审核**引擎** | **都在包内**——正文由运行时注册,引擎也从包内解析 | link 安装下立即生效;对他人要**重发一版** |
|
|
170
|
+
| **机器专属规则**——你自己某个技能声明的 `audit_extension` | 你的技能根 | 无——下一次审核即生效,永不需要发版 |
|
|
171
|
+
|
|
172
|
+
所以裸装即可拿到全部能力,而真正会一直变的那部分——你自己的本机规则——留在自己手里,不必重建、不必重启。
|
|
173
|
+
|
|
174
|
+
引擎留在包内才使"**只有一份**"成为可能:技能根下不再有第二份副本可以悄悄变旧。你仍然可以放一份在那里覆盖包内引擎(那个位置被尊重),但没有任何东西依赖它。
|
|
175
|
+
|
|
176
|
+
注册的技能以包内目录作为资源基准(resource base),因此技能正文里的相对脚本路径仍然可解析。
|
|
29
177
|
|
|
30
|
-
|
|
178
|
+
### 使用
|
|
179
|
+
|
|
180
|
+
装好之后**无需任何操作**——改动技能时会自动审核。需要主动复验时调用工具:
|
|
31
181
|
|
|
32
182
|
```
|
|
33
183
|
skill_audit() # 审核全部技能
|
|
@@ -39,10 +189,12 @@ skill_audit({ skill: 'a,b' }) # 只审指定技能(逗号分隔)
|
|
|
39
189
|
| 你的操作 | 审核范围 |
|
|
40
190
|
|---|---|
|
|
41
191
|
| 用 `write` / `edit` 改某个技能的文件 | 只审**该技能**(快) |
|
|
42
|
-
| `
|
|
43
|
-
| 用 shell
|
|
192
|
+
| 任何以 `mode: 'restore'` 或 `mode: 'backup'` 调用的工具 | **全量**(整批覆盖 / 入库前体检) |
|
|
193
|
+
| 用 shell 命令改写技能目录 | **全量** |
|
|
194
|
+
|
|
195
|
+
「整批改写」判定的是**调用形态**而不是工具名——所以本插件配合任何备份插件都能工作,没有也不影响。若你所用工具的模式参数不叫 `mode`,把它的工具名列进 `fullAuditTools` 即可。
|
|
44
196
|
|
|
45
|
-
|
|
197
|
+
提示强度按场景分级:
|
|
46
198
|
|
|
47
199
|
- 定向单技能改动 → 详列 `fail` 与 `warn`;
|
|
48
200
|
- 全量场景 → 只详列 `fail`,`warn` 压成一行汇总;
|
|
@@ -50,32 +202,44 @@ skill_audit({ skill: 'a,b' }) # 只审指定技能(逗号分隔)
|
|
|
50
202
|
|
|
51
203
|
审核日志写在 `<DSH_HOME>/vet/skill-audits/`(`latest.json` 加时间戳档,保留最近 40 份)。
|
|
52
204
|
|
|
53
|
-
|
|
205
|
+
### 配置
|
|
54
206
|
|
|
55
207
|
一般无需配置。需要定制时在 profile 的 `cordis.patch.yml` 里给该条目加 `config`:
|
|
56
208
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```
|
|
209
|
+
| 配置键 | 缺省值 | 含义 |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `skillsRoot` | `<DSH_HOME>/skills` | 技能根目录 |
|
|
212
|
+
| `auditScript` | 自动:技能根 → 包内 | 审核引擎脚本;显式指定可钉住(指定却不存在会报错) |
|
|
213
|
+
| `autoAudit` | `true` | `false` 关闭自动审核(`skill_audit` 工具仍可用) |
|
|
214
|
+
| `powershell` | Windows PowerShell / `pwsh` | PowerShell 可执行文件 |
|
|
215
|
+
| `timeoutMs` | `120000` | 单次审核超时 |
|
|
216
|
+
| `maxContextChars` | `2000` | 回传上下文的字符上限 |
|
|
217
|
+
| `fullAuditTools` | `[]` | 额外视为「整批改写技能目录」的工具名。通常不需要:通用规则已覆盖 `mode` 为 `restore` / `backup` 的调用。 |
|
|
67
218
|
|
|
68
|
-
|
|
219
|
+
### 依赖约定
|
|
69
220
|
|
|
70
|
-
`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-subprocess`、`@deepseek-ai/dsh-llm` 声明为 **optional peerDependencies**,由宿主提供,本包不打包。这是刻意的:它们若与主包各装一份会形成两个模块实例,导致工具注册失败。`dsh-llm`
|
|
221
|
+
`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-subprocess`、`@deepseek-ai/dsh-llm` 声明为 **optional peerDependencies**,由宿主提供,本包不打包。这是刻意的:它们若与主包各装一份会形成两个模块实例,导致工具注册失败。`dsh-llm` 仅用于构造回传消息,缺失时插件降级为「审核照跑、不注入上下文」。
|
|
71
222
|
|
|
72
|
-
|
|
223
|
+
**技能服务**是另一类依赖:它是宿主进程的服务而非包,且**必须**在 `inject` 里声明。这个声明不是形式——cordis 对未声明的服务属性会**直接抛错**而不是返回 `undefined`,所以不声明就读 `ctx.skills` 会让插件装载失败(2026-09-17 实测:`cannot get property "skills" without inject` → `plugin tree failed to load`),"读了再判空"的兜底根本没机会执行。该服务本身在所有环境都存在:由 `@deepseek-ai/dsh-base` 提供,而每个 profile 都基于它。
|
|
73
224
|
|
|
74
|
-
|
|
225
|
+
### 边界(明确不做)
|
|
226
|
+
|
|
227
|
+
- 本身不含审核逻辑:它携带技能正文与引擎、并回传引擎的输出;可编辑的真源有两处——包内(技能正文 + 引擎),以及本机技能里的 `audit_extension`;
|
|
75
228
|
- 不改写工具输入、不阻塞工具调用;
|
|
76
229
|
- 不覆盖 `SessionStart` / `Stop` 等非工具事件;
|
|
77
230
|
- 不监视技能目录的文件变化(只在工具调用后触发)。
|
|
78
231
|
|
|
79
|
-
|
|
232
|
+
### 维护 `skill/`(插件作者)
|
|
233
|
+
|
|
234
|
+
`skill/` 里两部分——`SKILL.md`(技能正文)与 `scripts/audit-skills.ps1`(引擎)——**都是真源**,就在本仓库里直接编辑。**同步步骤已经取消**:引擎过去住在作者技能根里、必须复制进包,于是存在第二份副本、随时可能悄悄变旧。那份副本连同这一整类问题都已消失。
|
|
235
|
+
|
|
236
|
+
现在由两道检查守住契约:
|
|
237
|
+
|
|
238
|
+
- `npm test` 用**真引擎**审包内技能(按真实技能布局摆好),并断言引擎的 UTF-8 BOM 还在——所以引擎不可能以"解析不了"的状态发布,技能正文也不可能带着断裂发布;
|
|
239
|
+
- `prepublishOnly` 跑 `build && test`,没通过这两道就发不出去。
|
|
240
|
+
|
|
241
|
+
一个生效时机差异值得记住:**引擎**每次审核现读,改完立即生效;**技能正文**只在插件启动时读一次,改完要重启 dsh。
|
|
242
|
+
|
|
243
|
+
### License
|
|
80
244
|
|
|
81
245
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -2,27 +2,55 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 为什么必须是插件,而不是 hooks 桥接:
|
|
4
4
|
// @deepseek-ai/dsh-hooks-claude-code 通过 `ctx.shell` 运行钩子命令。当宿主没有可用的沙箱
|
|
5
|
-
// runner
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// runner 时(执行器按设计 fail-closed、绝不静默降级,表现为 `SANDBOX_UNAVAILABLE`),钩子
|
|
6
|
+
// 命令根本无法启动 —— 钩子配置配得再对也没用。本插件在 harness 进程内用 `ctx.subprocess`
|
|
7
|
+
// (host 层)直接跑审核脚本,绕开该限制;这也是官方对"没有 Claude Code 对应物的定制行为"
|
|
8
|
+
// 给出的推荐形态。
|
|
9
9
|
//
|
|
10
10
|
// 两条通道:
|
|
11
11
|
// 1) `tools/post-execute` 自动触发(写入**之后**,审的是新内容):
|
|
12
12
|
// · write / edit 命中 <DSH_HOME>/skills/<技能>/ → 只审该技能
|
|
13
|
-
// ·
|
|
13
|
+
// · 调用形态为「整批改写」的工具(参数 mode 为 restore / backup)→ 全量(整批覆盖 / 入库前)
|
|
14
14
|
// · pwsh 等 shell,命令行同时含 skills 与写操作迹象 → 全量
|
|
15
15
|
// 2) `skill_audit` 工具 —— agent 可主动定向或全量审核。
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// 审核逻辑不在本插件内(单一真源),按优先级三选一:
|
|
18
|
+
// 1) config.auditScript(显式;指定了却不存在 → 直接报错,不静默回落)
|
|
19
|
+
// 2) 用户态技能 <DSH_HOME>/skills/skill-audit/scripts/audit-skills.ps1 —— 可选覆盖位
|
|
20
|
+
// 3) 包内 <pkg>/skill/scripts/audit-skills.ps1 —— 随版本发布的真源
|
|
21
|
+
//
|
|
22
|
+
// 分层(2026-09-17 定):**常改的**与**不常改的**分开住,各自只有一个真源。
|
|
23
|
+
// · 技能正文(边界、触发规则、判据表、豁免与扩展点契约)+ **判据引擎** → 都在**包内**:
|
|
24
|
+
// 正文由运行时注册提供,引擎即包内那份脚本。开发态(把本包 link 进 profile)下改它们
|
|
25
|
+
// 立即生效,但要发版才对他人生效——**因此不再有"包内 vs 技能根"两份引擎可以分叉**。
|
|
26
|
+
// · 机器专属规则 → **本地**扩展技能的 audit_extension(改完即生效,永远不必发版)。
|
|
27
|
+
// 注意包内引擎与技能正文**在不同时机生效**:引擎是每次审核现读的文件(改完即生效,不必重启),
|
|
28
|
+
// 而技能正文只在插件启动注册时读一次(改完要重启 dsh)。
|
|
29
|
+
// 用户态那一档仍然保留:某台机器若想用自己的引擎(或想覆盖包内判据),放一份在那即可;
|
|
30
|
+
// 若放的是**完整技能**(含 SKILL.md),则连注册都让位——层级是 project > runtime > user,
|
|
31
|
+
// 无条件注册会遮蔽它(硬约束,见 registerFallbackSkill)。
|
|
32
|
+
// 引擎缺失时工具报明确错误、自动触发静默跳过(不打扰正常写文件)。
|
|
33
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
34
|
import { stat } from 'node:fs/promises';
|
|
21
|
-
import { isAbsolute, join, resolve as resolvePath } from 'node:path';
|
|
35
|
+
import { dirname, isAbsolute, join, resolve as resolvePath } from 'node:path';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
22
37
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
23
38
|
// Plugin display name, shown in loader diagnostics.
|
|
24
39
|
export const name = 'tool-skill-audit';
|
|
25
|
-
|
|
40
|
+
// 'skills' 必须在 inject 里声明,**不能靠"读了再判空"**:cordis 对未在 inject 声明过的
|
|
41
|
+
// 服务属性直接抛错,而不是返回 undefined(报错形如
|
|
42
|
+
// `cannot get property "skills" without inject` → `plugin tree failed to load`,
|
|
43
|
+
// 即整个插件装载失败、进程起不来),
|
|
44
|
+
// 于是 registerFallbackSkill 里 `skills === undefined` 的兜底根本到不了。
|
|
45
|
+
// 代价:cordis 4 的 inject 只有**必需**语义(没有 required/optional 之分),本插件会等
|
|
46
|
+
// skills 服务就绪才 apply。本项目里无风险——skills 由 `@deepseek-ai/dsh-base` 提供
|
|
47
|
+
// (实测 web / headless 两个 profile 都基于它),而本 bundle 排在 bundles 末尾。
|
|
48
|
+
// 备选:cordis 另有 `ctx.get('skills')`,它**不需要 inject**、未提供时返回 undefined;
|
|
49
|
+
// 若将来要让"审核为主、注册技能为辅"彻底解耦(审核不因注册失败而受累),改用它才是正解。
|
|
50
|
+
export const inject = ['tools', 'subprocess', 'skills'];
|
|
51
|
+
const SKILL_NAME = 'skill-audit';
|
|
52
|
+
const ENGINE_RELATIVE = join('scripts', 'audit-skills.ps1');
|
|
53
|
+
const BUNDLED_SKILL_DIRNAME = 'skill';
|
|
26
54
|
const POWERSHELL = process.platform === 'win32'
|
|
27
55
|
? 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
|
28
56
|
: 'pwsh';
|
|
@@ -45,6 +73,91 @@ async function fileExists(path) {
|
|
|
45
73
|
return false;
|
|
46
74
|
}
|
|
47
75
|
}
|
|
76
|
+
export function resolveEngine(options) {
|
|
77
|
+
const isFile = options.isFile ?? ((p) => existsSync(p));
|
|
78
|
+
if (options.configured) {
|
|
79
|
+
return isFile(options.configured) ? { path: options.configured, source: 'config' } : null;
|
|
80
|
+
}
|
|
81
|
+
const userEngine = join(options.skillsRoot, SKILL_NAME, ENGINE_RELATIVE);
|
|
82
|
+
if (isFile(userEngine))
|
|
83
|
+
return { path: userEngine, source: 'user-skill' };
|
|
84
|
+
if (options.bundledSkillDir) {
|
|
85
|
+
const bundled = join(options.bundledSkillDir, ENGINE_RELATIVE);
|
|
86
|
+
if (isFile(bundled))
|
|
87
|
+
return { path: bundled, source: 'bundled' };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/** 包内 skill/ 目录(技能正文与引擎的真源):从本模块向上找含 skill/SKILL.md 的目录(逐文件 dist/ 与打包形态都可定位)。 */
|
|
92
|
+
export function resolveBundledSkillDir(startUrl = import.meta.url) {
|
|
93
|
+
let dir = dirname(fileURLToPath(startUrl));
|
|
94
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
95
|
+
const candidate = join(dir, BUNDLED_SKILL_DIRNAME);
|
|
96
|
+
if (existsSync(join(candidate, 'SKILL.md')))
|
|
97
|
+
return candidate;
|
|
98
|
+
const parent = dirname(dir);
|
|
99
|
+
if (parent === dir)
|
|
100
|
+
break;
|
|
101
|
+
dir = parent;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** 极简 frontmatter 取值(只认单行 `key: value`,够用且不为此引 YAML 依赖)。 */
|
|
106
|
+
function frontmatterField(content, key) {
|
|
107
|
+
// 注意:行内标志 (?m) 是 .NET/PCRE 写法,JS 的 RegExp 会抛 "Invalid group" —— 多行标志必须走第二参数。
|
|
108
|
+
const match = content.match(new RegExp(`^${key}\\s*:\\s*(.+?)\\s*$`, 'm'));
|
|
109
|
+
if (!match)
|
|
110
|
+
return undefined;
|
|
111
|
+
return match[1].replace(/^["']|["']$/g, '');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 把回退技能正文里的"用户态引擎路径"改写成实际生效的路径。
|
|
115
|
+
* 只替换**精确字面量**:真源改了措辞就自然不再匹配,不会误伤别的路径。
|
|
116
|
+
*/
|
|
117
|
+
export function rewriteEnginePaths(content, enginePath) {
|
|
118
|
+
return content
|
|
119
|
+
.replace(/\$env:USERPROFILE\\\.dsh\\skills\\skill-audit\\scripts\\audit-skills\.ps1/g, enginePath)
|
|
120
|
+
.replace(/<DSH_HOME>[\\/]skills[\\/]skill-audit[\\/]scripts[\\/]audit-skills\.ps1/g, enginePath);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 注册包内技能——**仅当用户态没有一份完整的同名技能(含 SKILL.md)时**。
|
|
124
|
+
*
|
|
125
|
+
* 守卫的用意:该位置只要出现一份**完整技能**(含 SKILL.md),就说明那台机器要自己维护它,
|
|
126
|
+
* 插件必须让位。默认情形(该位置不存在,或只有脚本)则由包内提供技能正文——技能正文与判据
|
|
127
|
+
* 引擎的真源都在包内,故这一支是常态。
|
|
128
|
+
*
|
|
129
|
+
* 硬约束:dsh-skill 的 `register()` 层级是 **project > runtime > user**,而
|
|
130
|
+
* `<DSH_HOME>/skills` 属于 user 层(`source: 'user-dsh'`)——若某台机器上保留了**完整的**
|
|
131
|
+
* 用户态技能,无条件注册同名 runtime 技能会**遮蔽它**(2026-09-17 查 dsh-skill 的
|
|
132
|
+
* `lib/types/index.d.ts` 确认)。故这里先做文件系统判定,存在就一步都不做。
|
|
133
|
+
*
|
|
134
|
+
* `skipped-no-service` 是**防御性**分支:skills 已在 `inject` 里声明,服务必然就绪,
|
|
135
|
+
* 正常跑不到;留着是因为"服务在、但形态变了(没有 register)"无法由 inject 保证。
|
|
136
|
+
*/
|
|
137
|
+
export function registerFallbackSkill(ctx, options) {
|
|
138
|
+
if (existsSync(join(options.skillsRoot, SKILL_NAME, 'SKILL.md')))
|
|
139
|
+
return 'skipped-user-skill';
|
|
140
|
+
const skills = ctx.skills;
|
|
141
|
+
if (skills === undefined || typeof skills.register !== 'function')
|
|
142
|
+
return 'skipped-no-service';
|
|
143
|
+
if (!options.bundledSkillDir)
|
|
144
|
+
return 'skipped-no-bundle';
|
|
145
|
+
const skillMd = join(options.bundledSkillDir, 'SKILL.md');
|
|
146
|
+
if (!existsSync(skillMd))
|
|
147
|
+
return 'skipped-no-bundle';
|
|
148
|
+
const content = readFileSync(skillMd, 'utf8');
|
|
149
|
+
skills.register({
|
|
150
|
+
name: SKILL_NAME,
|
|
151
|
+
description: frontmatterField(content, 'description') ??
|
|
152
|
+
'DSH skill audit: deterministic static checks over skill frontmatter, script usability, reference integrity, credential leakage, machine-specific paths and dangerous commands.',
|
|
153
|
+
whenToUse: frontmatterField(content, 'whenToUse') ??
|
|
154
|
+
'A skill was created, edited, or restored from a backup repository; you need to know whether a skill is usable, self-consistent, and free of leaked credentials.',
|
|
155
|
+
source: 'runtime',
|
|
156
|
+
content: rewriteEnginePaths(content, options.enginePath),
|
|
157
|
+
resourceBase: { kind: 'directory', path: options.bundledSkillDir },
|
|
158
|
+
});
|
|
159
|
+
return 'registered';
|
|
160
|
+
}
|
|
48
161
|
/** 从工具参数里尽力提取被操作的路径(不同工具键名不同,另有兜底扫描)。 */
|
|
49
162
|
export function candidatePaths(args) {
|
|
50
163
|
const out = [];
|
|
@@ -85,14 +198,25 @@ const FILE_WRITE_TOOLS = new Set([
|
|
|
85
198
|
'str_replace_editor',
|
|
86
199
|
]);
|
|
87
200
|
const WRITE_HINTS = /(?:Set-Content|Out-File|Add-Content|Clear-Content|Copy-Item|Move-Item|Remove-Item|New-Item|robocopy|git\s+(?:checkout|restore|apply))/i;
|
|
201
|
+
/**
|
|
202
|
+
* 「整批改写」的调用形态:这类工具会大范围改动技能目录(备份 / 恢复 / 同步),但**参数里不带路径**,
|
|
203
|
+
* 没法靠路径判定,只能靠调用形态。判据是**参数**而不是工具名——插件因此不绑定任何具体备份插件。
|
|
204
|
+
*
|
|
205
|
+
* 2026-09-17 去硬依赖:原先写成按**工具名**硬匹配,对没有装那个备份插件的用户是纯死逻辑。
|
|
206
|
+
* 现在只要参数是 `mode: 'restore' | 'backup'` 就触发,无论工具叫什么(真实备份操作必然带 `mode`,
|
|
207
|
+
* 故行为不变);模式名不叫 `mode` 的场景用 config.fullAuditTools 显式补充。
|
|
208
|
+
*/
|
|
209
|
+
const BULK_MODES = new Set(['restore', 'backup']);
|
|
88
210
|
/** 决定这次工具调用要不要触发审核;返回 null 表示与该工具无关。 */
|
|
89
|
-
export function planAudit(toolName, args, skillsRoot) {
|
|
211
|
+
export function planAudit(toolName, args, skillsRoot, fullAuditTools = [], isSkillDir = (dir) => existsSync(join(dir, 'SKILL.md'))) {
|
|
90
212
|
const lower = toolName.toLowerCase();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
213
|
+
const record = (args ?? {});
|
|
214
|
+
if (fullAuditTools.some((t) => t.toLowerCase() === lower)) {
|
|
215
|
+
return { skills: null, scope: `${toolName}(配置为整批改写) → 全量` };
|
|
216
|
+
}
|
|
217
|
+
const mode = typeof record.mode === 'string' ? record.mode.toLowerCase() : '';
|
|
218
|
+
if (BULK_MODES.has(mode)) {
|
|
219
|
+
return { skills: null, scope: `${toolName}(${mode}) → 全量` };
|
|
96
220
|
}
|
|
97
221
|
if (SHELL_TOOLS.has(lower)) {
|
|
98
222
|
const command = String(args?.command ?? '');
|
|
@@ -115,7 +239,23 @@ export function planAudit(toolName, args, skillsRoot) {
|
|
|
115
239
|
}
|
|
116
240
|
if (hits.length === 0)
|
|
117
241
|
return null;
|
|
118
|
-
|
|
242
|
+
// 命中项分两类。**只有含 SKILL.md 的目录才是技能**,可作为定向审核目标;
|
|
243
|
+
// skillsRoot 下一旦出现"无 SKILL.md 的目录被改写",它就不是技能(技能目录还没成形,
|
|
244
|
+
// 或只是有人把脚本放进了技能根),此时定向审核无从谈起 → 一律升级为**全量重审**。
|
|
245
|
+
// 顺带消除一个真误报:若仍按目录名送 `-Skill`,引擎会因该目录无 SKILL.md 报 `F1 缺少 SKILL.md`。
|
|
246
|
+
let nonSkillTouched = false;
|
|
247
|
+
const skills = [];
|
|
248
|
+
for (const name of [...new Set(hits)]) {
|
|
249
|
+
if (isSkillDir(join(skillsRoot, name)))
|
|
250
|
+
skills.push(name);
|
|
251
|
+
else
|
|
252
|
+
nonSkillTouched = true;
|
|
253
|
+
}
|
|
254
|
+
if (nonSkillTouched) {
|
|
255
|
+
return { skills: null, scope: `${toolName} → 引擎/资产目录改动(非技能目录)→ 全量` };
|
|
256
|
+
}
|
|
257
|
+
if (skills.length === 0)
|
|
258
|
+
return null;
|
|
119
259
|
return { skills, scope: `${toolName} → 技能 ${skills.join(', ')}` };
|
|
120
260
|
}
|
|
121
261
|
export function parseReport(text) {
|
|
@@ -140,13 +280,17 @@ export function parseReport(text) {
|
|
|
140
280
|
}
|
|
141
281
|
export function apply(ctx, config = {}) {
|
|
142
282
|
const skillsRoot = (config.skillsRoot ?? join(dshHome(), 'skills')).replace(/[\\/]+$/, '');
|
|
143
|
-
const
|
|
283
|
+
const bundledSkillDir = resolveBundledSkillDir();
|
|
284
|
+
const resolved = resolveEngine({ configured: config.auditScript, skillsRoot, bundledSkillDir });
|
|
285
|
+
// 解析不到时仍算出"期望路径"作为报错文案(显式配置优先展示用户给的那条)。
|
|
286
|
+
const auditScript = resolved?.path ?? config.auditScript ?? join(skillsRoot, SKILL_NAME, ENGINE_RELATIVE);
|
|
144
287
|
const powershell = config.powershell ?? POWERSHELL;
|
|
145
288
|
const autoAudit = config.autoAudit !== false;
|
|
146
289
|
const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
147
290
|
const maxContextChars = config.maxContextChars && config.maxContextChars > 0
|
|
148
291
|
? config.maxContextChars
|
|
149
292
|
: DEFAULT_MAX_CONTEXT_CHARS;
|
|
293
|
+
const fullAuditTools = Array.isArray(config.fullAuditTools) ? config.fullAuditTools : [];
|
|
150
294
|
async function runAudit(skills, signal) {
|
|
151
295
|
if (!(await fileExists(auditScript))) {
|
|
152
296
|
return { exitCode: -1, stdout: '', stderr: `审核脚本不存在: ${auditScript}` };
|
|
@@ -232,9 +376,9 @@ export function apply(ctx, config = {}) {
|
|
|
232
376
|
/**
|
|
233
377
|
* 构造回传上下文;只有存在需要行动的问题时才返回消息。
|
|
234
378
|
*
|
|
235
|
-
* `failsOnly` 用于**全量场景**(restore/backup、shell 批量改写):那里 warn
|
|
236
|
-
*
|
|
237
|
-
*
|
|
379
|
+
* `failsOnly` 用于**全量场景**(restore/backup、shell 批量改写):那里 warn 的绝对数量很大,
|
|
380
|
+
* 逐条列出会把上下文挤爆且失去焦点 —— 全量时只详列 fail,warn 压成一行汇总;
|
|
381
|
+
* 定向单技能时(通常只有个位数条)才 fail + warn 都列。
|
|
238
382
|
* 全量且**只有 warn**时直接返回 undefined:那属于背景噪音,不该打断任何一次写入。
|
|
239
383
|
*/
|
|
240
384
|
async function buildContextMessage(report, scope, failsOnly = false) {
|
|
@@ -350,7 +494,7 @@ export function apply(ctx, config = {}) {
|
|
|
350
494
|
const toolName = typeof exec?.name === 'string' ? exec.name : '';
|
|
351
495
|
if (!toolName || typeof next !== 'function')
|
|
352
496
|
return await next?.();
|
|
353
|
-
const plan = planAudit(toolName, exec?.arguments, skillsRoot);
|
|
497
|
+
const plan = planAudit(toolName, exec?.arguments, skillsRoot, fullAuditTools);
|
|
354
498
|
if (!plan)
|
|
355
499
|
return await next();
|
|
356
500
|
const run = await runAudit(plan.skills, exec?.signal);
|
|
@@ -373,5 +517,12 @@ export function apply(ctx, config = {}) {
|
|
|
373
517
|
});
|
|
374
518
|
}
|
|
375
519
|
}
|
|
376
|
-
|
|
520
|
+
// 回退技能注册与 autoAudit 无关:它决定"模型能不能加载到 skill-audit 技能",
|
|
521
|
+
// 而不是"要不要自动跑审核"。用户态技能在场时这一步是空操作。
|
|
522
|
+
const fallbackSkill = registerFallbackSkill(ctx, {
|
|
523
|
+
skillsRoot,
|
|
524
|
+
bundledSkillDir,
|
|
525
|
+
enginePath: auditScript,
|
|
526
|
+
});
|
|
527
|
+
ctx.logger.info(`[tool-skill-audit] registered "skill_audit" — engine=${auditScript} (${resolved?.source ?? 'MISSING'}) skillsRoot=${skillsRoot} autoAudit=${autoAudit} fallbackSkill=${fallbackSkill}`);
|
|
377
528
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caesarloo/dsh-skill-audit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Audit DSH skills automatically: a host-layer tools/post-execute plugin that
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Audit DSH skills automatically: a host-layer tools/post-execute plugin that ships both the skill body and the audit engine, runs the engine after skill files change (write/edit/shell) or after a bulk restore/backup (any tool invoked with mode: restore|backup), feeding findings back to the model as context; also registers the skill_audit tool.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"deepseek-harness",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
26
|
-
"cordis.patch.yml"
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"skill"
|
|
27
28
|
],
|
|
28
29
|
"dsh": {
|
|
29
30
|
"bundle": {
|
|
@@ -33,7 +34,9 @@
|
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "tsc -p tsconfig.json",
|
|
35
36
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
-
"test": "node test/smoke.mjs"
|
|
37
|
+
"test": "node test/smoke.mjs && node scripts/check-leaks.mjs",
|
|
38
|
+
"check-leaks": "node scripts/check-leaks.mjs",
|
|
39
|
+
"prepublishOnly": "npm run build && npm test"
|
|
37
40
|
},
|
|
38
41
|
"peerDependencies": {
|
|
39
42
|
"@deepseek-ai/cordis": "^4.0.1",
|