@moonquake2004/dsh-doctor 0.8.1 → 0.8.2
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/CHANGELOG.md +29 -0
- package/dsh-doctor.mjs +43 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,35 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [0.8.2] — 2026-09-16
|
|
20
|
+
|
|
21
|
+
**红队审计(fresh-context 证伪者)在"已自审三轮"的版本里找出 6 个真漏洞**,其中 2 个推翻了我刚声称"已验证"的断言。
|
|
22
|
+
完整档案:`docs/redteam-2026-09-16.md`;未修项见 `docs/check-authoring-rules.md` §5。
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **目录检查把"跳过"报成 pass,并伪造覆盖量**(最严重):`runCatalogCheck` 自报 `skipped` 被记成 `ok:true`;
|
|
27
|
+
`report()` 的 `examined` 回退到段级占位值 1,零对象永不降级。实测 `✓ 未发现 DSH 环境…跳过 dsh 的 PATH 检查`;
|
|
28
|
+
profile 目录**整个不存在**时 P6 仍 pass。
|
|
29
|
+
- 自报 `skipped` → `reportSkip`;探测原语的"目标不存在"分支补 `skipped: true`(根因是**分支里没设标记、文案却写着"跳过"**);
|
|
30
|
+
保留"文案含跳过 ⇒ skip"作兜底。空环境下被伪造的 `verified` 由 11 降到 8。
|
|
31
|
+
- **闭集检测有盲点**:`⚠` 也是判定符号却裸写在收口点外,而闭集正则只有 `/[✓⊖✗]/`。纳入 `⚠` 后**立刻又暴露 3 处**,已全部收口;
|
|
32
|
+
判定符号正则集中为 `VERDICT_GLYPH_RE`;**收口处剥掉数据里的判定符号**(远程目录数据不得伪造判定上屏)。
|
|
33
|
+
- **`reportSkip` 缺 `phase`** → 补上("单位属于求值阶段"这条断言此前在 skip 上无法核对)。
|
|
34
|
+
|
|
35
|
+
### Changed — 契约级决策,**未擅改**
|
|
36
|
+
|
|
37
|
+
- **退出码随输出格式分叉**(`--json` 1 / `--envelope` 2 / 文本 1):契约 `doctor-contract.md:32` 规定
|
|
38
|
+
`0 全通过 / 1 任何 WARN / 2 任何 FAIL` → **envelope 是对的一边,主路径违反契约**。
|
|
39
|
+
我一度把 envelope 改成 1(**统一到违反契约的一边**),已回滚;分歧现由测试钉住并列为待决策 D1。
|
|
40
|
+
|
|
41
|
+
### Infra
|
|
42
|
+
|
|
43
|
+
- `scripts/audit.sh` 的畸形-manifest 断言由 `n > 1` 收紧为 `n >= 15`,并把当前不达标**如实计为"已知缺口"**(自证太弱是 R8 的实例)。
|
|
44
|
+
- 新增 `docs/redteam-2026-09-16.md`(逐条反例 + 处置 + 红队未能验证项)。
|
|
45
|
+
|
|
46
|
+
测试 141 → 144。
|
|
47
|
+
|
|
19
48
|
## [0.8.1] — 2026-09-16
|
|
20
49
|
|
|
21
50
|
### 0.8.0 的回归审计 —— **修一个引入一个,被查出来了四个**
|
package/dsh-doctor.mjs
CHANGED
|
@@ -264,10 +264,19 @@ function report(section, id, ok, detail, fix, src, examined) {
|
|
|
264
264
|
}
|
|
265
265
|
const zeroCheck = typeof examined === 'number' ? examined : coverageNow()?.n;
|
|
266
266
|
if (ok === true && zeroCheck === 0) {
|
|
267
|
-
|
|
267
|
+
// 2026-09 语料发现的措辞缺陷:原实现把原判定文案原样留下 → 出现"结构正常(无可检查对象,未做任何比较)"
|
|
268
|
+
// 这种**自相矛盾**的句子(既宣称正常、又说没比较)。原文案改放 note(机器可读),人读文案只陈述事实。
|
|
269
|
+
results.push({ section, id, ok: true, skip: true, coverage: 'none',
|
|
270
|
+
// 措辞要求:**不得自相矛盾**(2026-09 语料曾产出"结构正常(无可检查对象,未做任何比较)"),
|
|
271
|
+
// 同时保留具体原因(否则用户与测试都只能看到一句通用话)。
|
|
272
|
+
detail: `无可检查对象、未做任何比较——不作为「通过」;原判定:${String(detail).replace(VERDICT_GLYPH_RE, '·')}`,
|
|
273
|
+
fix, src: src ?? 'builtin' });
|
|
268
274
|
return;
|
|
269
275
|
}
|
|
270
|
-
|
|
276
|
+
// R6(红队):detail 可来自远程目录/checks.local.json,**数据不得能伪造判定符号**上屏——
|
|
277
|
+
// 源码里没有 ✓ 也能让 stdout 出现 ✓。故在收口处剥掉数据里的判定符号(含 ⚠)。
|
|
278
|
+
const safeDetail = String(detail).replace(VERDICT_GLYPH_RE, '·');
|
|
279
|
+
const rec = { section, id, ok, detail: safeDetail, fix, src: src ?? 'builtin', phase: currentPhase };
|
|
271
280
|
if (ok === true) {
|
|
272
281
|
const ctx = coverageNow();
|
|
273
282
|
const n = typeof examined === 'number' ? examined : ctx?.n;
|
|
@@ -279,7 +288,8 @@ function report(section, id, ok, detail, fix, src, examined) {
|
|
|
279
288
|
|
|
280
289
|
/** skip 状态(v1 词汇表 r5:#1719)——"不适用"而非"通过",必须带 reason(detail)。不计入 pass/fail,不翻退出码。 */
|
|
281
290
|
function reportSkip(section, id, detail, src) {
|
|
282
|
-
|
|
291
|
+
// R2(红队):skip 记录此前**没有 phase 字段**,于是"单位属于求值阶段"这条断言在 skip 上无法核对。
|
|
292
|
+
results.push({ section, id, ok: true, skip: true, detail, src: src ?? 'builtin', phase: currentPhase });
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
|
|
@@ -298,6 +308,7 @@ function reportSkip(section, id, detail, src) {
|
|
|
298
308
|
* @param {{checked?: number, detail?: string, reason?: string, totals?: {verified:number, skipped:number, failed:number}}} opts
|
|
299
309
|
*/
|
|
300
310
|
function makeVerdict(state, opts = {}) {
|
|
311
|
+
if (!['pass', 'fail', 'skip', 'warn', 'action'].includes(state)) throw new Error(`makeVerdict: 未知状态 ${state}`);
|
|
301
312
|
if (state === 'pass') {
|
|
302
313
|
const n = opts.checked;
|
|
303
314
|
if (typeof n !== 'number' || n <= 0) {
|
|
@@ -313,6 +324,9 @@ function makeVerdict(state, opts = {}) {
|
|
|
313
324
|
* 两者构成本工具的**闭集**——闭集测试断言:源码里任何 ✓/⊖/✗ 都必须落在这两个函数体内。
|
|
314
325
|
*/
|
|
315
326
|
const VERDICT_MARKS = { pass: '✓', fail: '✗', skip: '⊖', warn: '⚠', action: '✓' };
|
|
327
|
+
/** 判定符号集合(含 ⚠)——**唯一**用于"从数据里剥掉符号"的正则。
|
|
328
|
+
* 红队 R6:数据(远程目录/checks.local.json)不得能伪造判定符号上屏;符号只许收口点产出。 */
|
|
329
|
+
const VERDICT_GLYPH_RE = /[✓✔⊖✗✘⚠]/g;
|
|
316
330
|
|
|
317
331
|
function printItem(state, text, indent = 2) {
|
|
318
332
|
console.log(`${' '.repeat(indent)}${VERDICT_MARKS[state] ?? '?'} ${text}`);
|
|
@@ -655,7 +669,7 @@ function checkPort3080() {
|
|
|
655
669
|
`端口 ${port} 被 dsh web 实例占用(PID ${info.pid})${httpAlive === true ? '且 HTTP 有应答' : ''}——宿主自身或另一实例,正常`, undefined);
|
|
656
670
|
}
|
|
657
671
|
else if (info) report('env', 'E10-port-3080', false, `端口 ${port} 被其他程序占用(PID ${info.pid}: ${info.cmd}),dsh web 启动会 address in use(#1719)`, `关掉占用进程,或让 dsh web 用别的端口`);
|
|
658
|
-
else report('env', 'E10-port-3080', true,
|
|
672
|
+
else report('env', 'E10-port-3080', true, `端口 ${port} 被占用但无法识别占用者`, undefined);
|
|
659
673
|
} else {
|
|
660
674
|
report('env', 'E10-port-3080', false, `端口 ${port} 探测异常: ${e.message.slice(0, 60)}`, undefined);
|
|
661
675
|
}
|
|
@@ -2101,7 +2115,7 @@ function scanAllSessions() {
|
|
|
2101
2115
|
if (unstableReads.length) parts.push(`${unstableReads.length} 个会话**读取间歇性失败**(重试即成功 → 非损坏,指向存储/内存/驱动的偶发读错误,社区 #6739): ${unstableReads.slice(0, 3).map((u) => `${u.id}(${u.attempts} 次尝试)`).join(' | ')}`);
|
|
2102
2116
|
if (oversized.length) parts.push(`${oversized.length} 个超大会话: ${oversized.map((o) => `${o.id}(${o.dsMB}MB/${o.events}事件)`).join(' | ')}`);
|
|
2103
2117
|
if (totalRisk) parts.push(`工作区估算物化堆 ~${estHeapMB}MB(估算= max(${totalEvents}事件×600B, ${totalMB}MB×6),跨 ${files.length} 会话累积,#1550 场景;阈值 ${heapLimit}MB,可设 DSH_DOCTOR_HEAP_MB)`);
|
|
2104
|
-
report('session', 'S11', true,
|
|
2118
|
+
report('session', 'S11', true, `全会话扫描:${parts.join(';')}(未损坏,可接受或归档)`,
|
|
2105
2119
|
unstableReads.length
|
|
2106
2120
|
? '间歇性读取失败不是日志问题:先重试读取;持续出现则排查存储健康(SMART)、内存与磁盘驱动(#6739 的证据是失败帧位置每次不同)'
|
|
2107
2121
|
: '冷启动会明显变慢;必要时压缩/归档历史会话', undefined, files.length);
|
|
@@ -2274,13 +2288,13 @@ export function runCatalogCheck(check, ctx) {
|
|
|
2274
2288
|
let ok = existsSync(fp);
|
|
2275
2289
|
if (ok && probe.type === 'path-is-dir') ok = lstatSync(fp).isDirectory();
|
|
2276
2290
|
if (ok && probe.type === 'path-is-file') ok = lstatSync(fp).isFile();
|
|
2277
|
-
return ok ? { ok: true, detail: check.detailOk ?? `${fp} 存在` }
|
|
2291
|
+
return ok ? { ok: true, checked: 1, detail: check.detailOk ?? `${fp} 存在` }
|
|
2278
2292
|
: { ok: false, detail: check.detailFail ?? `${fp} 不存在/类型不符` };
|
|
2279
2293
|
}
|
|
2280
2294
|
case 'json-valid': {
|
|
2281
2295
|
const fp = p(probe.path);
|
|
2282
2296
|
if (!existsSync(fp)) return probe.required === false
|
|
2283
|
-
? { ok: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
|
|
2297
|
+
? { ok: true, skipped: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
|
|
2284
2298
|
: { ok: false, detail: check.detailFail ?? `${fp} 缺失` };
|
|
2285
2299
|
let utf8ok = true, jsonok = false;
|
|
2286
2300
|
try { new TextDecoder('utf-8', { fatal: true }).decode(readFileSync(fp)); } catch { utf8ok = false; }
|
|
@@ -2292,7 +2306,7 @@ export function runCatalogCheck(check, ctx) {
|
|
|
2292
2306
|
case 'text-not-contains': {
|
|
2293
2307
|
const fp = p(probe.path);
|
|
2294
2308
|
if (!existsSync(fp)) return probe.required === false
|
|
2295
|
-
? { ok: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
|
|
2309
|
+
? { ok: true, skipped: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
|
|
2296
2310
|
: { ok: false, detail: check.detailFail ?? `${fp} 缺失` };
|
|
2297
2311
|
let re;
|
|
2298
2312
|
try { re = new RegExp(probe.pattern, probe.flags ?? ''); } catch (e) { return { ok: false, detail: `目录规则正则非法: ${e.message.slice(0, 60)}` }; }
|
|
@@ -2323,7 +2337,7 @@ export function runCatalogCheck(check, ctx) {
|
|
|
2323
2337
|
case 'file-writable': {
|
|
2324
2338
|
const fp = p(probe.path);
|
|
2325
2339
|
if (!existsSync(fp)) return probe.required === false
|
|
2326
|
-
? { ok: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
|
|
2340
|
+
? { ok: true, skipped: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
|
|
2327
2341
|
: { ok: false, detail: check.detailFail ?? `${fp} 缺失` };
|
|
2328
2342
|
let writable = false;
|
|
2329
2343
|
try { const fd = openSync(fp, 'a'); closeSync(fd); writable = true; } catch { /* 只读/属主问题 */ }
|
|
@@ -2350,7 +2364,14 @@ function checkCatalog(ctx, catalog) {
|
|
|
2350
2364
|
if (check.section === 'profile' && !ctx.profileDir) continue; // profile 无效时跳过 profile 段
|
|
2351
2365
|
let r;
|
|
2352
2366
|
try { r = runCatalogCheck(check, ctx); } catch (e) { r = { ok: false, detail: `catalog 检查异常: ${e.message.slice(0, 80)}` }; }
|
|
2353
|
-
|
|
2367
|
+
// 红队 R1(2026-09-16):`runCatalogCheck` 自报 `skipped`(原语不支持 / 无 DSH 环境 / 目标文件不存在)
|
|
2368
|
+
// 曾被当成 `ok:true`(pass)—— 于是出现"✓ 未发现 DSH 环境…跳过 dsh 的 PATH 检查"这种自相矛盾的行,
|
|
2369
|
+
// 而且它继承目录段的占位覆盖量 1,零对象永不降级。自报 skipped 就是 skip,覆盖量为 0。
|
|
2370
|
+
// 归一化(红队 R1 的第二半):探测原语里有多处文案写着"…不存在(跳过)"却返回 `ok: true`,
|
|
2371
|
+
// 于是"没有目标可查"被记成**通过**(实测:无 profile 目录时 P6 仍 pass)。判据用探测自己的话:
|
|
2372
|
+
// **凡自述跳过的一律是 skip** —— 这类"文案与状态不一致"只能从结构上消灭,不能靠逐个改分支。
|
|
2373
|
+
if (!r.skipped && r.ok === true && /跳过|不适用/.test(String(r.detail))) r.skipped = true;
|
|
2374
|
+
if (r.skipped) { reportSkip(check.section, check.id, r.detail, 'catalog'); continue; }
|
|
2354
2375
|
const severity = check.severity ?? 'error';
|
|
2355
2376
|
catalogSeverity.set(check.id, severity);
|
|
2356
2377
|
report(check.section, check.id, r.ok, r.detail, r.ok ? undefined : check.fix, 'catalog');
|
|
@@ -3039,7 +3060,7 @@ async function run() {
|
|
|
3039
3060
|
if (jsonOut) console.log(JSON.stringify(r, null, 2));
|
|
3040
3061
|
else if (r.ok && r.stage === 'verified') printVerdict(makeVerdict('pass', { checked: r.checked, detail: `已安装并预检通过(${r.checked} 条 entry 均可导入)——重启 dsh 即可` }));
|
|
3041
3062
|
else if (r.ok && r.stage === 'quarantined') {
|
|
3042
|
-
|
|
3063
|
+
printVerdict(makeVerdict('warn', { detail: '已安装,但该插件的 entry 导入失败,已自动隔离以避免 dsh 起不来:' }));
|
|
3043
3064
|
for (const f of r.failures || []) console.log(` ${f}`);
|
|
3044
3065
|
console.log(` 隔离项:${r.quarantined.join(', ')}(用 --unquarantine <包名> 放回,修好版本后再重试)`);
|
|
3045
3066
|
console.log(` dsh 现在可以正常启动。`);
|
|
@@ -3155,6 +3176,13 @@ async function run() {
|
|
|
3155
3176
|
try { await checkPort3080(); } catch (e) { report('env', 'E10-port-3080', false, `端口检查异常: ${e.message.slice(0, 60)}`); }
|
|
3156
3177
|
try { checkProfile(profileArg); } catch (e) { report('profile', 'P0', false, `profile 检查异常: ${e.message.slice(0, 100)}`); }
|
|
3157
3178
|
try { checkSession(sessionArg); } catch (e) { report('session', 'S0', false, `session 检查异常: ${e.message.slice(0, 100)}`); }
|
|
3179
|
+
// 段级覆盖(2026-09 语料发现):整段没有产出任何检查时,报告里会出现"整段消失"——
|
|
3180
|
+
// 用户看不出"会话检查根本没跑"。R3 的精神在**段**这一级同样成立:没看就必须说出来。
|
|
3181
|
+
for (const [sec, why] of [['session', '未选中任何会话(活跃会话在保护窗口内,或被 --session 过滤)']]) {
|
|
3182
|
+
if (wants(sec) && !results.some((r) => r.section === sec)) {
|
|
3183
|
+
reportSkip(sec, 'S0', `${why}——本段检查未运行,**这不代表通过**`);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3158
3186
|
try { scanAllSessions(); } catch (e) { report('session', 'S11', false, `全会话扫描异常: ${e.message.slice(0, 100)}`); }
|
|
3159
3187
|
try { scanMigrationRefusals(); } catch (e) { report('session', 'S12', false, `迁移拒载预检异常: ${e.message.slice(0, 100)}`); }
|
|
3160
3188
|
}
|
|
@@ -3286,6 +3314,9 @@ async function run() {
|
|
|
3286
3314
|
if (s === 'fail') baseFail++;
|
|
3287
3315
|
else if (s === 'warn') baseWarn++;
|
|
3288
3316
|
}
|
|
3317
|
+
// 契约(docs/doctor-contract.md:32):0 = 全通过 · **1 = 任何 WARN** · **2 = 任何 FAIL**。
|
|
3318
|
+
// 红队 R3 指出 envelope 与 json/文本路径退出码不一致——**方向是 json/文本错**,不是这里错。
|
|
3319
|
+
// 我一度把它改成 1,等于把一致性统一到违反契约的一边;已回滚。见 §"待决策"。
|
|
3289
3320
|
const baseExit = baseFail > 0 ? 2 : baseWarn > 0 ? 1 : 0;
|
|
3290
3321
|
const exitCode = Math.max(baseExit, secExit);
|
|
3291
3322
|
// v1.1 remediation(#1719 ADOPTED:ciceroyang 提名、两位 reviewer +1):opt-in --remediation,
|
|
@@ -3342,7 +3373,7 @@ async function run() {
|
|
|
3342
3373
|
if (!r.ok && r.fix) console.log(` ↳ 修复: ${r.fix}`);
|
|
3343
3374
|
}
|
|
3344
3375
|
if (updateInfo.available && !updateInfo.applied) {
|
|
3345
|
-
console.log(
|
|
3376
|
+
console.log(''); printVerdict(makeVerdict('warn', { detail: `新版本 ${updateInfo.latest} 可用(当前 ${updateInfo.current})→ 运行 \`dsh-doctor --update\` 或 \`dsh plugin update\`` }));
|
|
3346
3377
|
} else if (updateInfo.applied) {
|
|
3347
3378
|
console.log(''); printVerdict(makeVerdict('action', { detail: updateInfo.applied }));
|
|
3348
3379
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Offline diagnostic for DeepSeek Harness — 28 built-in + 5 catalog checks across env/profile/session (Layer A checks-as-data), self-update (Layer B), and a semi-automatic LLM observer (Layer C, --observe); Doctor panel in web UI settings.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|