dsh-immune-system 0.9.2 → 1.0.1
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 +192 -17
- package/dsh-immune-system/client/client.js +375 -159
- package/dsh-immune-system/dist/core/distiller.js +78 -0
- package/dsh-immune-system/dist/core/pairing.js +188 -0
- package/dsh-immune-system/dist/core/prompts.js +88 -2
- package/dsh-immune-system/dist/core/selfnoise.js +30 -5
- package/dsh-immune-system/dist/core/similarity.js +51 -0
- package/dsh-immune-system/dist/core/store/antibodies.js +37 -8
- package/dsh-immune-system/dist/core/store/episodes.js +19 -5
- package/dsh-immune-system/dist/core/store/index.js +58 -35
- package/dsh-immune-system/dist/core/store/jobs.js +17 -0
- package/dsh-immune-system/dist/core/store/open.js +1 -0
- package/dsh-immune-system/dist/core/store/rerank.js +57 -0
- package/dsh-immune-system/dist/core/store/schema.js +9 -0
- package/dsh-immune-system/dist/core/store/turn-state.js +52 -0
- package/dsh-immune-system/dist/core/ui/page.js +481 -132
- package/dsh-immune-system/dist/core/ui/query.js +12 -2
- package/dsh-immune-system/dist/core/ui/server.js +2 -2
- package/dsh-immune-system/dist/dsh/src/dsh-adapter.js +66 -17
- package/dsh-immune-system/dist/dsh/src/index.js +143 -9
- package/dsh-immune-system/dist/dsh/src/ui-route.js +7 -3
- package/dsh-immune-system/dist/zcode/src/doctor.js +51 -3
- package/dsh-immune-system/dist/zcode/src/drain.js +73 -20
- package/dsh-immune-system/dist/zcode/src/hooks/post-tool-use-failure.js +3 -1
- package/dsh-immune-system/dist/zcode/src/hooks/post-tool-use.js +4 -2
- package/dsh-immune-system/dist/zcode/src/hooks/session-start.js +6 -0
- package/dsh-immune-system/dist/zcode/src/hooks/user-prompt-submit.js +3 -0
- package/dsh-immune-system/dist/zcode/src/mcp-server.js +22 -0
- package/dsh-immune-system/package.json +7 -7
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ dsh plugin --profile <profile> add dsh-immune-system
|
|
|
55
55
|
**tarball / git**(等价,无需 npm 账号):
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
cd dsh-immune-system && npm pack # → dsh-immune-system
|
|
59
|
-
dsh plugin --profile <profile> add ./dsh-immune-system
|
|
58
|
+
cd dsh-immune-system && npm pack # → dsh-immune-system-<version>.tgz
|
|
59
|
+
dsh plugin --profile <profile> add ./dsh-immune-system-<version>.tgz
|
|
60
60
|
dsh plugin --profile <profile> add github:meat-daemon-404/dsh-plugin-immune
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -90,7 +90,7 @@ node "<zcode.cjs 路径>" plugins install dsh-immune-system@dev-dsh-plugin-immun
|
|
|
90
90
|
|
|
91
91
|
| 命令 | 宿主 | 作用 |
|
|
92
92
|
| --- | --- | --- |
|
|
93
|
-
| `/immune` | DSH | 列出所有抗体(`/immune --all`
|
|
93
|
+
| `/immune` | DSH | 列出所有抗体(`/immune --all` 含已退役);`/immune --budget` 查看蒸馏预算,`--budget <ms>` 本次会话内改(内存态,重启回到环境变量) |
|
|
94
94
|
| `/immune-export` | DSH | 把经过验证的抗体导出成 `SKILL.md` |
|
|
95
95
|
| `/immune-forget <id>` | DSH | 删除指定抗体 |
|
|
96
96
|
| `/immune-forget --last` | DSH | 撤销上一回合刚学的那条 |
|
|
@@ -115,8 +115,9 @@ node "<zcode.cjs 路径>" plugins install dsh-immune-system@dev-dsh-plugin-immun
|
|
|
115
115
|
`/dsh-immune-system:immune-ui`(或 `cli.js ui`)会起一个本地页面:
|
|
116
116
|
|
|
117
117
|
- 搜索触发词/症状/处方,实时过滤;**浏览不计入命中数**,所以晋升和淘汰的统计不会被点鼠标污染
|
|
118
|
-
- 每条可**退役 / 恢复 /
|
|
119
|
-
-
|
|
118
|
+
- 每条可**退役 / 恢复 / 删除**,分「项目库」「共享库」两个页签,可按「最近更新 / 命中最多 / 失效最多」排序
|
|
119
|
+
- 顶部 5 张 KPI:生效数、退役数、累计注入、共享库条数,外加**当前蒸馏预算**
|
|
120
|
+
- 跟随系统深浅色、窄屏自动把表格降级成卡片、骨架屏与空态都是有设计的那种
|
|
120
121
|
|
|
121
122
|
安全边界是刻意收窄的:只绑 `127.0.0.1`、每次启动生成随机 token 且所有 `/api` 都要带它
|
|
122
123
|
(跨站简单 POST 不需要预检,只靠 Origin 判断不够)、严格 CSP、页面不加载任何外部资源。
|
|
@@ -127,20 +128,34 @@ UI 是自建的而非宿主插件 UI——ZCode 插件没有 UI 插槽(只有
|
|
|
127
128
|
#### the agent harness 原生 UI(设置页里的 tab)
|
|
128
129
|
|
|
129
130
|
the agent harness 有 web GUI 和客户端插槽,所以那边**不用另开页面**:
|
|
130
|
-
`设置 → 插件` 里会出现一个「免疫 / Immune」tab
|
|
131
|
+
`设置 → 插件` 里会出现一个「免疫 / Immune」tab。它与独立仪表盘共用同一套设计:
|
|
132
|
+
KPI(含预算)、搜索与清除、页签计数、排序、状态圆点、窄屏卡片、骨架屏与空态。
|
|
131
133
|
|
|
132
134
|
- 客户端是 `dsh-immune-system/client/client.js`,按模块装载器契约手写
|
|
133
135
|
(`window.__ModuleLoader__.load({ id, factory })`),**不需要打包器、不写 JSX**
|
|
136
|
+
- 样式是注入一段静态 `<style>`(选择器全部 scope 在 `.imm` 下),所以 hover、媒体查询、
|
|
137
|
+
跟随宿主的 `--dsh-*` 深浅色变量都能用,而不是几十个 inline style
|
|
134
138
|
- 数据走宿主路由 `/plugins/dsh-immune-system/api`,用
|
|
135
139
|
`ctx.inject(['webServer'], …)` 注册——这是可选依赖,CLI/桌面 profile 没有 web 服务时
|
|
136
140
|
插件照常工作,只是没有这个 tab
|
|
141
|
+
- **manifest 必须同时声明** `dsh.client: { platform: "web" }` 与字符串
|
|
142
|
+
`exports["./client"]`。宿主的 `client-modules` 只按这两条把包认成「浏览器半」,
|
|
143
|
+
再把文件原样挂到 `/plugins/<id>/client.js?rev=<hash>`;少一条,文件在磁盘上存在
|
|
144
|
+
但路由 404、tab 永不出现(而且判定在本进程内缓存,必须重启才生效)。
|
|
145
|
+
`test/manifests.test.mjs` 与 `scripts/verify-installed.mjs` 把声明、导出目标与
|
|
146
|
+
文件内容三件事都钉死了
|
|
137
147
|
- 变更类请求要求 `content-type: application/json` 且同源:JSON 会强制 CORS 预检,
|
|
138
148
|
跨站页面过不去
|
|
139
149
|
|
|
140
150
|
> 诚实说明:客户端与路由的**契约**有测试(模拟 `__ModuleLoader__` 装载、假 ctx 验证路由、
|
|
141
|
-
> 跨源拒绝、无 web
|
|
142
|
-
>
|
|
143
|
-
>
|
|
151
|
+
> 跨源拒绝、无 web 服务时不注册、预算透传),布局则用真实 React 18 做过静态渲染自检
|
|
152
|
+
> (含深色模式、空态、窄屏)。
|
|
153
|
+
>
|
|
154
|
+
> **已在真实宿主里验证过**:0.9.8 补齐 `dsh.client` 声明后,`设置 → 插件` 里的 tab
|
|
155
|
+
> 确实出现了(此前 `/plugins/dsh-immune-system/client.js` 返回 404,只有宿主半在跑)。
|
|
156
|
+
> 0.9.12 进一步把它放进 **shadow root**——真实的设置弹窗会用自己的样式表重排裸
|
|
157
|
+
> `table`/`td`/`div`,导致行被压成几像素、下拉框空白;shadow root 双向隔离,
|
|
158
|
+
> 宿主 CSS 进不来、我们的样式也出不去。
|
|
144
159
|
|
|
145
160
|
### 同步与体检
|
|
146
161
|
|
|
@@ -170,8 +185,13 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
|
|
|
170
185
|
- DSH:`result.isError` **只覆盖基础设施失败**(spawn 错误、abort、监听器抛错),
|
|
171
186
|
非零退出被明确设计为「reported, not errored」,模型自己决定怎么办。
|
|
172
187
|
|
|
173
|
-
所以两版都自己判结果。DSH
|
|
174
|
-
`isError` →
|
|
188
|
+
所以两版都自己判结果。DSH 侧的判据按权威度排序:
|
|
189
|
+
`isError` → sandbox 拒绝标记(判失败但**不入库学习**)→ kill/timeout(含 `value.timedOut`)→
|
|
190
|
+
结构化 `value.exitCode`(**有则权威:0 成功、非 0 失败,不再看文本**)→
|
|
191
|
+
有结构化 `value` 但无退出状态(**载荷即结论,正文是数据,不扫**)→ 行尾 `[exit code: N]` →
|
|
192
|
+
窄口径失败标记正则(**仅当无任何结构化状态**)。后面两步刻意让位给结构化信号:一条退出 0
|
|
193
|
+
的命令在 `grep` 输出里顺带印出 `command not found` 是常事,而 `read` 一个含 `Error:` 的日志
|
|
194
|
+
文件更是日常——都不该被记成失败。
|
|
175
195
|
|
|
176
196
|
**② 抗体失效会被淘汰(Anti-Rot)**
|
|
177
197
|
|
|
@@ -232,6 +252,32 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
|
|
|
232
252
|
所以它挂在 `agent/turn-stopping`(`@mode serial`,回合边界提交前 await)。
|
|
233
253
|
未清空的队列还会在下次 `session-start` 由 `requeueStuckJobs()` 兜底。
|
|
234
254
|
|
|
255
|
+
### 失败怎么被判出来(四层判定 × 三类失败)
|
|
256
|
+
|
|
257
|
+
`judgeOutcome` 不是「看一个 boolean」,而是按权威顺序逐层走。三类常见失败在四层里的落点并不相同:
|
|
258
|
+
|
|
259
|
+
| 层 | 判据 | bash 非零退出 | 超时 | 沙箱拒绝 |
|
|
260
|
+
| --- | --- | --- | --- | --- |
|
|
261
|
+
| 1 | `result.isError`(宿主**只**对基础设施失败置真) | ✗ 返回 `isError:false` | 可能 ✗ | 偶尔 ✓(file 工具抛异常) |
|
|
262
|
+
| 2 | 沙箱标记 `[sandbox: file access denied under … mode]` | ✗ | ✗ | ✓ 命中 → 打 `sandboxDenied`,**不记录、不惩罚、不蒸馏** |
|
|
263
|
+
| 3 | kill / timed-out 标记 + `value.timedOut` | ✗ | ✓ 命中(即便 `exitCode=0`) | ✗ |
|
|
264
|
+
| 4 | 结构化 `value.exitCode`(shell 工具的权威判据) | ✓ 非 0 即失败 | — | — |
|
|
265
|
+
| 5 | 尾标记 `[exit code: N]` / 特征词(**仅**当没有任何结构化状态) | 兜底 | 兜底 | — |
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
一次工具结果
|
|
269
|
+
├─ isError ──────────────────────────────→ 失败(error.message 原样记入 trace)
|
|
270
|
+
├─ 沙箱拒绝标记 ─────────────────────────→ 拒绝:不记录 / 不惩罚 / 不蒸馏
|
|
271
|
+
├─ killed / timed out ───────────────────→ 失败
|
|
272
|
+
├─ value.exitCode 存在 ──┬─ 0 ───────────→ 成功(此后不再扫文本)
|
|
273
|
+
│ └─ ≠0 ──────────→ 失败
|
|
274
|
+
├─ value 为非空对象(无退出码)──────────→ 成功(载荷即结论)
|
|
275
|
+
├─ [exit code: N] / 特征词 ──────────────→ 失败
|
|
276
|
+
└─ 其余 ─────────────────────────────────→ 成功
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
要点:**结构化 exitCode 一旦存在就压过文本**——`grep` 命中内容、测试打印 `Error:` 都不会被误判成功/失败;而 bash 的非零退出正是靠第 4 层抓到的(宿主本身不会把它置成 `isError`)。这也解释了为什么「读一份含 `Error:` 的日志」不会凭空长出一条抗体。
|
|
280
|
+
|
|
235
281
|
### 存储
|
|
236
282
|
|
|
237
283
|
抗体库是 SQLite 单文件(含 FTS5 trigram 索引,中文按 3 字窗口匹配)。
|
|
@@ -255,12 +301,30 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
|
|
|
255
301
|
| `IMMUNE_LLM_BASE_URL` | OpenAI 兼容端点,如 `https://api.example.com/v1` |
|
|
256
302
|
| `IMMUNE_LLM_MODEL` | 模型名,与上面配对使用 |
|
|
257
303
|
| `IMMUNE_LLM_API_KEY` | Bearer 令牌 |
|
|
304
|
+
| `IMMUNE_DISTILL_BUDGET_MS` | 单次回合末蒸馏的时间预算,默认 `12000`(两版同名同默认,钳制在 1–60 s);超预算的队列留到下次边界 |
|
|
258
305
|
| `IMMUNE_LOCALE` | `zh-CN` / `en` 强制单语;**默认中英双语** |
|
|
259
306
|
| `IMMUNE_ALLOW_PRIVATE_ENDPOINT` | 设为 `1` 才允许环回/私有端点。**默认拒绝**,防 SSRF |
|
|
260
307
|
| `IMMUNE_GLOBAL_LIBRARY` | 设为 `0` 关闭全局共享库。默认开启 |
|
|
261
308
|
| `IMMUNE_GUARD` | `off`(默认)/ `warn` / `deny` —— 高危错招事前检查,见下 |
|
|
262
309
|
| `IMMUNE_DEBUG_LOG` | 指定文件则记录执行细节,用于排障 |
|
|
263
310
|
|
|
311
|
+
### 什么时候值得开 LLM 蒸馏(成本 / 收益)
|
|
312
|
+
|
|
313
|
+
不配 `IMMUNE_LLM_*` 时走离线规则蒸馏器:零成本、结果确定,只从失败/成功的调用串里抽关键词拼出 trigger / prescription。配上模型后,同一段「失败→成功」会被改写成一句更像人话的处方(跨项目复用、导出成 `SKILL.md` 都更好),代价是**每条蒸馏一次模型往返**。
|
|
314
|
+
|
|
315
|
+
实测(`scripts/bench-tool-latency.mjs`,mock 端点 1500 ms/次):
|
|
316
|
+
|
|
317
|
+
| 队列深度 | `agent/turn-stopping` 耗时 | 蒸馏条数 |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| 1 | ~1.5 s | 1 |
|
|
320
|
+
| 2 | ~3.0 s | 2 |
|
|
321
|
+
| 3 | ~4.6 s | 3 |
|
|
322
|
+
| 5 | ~7.5 s | 5 |
|
|
323
|
+
|
|
324
|
+
- 成本只落在**回合边界**一次(`agent/turn-stopping`),不落在工具调用上——`tools/post-execute` 中位稳定在 ~1 ms。
|
|
325
|
+
- 预算默认 12 s(`IMMUNE_DISTILL_BUDGET_MS`)。队列超过预算时,多出的任务留到下一次边界:实测 3000 ms 预算 + 6 个任务只蒸馏 2 个,其余 4 个顺延。
|
|
326
|
+
- 经验阈值:**每回合最多攒几条**「失败→成功」时才开模型。如果一个回合常常攒十几条,先跑离线规则蒸馏,或把 `IMMUNE_DISTILL_BUDGET_MS` 调小、让它跨多个回合摊平。
|
|
327
|
+
|
|
264
328
|
### 安全
|
|
265
329
|
|
|
266
330
|
- **脱敏**:写入磁盘前擦除 Bearer 令牌、`*_KEY=` 类赋值、URL 内嵌凭证、JWT。
|
|
@@ -274,19 +338,23 @@ node "<plugin>/dist/zcode/src/cli.js" doctor # 安装自检
|
|
|
274
338
|
| 检查 | 结果 |
|
|
275
339
|
| --- | --- |
|
|
276
340
|
| `npm run build` | exit 0 |
|
|
277
|
-
| `npm test` | **
|
|
341
|
+
| `npm test` | **90 条全绿**(34 条 core、7 条 ZCode hook、29 条 DSH 单元、4 条原生 UI(tab 契约)、3 条 MCP、3 条 LLM、5 条 UI 查询层、5 条 manifest 一致性(含 `dsh.client` 浏览器半契约与 shadow root 隔离)) |
|
|
278
342
|
| `scripts/verify-step1.mjs` | 15 断言(真实载荷判失败,含 bash 非零退出) |
|
|
279
343
|
| `scripts/verify-steps23.mjs` | 22 断言(失败→成功在回合结束时产出抗体;新会话注入 `<immune_memory>`) |
|
|
280
344
|
| `scripts/verify-live-host.mjs` | 20 断言(**真 Cordis 宿主**:真事件总线 + 真 SQLite + 跨会话注入 + 冷启动注入) |
|
|
281
|
-
| `scripts/verify-installed.mjs` |
|
|
345
|
+
| `scripts/verify-installed.mjs` | desktop / web 各 22 断言(bundles 登记、裸包名解析、patch 形状,以及浏览器半的 `dsh.client` 声明 + `exports["./client"]` + 装载器注册面) |
|
|
346
|
+
| `scripts/bench-tool-latency.mjs` | PASS,19 断言(慢速 mock 模型 1500 ms/次:`tools/post-execute` 中位 ~1 ms;`agent/turn-stopping` 按队列线性 1.5/3.0/4.6/7.5 s(1/2/3/5 条);3000 ms 预算下 6 条只蒸馏 2 条、其余顺延) |
|
|
282
347
|
|
|
283
348
|
### 已知限制
|
|
284
349
|
|
|
285
350
|
- **模型端点默认拒绝环回**:本地 Ollama 类服务需显式设 `IMMUNE_ALLOW_PRIVATE_ENDPOINT=1`。
|
|
286
351
|
这是防 SSRF 的代价,不是疏漏。
|
|
287
|
-
- **外部 LLM 蒸馏未实测**:规则蒸馏器全流程有测试;接真实模型端点的路径只有 mock
|
|
352
|
+
- **外部 LLM 蒸馏未实测**:规则蒸馏器全流程有测试;接真实模型端点的路径只有 mock 覆盖
|
|
353
|
+
(含批量路径的降级链)。
|
|
288
354
|
- **真实 agent 端到端未验**:验证期间模型额度返回 402,turn 到不了工具阶段。同一条链路改为
|
|
289
355
|
向安装副本投喂真实事件载荷验证(含真 Cordis 宿主的 20 条断言)。
|
|
356
|
+
- **`IMMUNE_GUARD=warn` 仅 ZCode 有效**:DSH 的 `PreToolDecision` 没有携带上下文的字段,
|
|
357
|
+
那边只能"拦或不拦"。
|
|
290
358
|
|
|
291
359
|
---
|
|
292
360
|
|
|
@@ -314,7 +382,7 @@ dsh plugin --profile <profile> add dsh-immune-system
|
|
|
314
382
|
|
|
315
383
|
```bash
|
|
316
384
|
cd dsh-immune-system && npm pack
|
|
317
|
-
dsh plugin --profile <profile> add ./dsh-immune-system
|
|
385
|
+
dsh plugin --profile <profile> add ./dsh-immune-system-<version>.tgz
|
|
318
386
|
dsh plugin --profile <profile> add github:meat-daemon-404/dsh-plugin-immune
|
|
319
387
|
```
|
|
320
388
|
|
|
@@ -340,7 +408,10 @@ exiting non-zero is *not* an error result: ZCode's `PostToolUseFailure` only fir
|
|
|
340
408
|
when a handler throws, and DSH documents that "non-zero exits are reported, not
|
|
341
409
|
errored" — `isError` covers infrastructure failures only. Both ports therefore
|
|
342
410
|
classify the result themselves (exit code, trailing `[exit code: N]` /
|
|
343
|
-
`[killed by signal: X]` / timed-out markers, and a narrow marker regex).
|
|
411
|
+
`[killed by signal: X]` / timed-out markers, and a narrow marker regex). A
|
|
412
|
+
structured `value` is authoritative: a non-zero exit code is a failure and a
|
|
413
|
+
zero one is a success even if the output mentions `Error:`; a payload object with
|
|
414
|
+
no exit status is a success and is not text-scanned at all.
|
|
344
415
|
|
|
345
416
|
**Anti-rot, relevance-guarded.** An injected antibody that still did not prevent
|
|
346
417
|
the failure loses credit and is retired after three such episodes. The penalty is
|
|
@@ -352,6 +423,110 @@ blamed for any Bash failure.
|
|
|
352
423
|
never raw tool output; brackets are stripped so a line cannot close its wrapper
|
|
353
424
|
tag; length-capped; labelled advisory.
|
|
354
425
|
|
|
426
|
+
**Distillation cost sits at the turn boundary.** Jobs are queued during tool
|
|
427
|
+
use and drained once at turn end under `IMMUNE_DISTILL_BUDGET_MS` (default
|
|
428
|
+
12 s). Measured with a slow mock model (`scripts/bench-tool-latency.mjs`):
|
|
429
|
+
`tools/post-execute` stays ~1 ms median, while `agent/turn-stopping` pays one
|
|
430
|
+
round trip per queued job (~1.5 s each at 1500 ms/call) and defers whatever the
|
|
431
|
+
budget cannot fit to the next boundary. With a model configured the drain now
|
|
432
|
+
**batches four episodes per request**, so a backlog fits in one budget window
|
|
433
|
+
instead of four.
|
|
434
|
+
|
|
435
|
+
**Failure judgement — four layers.** A tool result is classified in this order,
|
|
436
|
+
and the first layer that yields a verdict wins:
|
|
437
|
+
|
|
438
|
+
| # | Signal | Verdict |
|
|
439
|
+
| --- | --- | --- |
|
|
440
|
+
| 1 | structured `value.exitCode` | non-zero = failure, zero = success — authoritative, output text is not scanned |
|
|
441
|
+
| 2 | `isError` / `error` payload | failure, and its message becomes the symptom |
|
|
442
|
+
| 3 | trailing markers (`[exit code: N]`, `[killed by signal: X]`, timed out) | failure when N≠0 |
|
|
443
|
+
| 4 | narrow marker regex over the body (`ENOENT`, `npm ERR!`, `Cannot find module`, …) | failure; a healthy log that merely mentions "error" does not match |
|
|
444
|
+
|
|
445
|
+
**Repeat suppression.** Asking three questions in a row about the same file
|
|
446
|
+
would otherwise inject the identical block three times and count three hits.
|
|
447
|
+
When the candidate set is unchanged and fewer than four turns have passed, the
|
|
448
|
+
injection is skipped and `hit_count` is left alone — that counter drives
|
|
449
|
+
promotion and anti-rot, so inflating it would corrupt the library's own
|
|
450
|
+
statistics. `resume` / `clear` / `compact` reset the state.
|
|
451
|
+
|
|
452
|
+
**Target-scoped pairing.** A failure only pairs with a success that is the
|
|
453
|
+
*same kind of work*: same file for file tools, same program family for commands
|
|
454
|
+
(`npm`/`npx`/`pnpm` are one family — a build failing under `npm` and passing
|
|
455
|
+
under `npx` is the same task's fix; `npm …` then `git …` is not). Read-only
|
|
456
|
+
probes (`ls`, `pwd`, `cat`, `git status`, `git log`, `git diff`, `which`, …)
|
|
457
|
+
never close an episode: they cannot have fixed anything, and they sit between
|
|
458
|
+
an error and its real fix constantly.
|
|
459
|
+
|
|
460
|
+
**Jaccard near-duplicate merge.** Before inserting, prescriptions within the
|
|
461
|
+
same trigger are compared with character-3-gram Jaccard (blended with a
|
|
462
|
+
character-set Jaccard for short CJK sentences, where inserting one character
|
|
463
|
+
shifts every trigram). At ≥0.70 the rule merges into the existing row and its
|
|
464
|
+
hit count increments; a human correction or a model rewrite then overwrites the
|
|
465
|
+
stored wording, because it outranks whatever was there.
|
|
466
|
+
|
|
467
|
+
**Path-affinity rerank.** Retrieval fetches twice the requested limit, then
|
|
468
|
+
re-ranks in memory: ×1.5 when the trigger names a file or directory the prompt
|
|
469
|
+
mentions, ×0.85 for shared-library rules (project rules always win ties), and a
|
|
470
|
+
time decay of `1 / (1 + 0.02 × days)` so a rule nobody has needed for a year
|
|
471
|
+
does not outrank a fresh one.
|
|
472
|
+
|
|
473
|
+
### Dashboard
|
|
474
|
+
|
|
475
|
+
`cli.js ui` serves a local page at `http://127.0.0.1:7788/`: search, retire /
|
|
476
|
+
restore / delete, project and shared tabs, live counts. Browsing is a
|
|
477
|
+
read-only path — it never counts as an injection.
|
|
478
|
+
|
|
479
|
+
Boundaries are deliberately narrow: binds `127.0.0.1` only, a random per-run
|
|
480
|
+
token is required on every `/api` call (a cross-origin simple POST needs no
|
|
481
|
+
preflight, so Origin checking alone would not do), strict CSP, and the page
|
|
482
|
+
loads nothing external.
|
|
483
|
+
|
|
484
|
+
### Native UI (dsh.client)
|
|
485
|
+
|
|
486
|
+
The DSH web app gets a real settings tab instead of a separate page. The client
|
|
487
|
+
is `dsh-immune-system/client/client.js`, hand-written against the module-loader
|
|
488
|
+
contract (`window.__ModuleLoader__.load({ id, factory })`) — no bundler, no JSX,
|
|
489
|
+
no new dependency. Data comes from a host route registered as an *optional*
|
|
490
|
+
`webServer` dependency, so CLI and desktop profiles still work without it.
|
|
491
|
+
Mutations require same-origin JSON, which forces a CORS preflight.
|
|
492
|
+
|
|
493
|
+
### Sync & Doctor
|
|
494
|
+
|
|
495
|
+
Library snapshots are plain tagged JSON, and merging is conservative:
|
|
496
|
+
prescription is the identity, tallies take the MAX (two machines count
|
|
497
|
+
independently and must not multiply), status is active when either side says
|
|
498
|
+
so, and malformed rows are counted invalid rather than aborting the import.
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
node "<plugin>/dist/zcode/src/cli.js" export-json library.json
|
|
502
|
+
node "<plugin>/dist/zcode/src/cli.js" import-json library.json
|
|
503
|
+
node "<plugin>/dist/zcode/src/cli.js" doctor
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`doctor` reports node version, an FTS5 trigram probe, database and shared-library
|
|
507
|
+
readability, the plugin marker, the MCP script, the native-UI contract
|
|
508
|
+
(`dsh.client` + `exports["./client"]` + a loader-registered bundle) and counts.
|
|
509
|
+
Any failure exits non-zero, so it can gate a script.
|
|
510
|
+
|
|
511
|
+
### Guard (IMMUNE_GUARD)
|
|
512
|
+
|
|
513
|
+
Advisory text can be dropped by a model in a long turn, so a short curated list
|
|
514
|
+
of calls that are wrong in essentially every environment also gets a
|
|
515
|
+
deterministic pre-execution check: force-push to main, `rm -rf /`,
|
|
516
|
+
`git reset --hard`.
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
IMMUNE_GUARD=off # default: no check
|
|
520
|
+
IMMUNE_GUARD=warn # louder advisory (ZCode only)
|
|
521
|
+
IMMUNE_GUARD=deny # refuse the call (both hosts)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Patterns live in code, never in the database: a distilled rule is model output,
|
|
525
|
+
and compiling it into a regex that can block tool calls would hand an
|
|
526
|
+
unreviewed string the power to stop work, plus ReDoS risk. `deny` is never the
|
|
527
|
+
default. `warn` is ZCode-only because DSH's `PreToolDecision` carries no
|
|
528
|
+
context channel.
|
|
529
|
+
|
|
355
530
|
### Layout
|
|
356
531
|
|
|
357
532
|
```
|
|
@@ -365,7 +540,7 @@ dsh/ DSH adapter — verified against the real host contract
|
|
|
365
540
|
dsh-immune-system/ installable bundle (manifest, patch, commands, dist)
|
|
366
541
|
zcode/ ZCode adapter (hooks, CLI)
|
|
367
542
|
docs/ dsh-host-contract.md — every contract claim with file:line
|
|
368
|
-
scripts/ the
|
|
543
|
+
scripts/ the five verification & benchmark scripts above
|
|
369
544
|
test/ node:test
|
|
370
545
|
```
|
|
371
546
|
|