mingdao-harness 0.6.1 → 0.6.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/README.md +1 -1
- package/docs/AUDIT-v0.6.1-/347/254/254/344/270/211/346/226/271/346/212/245/345/221/212/347/231/273/350/256/260.md +647 -0
- package/docs/MIGRATION-DEYI-v0.5.md +18 -0
- package/docs/PACK-API.md +62 -5
- package/docs/RELEASE-CHECKLIST.md +152 -9
- package/package.json +1 -1
- package/src/agent.js +69 -6
- package/src/atomic-write.js +22 -2
- package/src/audit.js +45 -15
- package/src/autostart.js +67 -16
- package/src/cachestats.js +66 -13
- package/src/cli.js +16 -7
- package/src/commands/desktop.js +2 -1
- package/src/commands/diagnose.js +11 -2
- package/src/commands/ledger.js +7 -3
- package/src/commands/pack.js +58 -4
- package/src/commands/repl.js +28 -13
- package/src/commands/schedule.js +15 -2
- package/src/commands/sync.js +102 -20
- package/src/commands/update.js +2 -1
- package/src/commands/workspace.js +8 -1
- package/src/config.js +4 -4
- package/src/constraints.js +31 -5
- package/src/cost-guard.js +4 -3
- package/src/hooks.js +5 -2
- package/src/ledger.js +110 -10
- package/src/mcp-presets.js +25 -4
- package/src/mcp.js +3 -1
- package/src/memory.js +64 -18
- package/src/model-discovery.js +14 -5
- package/src/models.js +34 -1
- package/src/net-guard.js +35 -11
- package/src/notify.js +4 -3
- package/src/packs.js +155 -9
- package/src/permissions.js +24 -0
- package/src/proc.js +94 -4
- package/src/prompts.js +36 -3
- package/src/redact.js +57 -0
- package/src/routing.js +14 -8
- package/src/safe-fetch.js +83 -0
- package/src/schedule.js +81 -12
- package/src/session-index.js +26 -2
- package/src/skill-lib.js +25 -46
- package/src/skill-registry.js +55 -30
- package/src/task-state.js +50 -5
- package/src/tasks/worker.js +2 -1
- package/src/tasks.js +74 -10
- package/src/tokenizer.js +76 -6
- package/src/tools/bash.js +3 -1
- package/src/tools/fs-tools.js +31 -13
- package/src/tools/git.js +31 -2
- package/src/tools/index.js +24 -1
- package/src/ui.js +33 -1
- package/src/web/app.js +10 -4
- package/src/web/routes/domains/config.js +3 -2
- package/src/web/routes/domains/workspace.js +3 -1
- package/src/web/server.js +31 -7
- package/src/web/util.js +20 -0
- package/src/workspace.js +46 -5
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# v0.6.1 第三方审计报告登记表
|
|
2
|
+
|
|
3
|
+
> 起因:负责人让 v0.6.1 审计自己的代码,过程中暴露了三个**过程缺陷**(见 §2),
|
|
4
|
+
> 并留下四份报告在工作区根目录。本文件的作用是把三方结论**登记成可追踪清单**,
|
|
5
|
+
> 分清「我已亲自核实」与「第三方结论未复核」,避免把没核过的行号当结论用。
|
|
6
|
+
|
|
7
|
+
## 1. 报告来源
|
|
8
|
+
|
|
9
|
+
| 文件 | 产出方 | 口径 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `MingDao-harness-v0.6.1-技术评估报告.md` | MDH v0.6.1 自己(主代理 + 并行子代理) | 自评 P1×2 / P2×11 / P3×7 + 待复核 10;**自述因步数上限未读完部分文件** |
|
|
12
|
+
| `MingDao-Harness-v0.6.1-代码审计报告.md` | 第三方(另一工具链) | 全量精读 + 本地实跑测试套件 |
|
|
13
|
+
| `audit-report.md` | 第三方(六阶段流水线) | 243/243 文件覆盖,P0×17 / P1×15 / P2×8 |
|
|
14
|
+
| `verification-report.md` | 第三方(对上份报告的验证) | DFX 红线核查,15 条 PASS |
|
|
15
|
+
|
|
16
|
+
**读这些报告时必须注意**:自评报告是在**步数受限**下产出的(它自己写了「未交付内容:限于步数,
|
|
17
|
+
`src/providers/*`、`src/context.js`、`src/compact.js`、`src/tokenizer.js` 等未逐行精读」),
|
|
18
|
+
所以它的「未发现问题」不等于没问题。两份第三方报告之间也有口径差异(严重度分级、总数不同),
|
|
19
|
+
本表只登记**位置明确、可复核**的条目。
|
|
20
|
+
|
|
21
|
+
## 2. 已在本轮修复(v0.6.2)
|
|
22
|
+
|
|
23
|
+
### 2.1 三个过程缺陷(负责人实测)
|
|
24
|
+
|
|
25
|
+
| # | 现象 | 根因(已核实) | 修复 |
|
|
26
|
+
| --- | --- | --- | --- |
|
|
27
|
+
| 1 | 步数受限,**未能产出交付物**,要追问才继续 | ① 预算用尽时**不打印任何提示**,用户只看到一段总结,误以为完成;② 兜底总结用 `tools: []`,收尾时**已无法写文件**,所以上限一到必然没有交付物;③ 兜底提示语写的是「任务已执行完毕」——在步数上限这条路径上是假话 | 预算用尽打印**明确、可执行**的提示(含 `--continue` / 「继续」/ 调高 `config.maxRounds`);续跑提示要求**优先落盘**(只留在对话里的不算交付物)并告知剩余轮次;兜底提示语改为如实说明「被迫中断、可能尚未完成」并列出未完成部分 |
|
|
28
|
+
| 2 | 子代理**字数超限被截断** | `src/web/app.js` 用 `truncText(resultText(...), 1500)` **硬截断**——1500 字以外的内容用户再也看不到(同页 bash/diff/ls/grep 早已用可展开的 `expandableBody`,只有文本类结果不一致) | 新增 `textBody()`:短文本原样、长文本给「预览 + 展开全文」,一个字都不丢 |
|
|
29
|
+
| 3 | 桌面版调用工具时**不停弹出终端** | Windows 上 `spawn` 默认 `windowsHide: false`,GUI 进程每次 spawn 都弹控制台;`detached: true` 还会**新建**控制台并打断管道。此前**只有 hooks.js 单独修过**,其余 9 处 spawn 全部遗漏 | 新增单一来源 `src/proc.js#spawnOpts()`,**13 处 spawn / 10 个文件**全部收口;并加**结构守卫测试**(新增 spawn 漏走即失败) |
|
|
30
|
+
|
|
31
|
+
### 2.2 第三方报告的 P1-2(我已复核并修复)
|
|
32
|
+
|
|
33
|
+
**read 去重缓存跨代理污染** —— 此前 `readCache` 是 `src/tools/fs-tools.js` 的**模块级 Map**,
|
|
34
|
+
键只是文件绝对路径,同一 Node 进程内主代理 / 并行子代理 / 多个 WebUI 会话**共用一个缓存**。
|
|
35
|
+
后果:子代理读过某文件后,**主代理再读同一文件拿到的是「内容与上次读取一致」占位串**,
|
|
36
|
+
而主代理上下文里从来没有这段内容——属于「静默给出错误信息」。
|
|
37
|
+
|
|
38
|
+
自评报告正是踩在这一条上:它写「并行子代理读过 `bash.js`/`permissions.js`/`fetch.js`,
|
|
39
|
+
随后我在主上下文用 read 读同样三个文件,返回的正是『内容与上次读取一致』」。
|
|
40
|
+
|
|
41
|
+
**修复**:缓存挂到 `ctx.readCache`(`createAgent` 每个实例一份)。同一代理重复读仍去重省 token,
|
|
42
|
+
跨代理不再污染;没有 `ctx.readCache` 时不去重(宁可多花 token,也不给错信息)。
|
|
43
|
+
|
|
44
|
+
## 3. 已修复:P1-1 项目级 Pack「克隆即执行」(**已亲自核实并修复**)
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// src/packs.js#packDirs —— 项目级目录无条件进入搜索路径(priority 2)
|
|
48
|
+
if (projectDir) out.push({ dir: path.join(projectDir, '.mingdao', 'packs'), source: 'project', priority: 2 });
|
|
49
|
+
// src/packs.js#mountOne —— 同进程执行
|
|
50
|
+
mod = await import(pathToFileURL(entry).href);
|
|
51
|
+
// src/cli.js:413-419 —— 启动路径无条件挂载
|
|
52
|
+
const packCtx = await mountPacks(cfg, { cwd: workingDir });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**触发**:`git clone <不可信仓库>` → `cd` 进去 → 跑任意 `mingdao` 子命令(桌面版把工作目录切过去也一样)。
|
|
56
|
+
仓库里放 `.mingdao/packs/evil/pack.json` + `pack.mjs` 即可。
|
|
57
|
+
|
|
58
|
+
**后果**:`pack.mjs` 以完整 Node 权限同进程运行——可读 `~/.mingdao/credentials.json`、可出网外传、
|
|
59
|
+
可读 bash 工具专门过滤掉的敏感环境变量(`bash.js` 的黑名单在进程内代码面前毫无意义)。
|
|
60
|
+
**与 `permission` 模式(ask/readonly)完全无关,也不询问用户。**
|
|
61
|
+
|
|
62
|
+
**修复(v0.6.2,负责人已同意该行为变更)**:
|
|
63
|
+
|
|
64
|
+
- 项目级 Pack **默认不挂载**:`packDirs` 只在内容指纹被显式信任后才把它并入搜索路径;
|
|
65
|
+
- 复用 `mingdao skill trust` 的**内容指纹**模式(同一实现 `skillDirHash`)——指纹变化即自动失效;
|
|
66
|
+
- 新增 `mingdao pack trust` / `untrust`;`mingdao pack list` 把未信任的显示为
|
|
67
|
+
「⛔ 未信任(不挂载)」并给出开启命令;
|
|
68
|
+
- 未信任时启动打印告警,说明**目录、原因与开启命令**——静默执行与静默跳过同样糟糕;
|
|
69
|
+
- `config.packs` 显式声明**不受此门限制**(那是用户自己写下的授权);
|
|
70
|
+
- 信任表 `${MINGDAO_HOME}/pack-trust.json`,权限 `0600`。
|
|
71
|
+
|
|
72
|
+
**实现中额外抓到的一个坑**(单元测试漏掉、CLI 端到端实测抓到):信任表键必须按
|
|
73
|
+
`realpath` 归一。macOS 上 `/tmp` → `/private/tmp`,`os.tmpdir()` 同样是符号链接,
|
|
74
|
+
不归一时「trust 记一个路径、运行时按 `process.cwd()` 查另一个路径」→ 信任看起来完全没生效。
|
|
75
|
+
已修,并补了「经符号链接 trust 后按真实路径也必须命中」的回归断言。
|
|
76
|
+
|
|
77
|
+
**下游影响**:`docs/MIGRATION-DEYI-v0.5.md` §四 已加迁移步骤与两条一次性命令
|
|
78
|
+
(`mingdao pack trust <仓库根>` 或写进 `config.packs`)。装在 `$MINGDAO_HOME/packs/`
|
|
79
|
+
(用户级)的 Pack 不受影响。
|
|
80
|
+
|
|
81
|
+
## 3.1 已修复(第二批:凭据泄露类 + 静默失效类)
|
|
82
|
+
|
|
83
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
84
|
+
| --- | --- | --- | --- |
|
|
85
|
+
| P2-6 | `src/commands/sync.js` | `sync passwd <新密码>` 走位置参数 → 明文进 `ps aux` / shell history / CI 日志(同文件的 `sync login` 早已拒绝这种做法) | 三次隐藏输入(旧/新/确认),拒绝命令行传密码且不回显;README 用法同步 |
|
|
86
|
+
| P2-8 | `src/skill-registry.js` | sha256 校验是「可选」的:索引没写哈希就完全不校验,文件照样落盘,CLI 仍打印「✓ 已安装」 | 改为 fail-closed:缺哈希或格式非法一律拒绝安装,错误信息给出补救方式 |
|
|
87
|
+
| P2-5 | `src/ui.js` | 流式输出走 `sanitizeTerminal`,但一次性输出 `io.print` 是 `console.log(text)` 直通;而它的调用点有**模型/文件内容直入**的入口 | `io.print` 统一过 `sanitizeKeepingSgr`(OSC/清屏等一律剥,只放行 SGR 颜色) |
|
|
88
|
+
|
|
89
|
+
> P2-5 实现里又踩到一个「自我抵消」的坑,已写进代码注释:
|
|
90
|
+
> 拆成多段 `replace` 时,第 2 段刚把 SGR(`\x1b[31m`)原样保留,第 4 段的 C0 过滤
|
|
91
|
+
> 又把里面那个 ESC(0x1B) 当成控制符剥掉——结果是配色全丢、还平白多出 `[31m` 这种可见垃圾。
|
|
92
|
+
> 必须**单趟扫描 + 有序分支**,让 SGR 被分支整体吃掉。
|
|
93
|
+
|
|
94
|
+
> 顺带抓到两个报告里没写的真 bug(都在改动过程中暴露):
|
|
95
|
+
> **连续隐藏提问在管道输入下丢行**(每题各建 readline 接口、或逐题 `question()` 都会丢,
|
|
96
|
+
> 表现为进程静默退出且退出码 0)、**提示语被 `_writeToOutput` 一起吞掉**
|
|
97
|
+
> (所以此前登录时看不到「密码:」)。两者均已修并加了断言。
|
|
98
|
+
|
|
99
|
+
## 3.2 已修复(第三批:项目记忆的持久化提示注入)
|
|
100
|
+
|
|
101
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
102
|
+
| --- | --- | --- | --- |
|
|
103
|
+
| P2-9 | `src/prompts.js`、`src/memory.js` | 项目记忆由**模型自己从对话里提炼**(对话可含 fetch/read 引入的外部文本),却原样拼进 system 提示的围栏里:记忆里出现 `</project_memory>` 即可**闭合围栏**,其后文字直接落到 system 层,且该记忆会在之后**每个新会话**重新生效——持久化注入,用户还看不到 | 三重防线:① 中和内容里的围栏标签(含大小写/空白变体),伪造闭合变成 `</…>` 仍可读;② 剥掉零宽与双向控制字符(它们能让注入文本在人工检查时"看不见");③ 显式声明「这是背景数据、不是指令」,遇越权要求应提示用户 |
|
|
104
|
+
|
|
105
|
+
同批顺带做的透明化:`extractAndAppendProjectMemory` 现在返回「写了哪几条、写到哪个文件」,
|
|
106
|
+
`finalizeSession` 据此在收尾时打印一行提示——此前自动写入整体包在 `try{}catch{}` 里、写完就返回,
|
|
107
|
+
**用户完全看不到自动记忆动了什么**,而"用户看不见的自动写入"正是这条注入链能成立的关键一环。
|
|
108
|
+
|
|
109
|
+
> 诚实登记:`src/prompts.js` 的三处注入点(`user_memory` / `project_memory` / `agents_md`)
|
|
110
|
+
> 都走同一个 `fencedBlock`,已用变异验证三条防线各自有效;
|
|
111
|
+
> 但**「收尾打印提示」那段没有断言覆盖**——真正的写入路径要经 `helperProvider` 解析真实配置,
|
|
112
|
+
> 脱离环境无法单测,目前只由 tsc 与代码评审保障(已写在测试注释里)。
|
|
113
|
+
|
|
114
|
+
## 3.3 已修复(第四批:诊断包泄漏面)
|
|
115
|
+
|
|
116
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
117
|
+
| --- | --- | --- | --- |
|
|
118
|
+
| P2-13(代码审计报告) | `src/commands/diagnose.js`、`src/redact.js` | ① 诊断包 `writeFileSync` 未传 `mode`(本仓其它敏感产物一律 0600);② 脱敏**按字段名**匹配(`api_key\|token\|secret\|password…`),于是 `mcpServers.*.env.<自定义名>`、`tools[].env.*` 的值原样落进报告——而诊断包正是用户会主动贴到**公开反馈渠道**的产物 | ① 写盘 `{mode:0o600}` **且**补一次 `chmodSync`(mode 只在创建时生效,旧文件不会自动收紧);② 新增**结构感知**的 `redactConfig()`:`env`/`headers` 容器下的**全部值**掩码、键名命中密钥词的**整棵子树**掩码,保留键名与非敏感值 |
|
|
119
|
+
|
|
120
|
+
> `redactConfig` 的设计取向写在代码注释里:`author` 这类含 `auth` 的键会被过度掩码,
|
|
121
|
+
> 诊断可读性略降——但方向是安全的,**宁可少显示,不可泄漏**。
|
|
122
|
+
|
|
123
|
+
## 3.4 已修复(第五批:默认模型名散落 12 处 + 老配置路由静默失效)
|
|
124
|
+
|
|
125
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
126
|
+
| --- | --- | --- | --- |
|
|
127
|
+
| P3-4(自评报告)+ P2-10(代码审计报告) | `models.js` / `routing.js` / `config.js` / `web/*` / `tasks/worker.js` / `cost-guard.js` / `cli.js` / `commands/{repl,update}.js` | 厂家把 `deepseek-v4-flash` 改名后,**默认值仍以字面量散落在 12 处**——全是新装用户与兜底路径会走到的位置,等于给新用户一个 API 已不提供的模型名(v0.6.1 那次故障的翻版) | 新增单一来源 `DEFAULT_MODEL` / `DEFAULT_PLANNER_MODEL` / `DEFAULT_EXECUTOR_MODEL`,12 处全部改引用;旧名只保留在 `MODELS` 里做兼容 |
|
|
128
|
+
|
|
129
|
+
**修复过程中由既有测试抓出的一个更严重的连带问题**(比原报告说得更深):
|
|
130
|
+
把默认值改成新名后,路由/子代理的「当前模型是否在池内」是**精确字符串比较**——
|
|
131
|
+
老用户 config 里仍是旧名,于是被判成「**池外模型**(用户手动指定)」,**自动路由对他们静默失效**,
|
|
132
|
+
而且没有任何提示。已新增 `canonicalModel()` 归一(只用于「是不是同一个模型」的比较,
|
|
133
|
+
发给 API 的名字仍用配置原值),并在 `routeTask` / `subagentModel` 的池判定处使用。
|
|
134
|
+
|
|
135
|
+
**防复发**:新增**结构守卫**断言——旧名作为字面量只允许出现在 `models.js`;
|
|
136
|
+
其余任何文件写回字面量即测试失败并指名文件。这样厂家下次改名时不可能再散落。
|
|
137
|
+
(实测变异:把字面量写回 `commands/update.js` → 守卫报
|
|
138
|
+
「旧模型名不得作为字面量散落在 models.js 之外:commands/update.js(1)」。)
|
|
139
|
+
|
|
140
|
+
## 3.5 已修复(第六批:自启文件转义)
|
|
141
|
+
|
|
142
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
143
|
+
| --- | --- | --- | --- |
|
|
144
|
+
| P2-14(代码审计报告) | `src/autostart.js` | 三个平台的自启文件都把**用户可控的路径**直接插进格式里:plist 的 `<string>` 不转义 `& < >` → XML 非法 → `launchctl` 加载失败,而写文件本身"成功",表现为「开关打开了但登录后不自启」且错误只在 StandardErrorPath 里;`.desktop` 不转义 `\ " $ \``;`.bat` 不转义 `%` | 抽出纯函数 `plistContent` / `desktopEntryContent` / `batchContent` + 各自格式的转义规则;macOS 写完后用 `plutil -lint` **自检**,把静默失败变成立即失败 |
|
|
145
|
+
|
|
146
|
+
## 3.6 已修复(第七批:静默失效收尾)
|
|
147
|
+
|
|
148
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
149
|
+
| --- | --- | --- | --- |
|
|
150
|
+
| P2-5(代码审计报告) | `src/mcp-presets.js` | 缺参默认 cwd 的判据是「args 里含 `{dir}`」——把 **sqlite 的 `--db-path`** 也算进去了。于是缺参时静默传一个**目录**给「数据库文件路径(必填)」,`mcp-server-sqlite` 启动即失败且用户看不到原因 | 预设显式声明 `argKind: 'dir' \| 'file'`:只有目录类才默认 cwd;文件类缺参**报错**,且传了已存在目录时当场说清楚 |
|
|
151
|
+
| P2-12(代码审计报告) | `src/commands/repl.js` | `autoTitle` 没有独立 try/catch,而 `cli.js` 早已为同一问题加过(P2-4)——标题模型所在服务商没有 Key 时 `helperProvider` 抛错落到**外层** catch,于是「回答已经成功输出」却被报成错误 | 与 `cli.js` 同口径加独立 try/catch;并把失败原因以 dim 提示出来(不再静默) |
|
|
152
|
+
|
|
153
|
+
> 这次做了**全仓普查**:`generateTitle` 共 4 处调用(cli / repl / tasks-worker / web-server),
|
|
154
|
+
> 另三处本来就有保护——`repl.js` 是唯一漏的。为防第 5 处,新增**结构守卫**断言。
|
|
155
|
+
>
|
|
156
|
+
> ⚠ 守卫本身也踩了一次坑,值得记下:第一版写成「往前 18 行内要能看到 `try {`」,
|
|
157
|
+
> 变异验证时**发现它匹配到了外层 try**(而外层 catch 正是问题本身),去掉 `repl` 的
|
|
158
|
+
> try/catch 时守卫毫无反应。改为「**紧随其后**必须有 `catch`」后,正向通过、变异被抓并指名
|
|
159
|
+
> `commands/repl.js:685`。**没有变异验证的守卫等于没有守卫**。
|
|
160
|
+
|
|
161
|
+
## 3.7 已修复(第八批:日志写入与轮转)
|
|
162
|
+
|
|
163
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
164
|
+
| --- | --- | --- | --- |
|
|
165
|
+
| P2-2(代码审计报告) | `audit.js`、`memory.js`(journal)、`cachestats.js`、**`net-guard.js`(报告未提,普查发现)** | 四份「追加 + 超限截断」实现的触发条件都是**进程内计数**(`auditCount > 20000` 等)。CLI 每次进程只写几条、计数随进程结束归零 → 条件永远不成立 → **轮转是死代码**,`audit.jsonl`/`journal.jsonl`/`cache-stats.jsonl`/出网账本对 CLI 用户**无界增长**(与注释里「低频截断」的意图正好相反) | 触发改为按**文件大小**(`statSync` 廉价,超限才整文件读一次并重写;截断后大小回落,不会反复读)。跨进程有效,语义不变 |
|
|
166
|
+
| P2-3(代码审计报告) | `audit.js`、**`net-guard.js`** | 轮转用非原子 `writeFileSync`,截断过程中崩溃会留下**半截文件**——而它们是审计证据 | 改用 `atomicWriteFileSync`(tmp 名含 pid+随机后缀,rename 原子替换),mode 保持 `0600` |
|
|
167
|
+
| P2-1(代码审计报告) | `memory.js#removeMemoryLines` | 写回 `kept.join('\n')` **缺尾换行**,下次 `appendMemory` 会把两条记忆拼成同一行(`- [date] 旧- [date] 新`),该条既解析不出也读不懂 | 补 `+ '\n'`;隔壁 `dedupeMemory` 一直是对的 |
|
|
168
|
+
|
|
169
|
+
> ⚠ **P2-1 的触发条件比报告写的更窄,这点值得记下**:`raw.split('\n')` 会保留末尾空串,
|
|
170
|
+
> 所以「文件本来以换行结尾」时 `kept.join('\n')` 恰好仍带换行,缺陷**不复现**;
|
|
171
|
+
> 只有**末尾无换行**(用户用编辑面板保存、或手改文件后)才会粘行。
|
|
172
|
+
> 我第一版测试没构造这个前提——把修复撤掉做变异验证时**毫无反应**,属于「假绿」。
|
|
173
|
+
> 改为先写入一个无尾换行的文件后再删条目,变异即被抓住。
|
|
174
|
+
|
|
175
|
+
## 3.8 已修复(第九批:进程归属校验与整组清理)
|
|
176
|
+
|
|
177
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
178
|
+
| --- | --- | --- | --- |
|
|
179
|
+
| P2-6(自评报告) | `src/schedule.js#sleeperAlive` | **全仓唯一**的裸 `process.kill(pid, 0)` 判活,没有归属校验(其它等待/回收路径早已走 `proc.js`)。PID 被系统回收复用后,崩溃 worker 的 pid 若被无关进程占用,这里会认为「任务仍在跑」→ 该任务**永不重跑** | 走 `pidOwnedBy(pid, 'schedule-worker <id>')`;三值语义与 `proc.js` 一致(`null` = 无从判断时如实退化为存活判定,不夸大)。调用方一并传入 job id |
|
|
180
|
+
| P2-9(代码审计报告) | `src/tools/index.js` | 声明式工具(`config.tools`)超时只 `child.kill('SIGKILL')`——shell 死了,`sh -c 'a && b'` 的孙进程成孤儿继续跑,且它**持有 stdout/stderr 管道**,Node 的 `close` 会被拖到孙进程自己退出(每次"超时"实际拖满整个命令时长) | 与 `bash.js` 同口径加 `killGroup`(`process.kill(-pid)` 整组杀,失败回退 child);**并补 `detached: true`** 让子进程自成进程组——见下方教训。顺带给结果补 `timedOut` 字段与 bash 工具对齐 |
|
|
181
|
+
|
|
182
|
+
> ⚠ **第一个版本只对了一半,是测试把它揪出来的**:我只加了 `killGroup`,注释里还写着
|
|
183
|
+
> 「POSIX 下 spawnOpts 已让子进程自成进程组」——**但这里从来没传 `detached: true`**,
|
|
184
|
+
> 于是子进程与父进程同组,`process.kill(-pid)` 因「无此进程组」抛错、静默回退成只杀 shell,
|
|
185
|
+
> 孙进程照旧存活。补齐 `detached: true` 后,整轮 smoke 从 **92 秒降到 33 秒**(正是被拖住的那 60 秒)。
|
|
186
|
+
>
|
|
187
|
+
> ⚠ **断言必须带时限,只看"最终状态"会被掩盖**:孙进程最终总会退出(`sleep 60` 自己结束),
|
|
188
|
+
> 所以「超时后孙进程不存在」这类断言在缺陷存在时**依然全绿**,只是整轮慢了 60 秒。
|
|
189
|
+
> 改为断言「超时后必须**在 5 秒内**返回」才抓得住——这也正是用户能感知的症状。
|
|
190
|
+
|
|
191
|
+
## 3.9 已修复(第十批:任务终止与调度租约)
|
|
192
|
+
|
|
193
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
194
|
+
| --- | --- | --- | --- |
|
|
195
|
+
| P2-10(自评报告) | `src/tasks.js#killTask` | 只发 SIGTERM 就**立即**置终态 `killed`。worker 若被 `process.on('SIGTERM')` 拦下或处于同步阻塞,SIGTERM 不会让它退出——它继续跑、继续改文件,而面板已显示「已停止」(假的"已停止")。全仓此前**没有任何超时升级逻辑** | SIGTERM 后安排**非阻塞**升级:给 2.5 秒自己退出,仍在跑就 SIGKILL 整组;导出 `flushKillEscalation()` 供短命进程(CLI)显式等待,否则升级用的定时器会随进程退出丢失。Windows 用 `taskkill /T /F` 杀整棵进程树 |
|
|
196
|
+
| P2-11(自评报告) | `src/schedule.js#runSleeper`(报告写作 `runOnce`,实际函数名不同) | 轮询等待 worker 的循环**不检查租约**:主循环每个分支开头都有 `shouldStop()`、避峰等待后也有,唯独这里没有。daemon 租约被接管后旧 daemon 仍陪跑最多 **2 小时**,期间与新 daemon 并发操作同一批调度状态 | 循环条件加租约判据,并把判据抽成纯函数 `shouldKeepPolling()` 以便直接断言 |
|
|
197
|
+
|
|
198
|
+
> 实现取舍(写进注释):升级用**定时器轮询**而不是同步等待——本仓已把"阻塞事件循环"
|
|
199
|
+
> (`Atomics.wait`)列为缺陷 P2-7,不能自己再犯。定时器**刻意不 unref**:升级必须在进程
|
|
200
|
+
> 退出前跑完;代价是短命进程多活到升级结束(正常退出约 100–200ms)。
|
|
201
|
+
>
|
|
202
|
+
> 测试里也踩了两个坑:① `procAlive` 在 node **装好 SIGTERM 处理器之前**就为真,
|
|
203
|
+
> 此时发信号会走默认动作直接退出,测试会误判成"升级没生效"——改用 ready 文件消除竞态;
|
|
204
|
+
> ② 第一版 `unref()` 让 `await` 的 promise 永不 settle(顶层 await 悬空、退出码 13)。
|
|
205
|
+
|
|
206
|
+
## 3.10 部分修复(第十一批:文件锁的陈旧判据;**阻塞面未解决**)
|
|
207
|
+
|
|
208
|
+
| 项 | 位置 | 状态 |
|
|
209
|
+
| --- | --- | --- |
|
|
210
|
+
| P2-7 问题②③(死区 / 反向回收) | `src/atomic-write.js` | ✅ **已修**:陈旧判据改为看锁内容里的 `{pid}`——**持有者已死则立刻回收**(不再白等 15 秒),**持有者仍活着则绝不回收**(哪怕 `fn` 跑了很久)。默认值同时改为 `timeoutMs(20s) > staleMs(15s)` |
|
|
211
|
+
| P2-7 问题①(`Atomics.wait` 阻塞事件循环) | 同文件 | ⚠️ **未解决**,见下方说明 |
|
|
212
|
+
|
|
213
|
+
原判据只看 mtime 超过 `staleMs`,带来两个反向问题:崩溃后要白等满 15 秒才允许回收(配 5 秒的
|
|
214
|
+
timeout 就是死区,期间**所有写方必然失败**——`cachestats` 会静默跳过轮转丢计费明细);
|
|
215
|
+
而某个 `fn` 若耗时超过 15 秒(大文件重写/慢盘),别人会把**仍然活着**的锁判成陈旧并回收,
|
|
216
|
+
**互斥直接失效**。锁内容本来就写着 `{pid, at}`(此前只用于 TOCTOU 比对),现成判据一直没用上。
|
|
217
|
+
|
|
218
|
+
> ⚠ **未解决的那一面(如实登记)**:`withFileLockSync` 仍是**同步**实现,抢锁失败时用
|
|
219
|
+
> `Atomics.wait` 阻塞事件循环。彻底修需要引入异步变体并迁移调用点——全仓 **24 处**调用分布在
|
|
220
|
+
> `schedule.js`(12) / `workspace.js`(7) / `tasks.js`(2) / `cachestats.js` / `sync.js`,
|
|
221
|
+
> 其中多处位于**同步函数**内(如 `removeSchedule` 整体包在锁里做读-改-写),改造面很大。
|
|
222
|
+
> 本次先修正确性(死区与反向回收),阻塞面留待后续;**影响面已因死区消失而显著收窄**:
|
|
223
|
+
> 现在只有"另一个进程正合法持锁"时才会等待,等待时长由对方的真实工作量决定,而不是固定的 15 秒。
|
|
224
|
+
|
|
225
|
+
## 3.11 已修复(第十二批:能力声明与实现一致)
|
|
226
|
+
|
|
227
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
228
|
+
| --- | --- | --- | --- |
|
|
229
|
+
| P2-2(自评报告) | `src/permissions.js` | deny 前缀规则用**整条命令的前缀**匹配:`deny:['bash:rm *']` 拦得住 `rm -rf /x`,却放过 `cd /tmp && rm -rf /x`。auto 档下 deny 是**唯一防线**,失配即等于规则不存在 | deny 对 bash 一律按 shell 分隔符(`;` `&&` `\|\|` `\|` `&` 换行)**拆段**后逐段匹配,任一段命中即拒(fail-closed);allow 仍走前缀+字符白名单 |
|
|
230
|
+
| P2-3(自评报告) | `src/tools/git.js` | 黑名单用 `Set.has()` 精确匹配,而 git 接受长选项**唯一前缀缩写**:`--no-inde` ≡ `--no-index`(越界读任意路径,且 git ∈ READONLY_TOOLS **免权限确认**)、`--out=` ≡ `--output=`(写文件) | 长选项按**互为前缀**判定;短选项保持精确匹配并额外覆盖捆绑形式(`-Df`) |
|
|
231
|
+
| P2-1(自评报告) | `src/packs.js`、`docs/PACK-API.md` | `permissions` 全仓**无执行点消费**,文档却写着「最小权限/越出即拒绝」——失真的安全叙事比没有声明更危险 | ① 文档所有"声称强制"的措辞标注**尚未实现**并加醒目说明;② 加载时做**静态对照**:源码用到未声明能力(fetch→net、fs.xxx→fs、process.env→env)即告警并写明"不强制、请人工审阅 pack.mjs" |
|
|
232
|
+
| P2-4(自评报告) | `src/web/server.js` | `busySessions` 用请求开始时的 `session.file` 作键,而同一请求内自动标题会**改写**它 → 改名后用新文件名发起的 `/api/chat` 不被判「忙」,同一会话可并发两个回合 | 占位改为键集合,改名时迁移键(`claimSessionKey`),释放时清全部 |
|
|
233
|
+
|
|
234
|
+
> ⚠ P2-3 的实现我先写错了方向:正则写成 `^--(no-index|…)`(要求 token 含**完整名**),
|
|
235
|
+
> 而缩写是「token 是完整名的**前缀**」——实测 `--no-inde`、`--out=/tmp/x` 全部放行,**假绿**。
|
|
236
|
+
> 是验证脚本把变体逐个跑一遍才暴露,已改为双向判定。
|
|
237
|
+
>
|
|
238
|
+
> ⚠ P2-4 的端到端竞态窗口很短(改名后仅剩 token 统计与 `done` 事件),难以稳定复现,
|
|
239
|
+
> 故用**结构守卫**断言(改名分支必须调用 `claimSessionKey(session.file)`)——已如实标注。
|
|
240
|
+
|
|
241
|
+
## 3.12 已修复(第十三批:pack list 读配置 / 只读集合单一来源)
|
|
242
|
+
|
|
243
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
244
|
+
| --- | --- | --- | --- |
|
|
245
|
+
| P2-4(代码审计报告) | `src/commands/pack.js` | `listPacks({}, cwd)` 硬传空配置 → 用户按文档在 `config.packs` 里声明的 Pack,`mingdao pack list` / `pack info` **根本看不见** | 改传 `loadConfig()` |
|
|
246
|
+
| P2-11(代码审计报告) | `src/agent.js` | 只读子代理工具集是**手写的第二份副本**(与导出的 `READONLY_TIER_SET` 内容相近但不同),两处必然漂移 | 从 `READONLY_TIER_SET` 派生,并把两处**刻意**差异写明(去掉 `task`:子代理不再派子代理;去掉 `todo`:清单不与主线程共享)。派生结果与历史可见集逐项一致,**行为不变** |
|
|
247
|
+
|
|
248
|
+
> **连带发现(比报告说的更深)**:`listPacks` 把每个 tier 目录一律当作「装着若干 Pack 的
|
|
249
|
+
> **根目录**」扫描其子目录,而文档与 `MIGRATION-DEYI-v0.5.md` 写的都是**直接指向 Pack 目录**——
|
|
250
|
+
> 于是按文档声明的 Pack **一个都发现不了**(修好 `loadConfig` 也还是发现不了)。
|
|
251
|
+
> 现已两种形态都支持,并在 PACK-API 里写明。这个坑是写测试夹具时撞出来的:
|
|
252
|
+
> 我按**文档**写夹具,测试失败——第一反应是夹具错了,实际是**实现与文档不一致**。
|
|
253
|
+
|
|
254
|
+
## 3.13 已修复(第十四批:SSRF 逐跳复检单一来源)
|
|
255
|
+
|
|
256
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
257
|
+
| --- | --- | --- | --- |
|
|
258
|
+
| P2-7(代码审计报告) | `src/skill-registry.js`(+ 普查发现 `src/model-discovery.js`) | 同一件事**两份口径**:`skill-lib.js` 做了完整逐跳复检(`redirect:'manual'` + 每跳 `isPrivateHost` + DNS 复检 + 跳数上限),而 registry 只写 `redirect:'follow'`——自动跟随且**每一跳都不复检**,于是「线上技能库索引/技能文件」可被重定向到内网(云元数据 169.254.169.254 等)。**同一个安全判定有两套口径,等于最弱的那一套说了算** | 抽出单一来源 `src/safe-fetch.js`(`allowPrivate` 参数区分"用户显式配置"与"自动路径"),`skill-lib` / `skill-registry` / `model-discovery` **三处共用**;新增**结构守卫**:全仓不得再出现 `redirect:'follow'`(剥注释后判定) |
|
|
259
|
+
|
|
260
|
+
> ⚠ 迁移时被测试立刻抓到一处**真实回归**:registry 测试跑在 `127.0.0.1` 上,被新判据当成内网拒绝——
|
|
261
|
+
> 而「自建 registry 指向企业内网」是**文档明确支持**的场景。修法是按「是否用户显式配置」区分:
|
|
262
|
+
> `MINGDAO_REGISTRY_URL` 显式设置的源 → `allowPrivate: true`(用户自担意图,与 CLI 显式输入 URL 同口径);
|
|
263
|
+
> 默认公网源保持严格,不允许被重定向到内网。
|
|
264
|
+
>
|
|
265
|
+
> `model-discovery` 同样迁入(它的端点是**用户配置的服务商地址**,本地 vLLM/Ollama 就是内网,
|
|
266
|
+
> 故 `allowPrivate: true`),但逐跳的「非 http(s) 跳转拒绝 + 跳数上限 + 大小上限」仍然生效。
|
|
267
|
+
|
|
268
|
+
## 3.14 已修复(第十五批:出网闸门保留 Request 语义)
|
|
269
|
+
|
|
270
|
+
| 项 | 位置 | 问题 | 修复 |
|
|
271
|
+
| --- | --- | --- | --- |
|
|
272
|
+
| P2-8(自评报告) | `src/net-guard.js` | 闸门包装 `fetch` 时只取 `Request` 的 `.url`,随后 `curInit = { redirect: 'manual' }` —— method / body / headers / signal **全部丢失**,请求被**静默降级成 GET**(`fetch(new Request(u, { method:'POST', body }))` 实际发出的是空 GET)。同源第二症状:`new URL(loc, current)` 里 current 若是 Request 对象会退化成 `"[object Request]"` → 抛「非法重定向地址」 | 只传 `Request`(无 `init`)时把它的语义摊进 init;跳转基准统一为字符串 URL。有 `init` 时按 fetch 规范由 init 覆盖,不改变既有语义 |
|
|
273
|
+
|
|
274
|
+
> 报告标注为"潜在"(全仓当时 `grep 'new Request('` 无匹配)——但**Pack 与第三方代码用的是全局
|
|
275
|
+
> fetch**,很容易踩;而且失败是静默的(服务端只看到一个空 GET)。故一并修掉。
|
|
276
|
+
|
|
277
|
+
## 3.15 已修复(第十六批:audit-report 索引提取 + B-CT-1 核实)
|
|
278
|
+
|
|
279
|
+
`audit-report.md` 的明细指向 `30-defects/defect-register.md`(**未提供**),但 §4.2 给出了
|
|
280
|
+
22 个高级问题的代码索引——已抄录成可排期清单(见 §4.3)。
|
|
281
|
+
|
|
282
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
283
|
+
| --- | --- | --- | --- |
|
|
284
|
+
| B-CT-1(audit-report) | `src/tokenizer.js#makeTokenCounter` | 报告称「WeakMap 消息级 token 缓存**永远** miss、每步全量 BPE」——**实测不成立**:同一计数器下 200 次调用耗时 **0.0ms**(缓存命中),失效只发生在「计数器函数对象被重建」时 | 但确有一个**真实(较小)**的浪费:`makeTokenCounter` 每次调用都返回新函数对象,而 `context.js` 的缓存守卫是 `hit.fn === count` → 跨实例/跨回合必然失配、白算。已按模型名缓存计数器(模型名数量有限,不会无界增长) |
|
|
285
|
+
|
|
286
|
+
> **方法上的教训**:这份报告的**标题级结论会夸大**("永远 miss"实测不复现)。
|
|
287
|
+
> 引用它的条目时应**先复现再排期**——否则会把"实际很小"的问题当成性能主因去优化,
|
|
288
|
+
> 而真正的东西被漏掉。(这与前面 P2-1 的处理同源:先核实,再决定是修实现还是改措辞。)
|
|
289
|
+
|
|
290
|
+
## 3.16 已修复(第十七批:约束 pattern 的灾难性回溯 ReDoS)
|
|
291
|
+
|
|
292
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
293
|
+
| --- | --- | --- | --- |
|
|
294
|
+
| B-CON-1(audit-report) | `src/constraints.js#isValidPattern`、`src/packs.js` | **实测复现,且严重**:原校验只判「正则能否编译」,完全不防回溯。而 pattern 来自 **Pack 清单 / 用户配置**,匹配对象是**模型输出**(可被 fetch/read 引入的外部文本影响)。实测 `(a+)+$` 对 **29 字符**输入耗时 **4786ms**,每多一个字符翻倍——长输出是常态,**一个 Pack 就能让内核永久卡死** | 装载阶段 fail-closed 拒绝经典嵌套量词形状(`(a+)+`、`(a*)*`、`(\d+)*`、`(x+){2,}`),并把**具体原因**透给作者(原先只说"不合法",作者不知道怎么改)。刻意保守:`(a|b)+`、`(ab)+`、`(a+)?` 等常见且安全的写法零误伤 |
|
|
295
|
+
| B-HK-1(audit-report) | `src/hooks.js` | **复核结论:无外部注入面**。`hooksCfg` 来自用户自己的 `config.json`,Pack 的 contributions 只有 tools/constraints/promptSections/memorySchema,**无法贡献 hooks** | 不改(保留 `shell:true` 以支持管道等写法)。如实登记为"已核实、非缺陷" |
|
|
296
|
+
|
|
297
|
+
> B-CON-1 的实测过程值得记下:报告只给了代号(B-CON-1),没有位置。我是从代号推断
|
|
298
|
+
> `CON` = constraints,再实测复现(29 字符 4.8 秒)才确认它是**真的**——与上一个 B-CT-1
|
|
299
|
+
> (标题夸大、实测不复现)形成对照。**"先复现再动手"这条原则对两份方向都适用**:
|
|
300
|
+
> 既不轻信"很严重",也不轻信"不严重"。
|
|
301
|
+
|
|
302
|
+
## 3.17 已修复(第十八批:账本的「降级可见」与「尾部截断可检出」)
|
|
303
|
+
|
|
304
|
+
两条都属于同一类最危险的形态:**看起来有账本,其实不算数**。对合规物而言这比直接报错严重——
|
|
305
|
+
报错会被看见,「以为记全了、其实没有」不会。而 `ledger.js` 自己的文件头就写着
|
|
306
|
+
「被静默截断的合规账本比不记账更糟」。
|
|
307
|
+
|
|
308
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
309
|
+
| --- | --- | --- | --- |
|
|
310
|
+
| B-WS-1/2(audit-report) | `src/ledger.js#write` 的 `catch { alive = false }` | **实测复现**:让账本路径不可写后,`runEnd` 返回 `null`、`enabled=false`,而**用户看不到任何提示**,照常拿到一份「正常」总结 | 写失败记下次数与**具体原因**,由 `agent.js` 在回合收尾时明确告知("这次运行在 `~/.mingdao/ledger` 里可能不完整,不能作为完整审计依据"),并给出排查方向。**仍然不抛异常**——记账失败绝不影响正常执行,这条纪律不变 |
|
|
311
|
+
| A-LG-1(audit-report) | `src/ledger.js#verifyRun` | **实测复现,且比报告说得更具体**:只比对链内 `prev` 时,「删掉尾部若干行(含 `run.end`)」**完全查不出**——实测「只留前 3 行」照样返回 `ok:true`。尾部截断恰恰是最常见的形状(部分写入、误删、随手截断),而报告所说的"改行/删行"只覆盖了中间行 | `run.end` 落盘后额外写一份**封条**侧车(`<runId>.seal.json`:应有条数 + 链头哈希)。`verifyRun` 据此区分「链内一致」与「完整」:条数少了报截断、多了报追加、末行链头不符报尾部改写。**没有封条时不再谎报完整**,而是 `sealed:false` + 明确警告(含"封条可能被删除"这一情形) |
|
|
312
|
+
|
|
313
|
+
### 两处顺带发现的连带问题(不修就是留坑)
|
|
314
|
+
|
|
315
|
+
1. **封条未随轮转删除** → 每轮转一次就多留下永不回收的孤儿文件。已让 `rotateLedger` 一并删除,并加断言。
|
|
316
|
+
2. **命令层会说反话**:只改库不改命令,`mingdao ledger verify` 仍输出「✅ 哈希链完整」。现在
|
|
317
|
+
「链内一致但完整性未知」**退出码为 1** 且明说"无法确认"——这是**行为变更**,已记入
|
|
318
|
+
`docs/RELEASE-CHECKLIST.md`。只改库的修复等于没修:用户看到的还是那句 ✅。
|
|
319
|
+
|
|
320
|
+
### 诚实边界(写进导出物)
|
|
321
|
+
封条与账本**同目录同权限**,能防误删/漏写/随手改,**防不住同时改写两者的对手**;
|
|
322
|
+
仍然**不含可信时间戳**。导出物的说明行已按此改写,不再让读者以为拿到的是不可否认的证据。
|
|
323
|
+
|
|
324
|
+
> 另记:本次只处理了 `ledger.js` 这一处静默吞写入。全仓 `catch {}` 里**还有若干处包着写操作**
|
|
325
|
+
> (`task-state.js` / `session-index.js` / `titles.js` / `compact.js` / `cachestats.js`),
|
|
326
|
+
> 它们丢失的是任务状态、会话索引、标题与压缩摘要。**未修,如实登记**,见 §5 待办。
|
|
327
|
+
|
|
328
|
+
## 3.18 已修复(第十九批:任务检查点写失败——「承诺兑现不了」)
|
|
329
|
+
|
|
330
|
+
这一处与 §3.17 同属静默吞写入,但**危害形态更隐蔽**:不是"少一个文件",而是
|
|
331
|
+
**用户按提示操作却得不到承诺的效果**。
|
|
332
|
+
|
|
333
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
334
|
+
| --- | --- | --- | --- |
|
|
335
|
+
| B-WS-1/2(第二处) | `src/task-state.js#saveTaskState/clearTaskState` + 三个调用点(`cli.js` / `web/server.js` / `commands/repl.js`) | **实测复现**:跑满步数时 agent 打印「继续方式:直接发送『继续』即可」——那是**一个承诺**;而真正写检查点的是调用方,写在 banner **之后**,失败只 `catch {}`。于是承诺照旧给出、续跑提示却永远不出现:用户说「继续」,模型拿不到 goal/进度/交付物清单,只能从头猜。反向同样成立:`clearTaskState` 静默失败 → 已完成的任务仍留着「未完成」检查点 → 该会话下次被误提示续跑 | 两个函数都返回 `{ok, error}`(`clearTaskState` 把 **ENOENT 视为成功**——本来没有检查点是常态,不能借机刷告警);三个调用点全部消费结果,失败时明确告知后果与补救方式;文案只在 `checkpointHint()` 一处,避免"改一处漏两处" |
|
|
336
|
+
|
|
337
|
+
### 顺带修掉的一个**未核实断言**
|
|
338
|
+
agent 的续跑 banner 原文是「**已保存**检查点,继续方式:」——而打印它的时刻,
|
|
339
|
+
检查点**一个字都还没写**(要等 `runTurn` 返回后由调用方落盘)。一个未经核实的断言会让用户
|
|
340
|
+
以为断点已经安全,正是 B-WS-1/2 那类「看起来有、其实没有」。已改为
|
|
341
|
+
「检查点会在本回合收尾时保存(保存失败会另行提示)」,并加结构守卫**禁止**再出现「已保存检查点」。
|
|
342
|
+
|
|
343
|
+
### 结构守卫(这条是本次的重点)
|
|
344
|
+
三个调用点各两处 = 6 个调用点,**逐个**都必须用 `checkpointHint(...)` 包裹;同时断言
|
|
345
|
+
「调用点总数为 6」。只修 cli 而漏掉 web/repl 是这类缺陷最典型的复发方式——总数断言让
|
|
346
|
+
**新加的裸调用**当场暴露,而不是等下一次第三方报告再来指出。
|
|
347
|
+
|
|
348
|
+
## 3.19 已修复(第二十批:费用明细与会话索引的写失败)
|
|
349
|
+
|
|
350
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
351
|
+
| --- | --- | --- | --- |
|
|
352
|
+
| B-WS-1/2(第三处) | `src/cachestats.js#recordCacheStats` | **实测复现,且是四德里最要紧的一处**:`cache-stats.jsonl` 是 `todayCost()` 与日费用护栏(`costGuard`)的**唯一数据源**。读侧早就做了「读不了就告警一次、返回 null 表示无法判断」,**写侧却一直 `catch {}`**——追加失败时文件根本不变,读侧的 mtime 缓存继续命中旧值,护栏拿到一个偏小的「正常」数字,**用户可能超支而毫不知情**。读侧会说真话、写侧不会,这就是要补的不对称 | 写入返回 `{ok, error, phase}`(`append` / `rotate` 分开归因),失败时**一次性** `console.warn` 点明后果("护栏会少计、可能超支而不知")与排查方向;`cacheStatsWriteError()` 留可查痕迹。只告警一次:写不进去通常是持续性问题,每回合刷屏反而让人忽略它 |
|
|
353
|
+
| B-WS-1/2(第四处) | `src/session-index.js#saveShard` | **核实后的真实后果比看上去轻**:`syncSessionIndex` 返回的 `combined` 用的是**内存里**刚更新的条目,所以本次检索仍然正确;丢的是持久化 → 下次进程要重新扫描并分词,磁盘不可写时**每次都重算**。这是「能力静默降级」,不是「结果会缺失」 | 同样返回结果 + 一次告警,但**措辞如实**:明说"本次检索仍可用(索引在内存里),只是无法持久化"。告警不该把事情说得比实际严重 |
|
|
354
|
+
| — | `src/titles.js#renameSessionFile` | **复核结论:不是缺陷**。失败返回 `null`,而两个调用点(`cli.js` / `web/server.js`)**都已经判空** | 不改 |
|
|
355
|
+
|
|
356
|
+
### 一条跨平台教训(CI 实证)
|
|
357
|
+
第一版测试用「父路径是文件」制造 `clearTaskState` 的非 ENOENT 失败——**Windows 腿当场红**:
|
|
358
|
+
Windows 对这种情况回报的正是 **ENOENT**,与"本来就没有检查点"无法区分。
|
|
359
|
+
改用「目标是目录」(unlink 目录:POSIX 报 `EISDIR`、Windows 报 `EPERM`,两边都不是 ENOENT)才跨平台成立。
|
|
360
|
+
|
|
361
|
+
同一条断言还暴露了另一个**假绿**:`taskStateFile()` 会在会话名后再补 `.json`,
|
|
362
|
+
手拼 `'s.jsonl'` 实际指向 `'s.jsonl.json'`——一个根本不存在的文件,于是永远走 ENOENT 分支返回成功,
|
|
363
|
+
断言无论实现对错都通过。现在改用导出的 `taskStateFile()` 构造路径,并额外断言"文件确实还在",
|
|
364
|
+
以证明这次失败不是"本来就没有"。**假绿比红灯更值得警惕**:它会让一个从未生效的断言一直装作在保护你。
|
|
365
|
+
|
|
366
|
+
## 3.20 已修复(第二十一批:stdout/stderr 的 EPIPE 未处理)
|
|
367
|
+
|
|
368
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
369
|
+
| --- | --- | --- | --- |
|
|
370
|
+
| B-UI-1 / B-CLI-1 / B-REPL-1 | 全部裸 `process.stdout.write` / `process.stderr.write`(`ui.js` ×5、`commands/sync.js`、`net-guard.js`),入口未安装兜底 | **实测复现**:`process.stdout.write` 在读端提前关闭时产生的 EPIPE 是以 **stream `'error'` 事件**抛出的——没有监听者就是未捕获异常,**整个进程带堆栈崩溃**。同机对照:`console.log` 写满管道对端关闭退 0(Node 给 `console` 兜了底),换成 `process.stdout.write` 就崩(未捕获异常,退出码 99)。本仓有 7 处输出走的是裸 write,于是 `mingdao … | head -1` 这种**最常见的用法**正好命中 | `proc.js` 新增 `installPipeGuards()`。**两种策略必须在调用点显式选**:CLI/REPL 为 `exitOnEpipe:true`(对方关了管道就安静退出 0,不打印堆栈、不继续白跑长任务);常驻 WebUI 服务为 `exitOnEpipe:false`(服务不能因为「日志管道断了」自杀,写降级为静默)。**非 EPIPE 的流错误绝不吞**——重抛保持可见,否则会变成最难查的「输出莫名其妙没了」 |
|
|
371
|
+
|
|
372
|
+
### 两点值得记下
|
|
373
|
+
1. **「库里有函数」不等于修好**:只加 `installPipeGuards` 而不接线是最典型的假修复。因此结构守卫
|
|
374
|
+
同时钉住「`cli.js` 必须以 `true` 安装」「`web/server.js` 必须改回 `false`」「安装必须在第一次输出之前」。
|
|
375
|
+
2. **对照组不能省**:测试第一步先验证「不装兜底时确实会崩」。没有这个对照,三个断言在
|
|
376
|
+
一个从不触发 EPIPE 的环境里会全部假绿——而「从未真正复现过」正是这份审计报告里
|
|
377
|
+
B-CT-1 那种夸大结论的成因。
|
|
378
|
+
|
|
379
|
+
> 管道复现不依赖 shell:父进程 spawn 子进程后**立刻 `child.stdout.destroy()`** 关掉读端,
|
|
380
|
+
> 子进程写入即 EPIPE。这样 Windows 上同样成立,不必为跨平台写两套管道命令。
|
|
381
|
+
|
|
382
|
+
## 3.21 已修复(第二十二批:写失败却报告成功 + 把「静默吞写」变成受审阅的清单)
|
|
383
|
+
|
|
384
|
+
这一批的形态比「少一个文件」更坏:**用户被告知成功**。
|
|
385
|
+
|
|
386
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
387
|
+
| --- | --- | --- | --- |
|
|
388
|
+
| B-WS-1/2(第五处) | `src/workspace.js#saveWorkspaces` + `addWorkspace` / `renameWorkspace` / `removeWorkspace` | **实测复现的假成功**:`saveWorkspaces` 写失败只 `catch {}`,而调用方**无论如何都返回 `{ok:true}`** → CLI 与 WebUI 照常打印「✓ 已登记」。用户以为登记好了,`mingdao workspace list` 里永远不会有它,下次进项目目录也不会跟着切 | 三个函数都检查写入结果;失败返回 `{error: '工作空间注册表写入失败:…'}`。`removeWorkspace` 的返回类型特意保持 `true/false`(调用方用它做存在性判断)——写失败用 `{error}` 区分,只回 `false` 会被读成「没找到这个工作空间」,那是另一回事 |
|
|
389
|
+
| B-WS-1/2(第六处) | `src/schedule.js#spawnDaemon` 的 pidfile 写入 | **这一处最危险**:pidfile 写失败会让 `daemonAlive()` 永远为 false → 之后每次调用都**再拉起一个守护进程**,同一个定时任务被并发执行两次、三次……而 `spawnDaemon` 上方那段锁注释要防的正是这个 P0。**静默吞掉这一处,等于把那个 P0 重新引进门** | 写失败 → **无条件 `SIGKILL` 刚拉起的子进程** + 一次告警 + 如实返回 `false`(旧版返回「调用即 true」) |
|
|
390
|
+
| B-WS-1/2(第七处) | `src/memory.js#dedupeProjectMemory` | 算出了重复条数但**写不回去**时仍上报「已去重 N 条」——文件里那些重复行一条都没少 | 写失败返回 `0`(WebUI/CLI 都按这个数报给用户)+ 一次告警 |
|
|
391
|
+
| B-WS-1/2(第八处) | `src/audit.js#writeAudit` | 设计上「审计失败绝不影响会话」是对的,但**完全无声**:连"这次运行的审计是空的"都无从得知 | 保留「不打断会话」,但计数(`auditWriteFailures()`)、留原因、一次告警 |
|
|
392
|
+
|
|
393
|
+
### 根因反思:为什么上一轮漏了这四处
|
|
394
|
+
上一轮我是**按文件逐个查**的(`task-state` / `session-index` / `titles` / `compact` / `cachestats`),
|
|
395
|
+
于是 `workspace` / `schedule` / `memory` / `audit` 这四处全被漏掉。
|
|
396
|
+
「修一个漏九个」的根因不是不仔细,而是**没有穷举的手段**。
|
|
397
|
+
|
|
398
|
+
因此这一批把手段本身固化成测试:一个**全仓扫描器**找出所有「`catch` 里既不打日志、不抛、也不返回
|
|
399
|
+
任何东西,而 `try` 块里含写操作」的位置,与一份**已审阅白名单**逐一比对——
|
|
400
|
+
白名单里每一处都写明"为什么可以吞"(清理临时文件、chmod 收权、缓存可重建……)。
|
|
401
|
+
新增一处、或某文件处数变化,测试**立即失败**并要求复核。
|
|
402
|
+
|
|
403
|
+
> 扫描器同时也是一份**诚实的现状清单**:修完本批后仍有 18 处静默吞写,分布在 12 个文件里,
|
|
404
|
+
> 全部已在白名单中给出理由。它们不是"没看见",而是"看过并认为可以接受"。
|
|
405
|
+
|
|
406
|
+
### 一条自我纠错:假验证
|
|
407
|
+
本批的结构守卫(守护进程撤回那一步)第一版正则写成 `String\([^)]*\)`,因源码里是
|
|
408
|
+
`String(/** @type {any} */ (err)?.message ?? err)`——**括号嵌套**导致它在真实代码上就匹配不上。
|
|
409
|
+
于是两个突变"看起来被拦住了",其实测试**本来就是红的**,属于**假验证**。
|
|
410
|
+
改成 `[\s\S]{0,80}?` 容忍嵌套后重跑:真实代码通过、三个突变(删 kill / 包进 `if(false)` / 只改返回值)
|
|
411
|
+
全部被拦。**"突变让测试失败"只有在"未突变时测试确实通过"的前提下才有意义**,
|
|
412
|
+
这次差点又踩进去。
|
|
413
|
+
|
|
414
|
+
## 3.22 已修复(第二十三批:发布链路的凭据不进 argv / URL / 回显)
|
|
415
|
+
|
|
416
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
417
|
+
| --- | --- | --- | --- |
|
|
418
|
+
| D-REL-1/2 | `scripts/publish-mirror-releases.sh` | **实测确认三条泄露路径同时存在**:① 内层脚本形如 `ssh mingdao-server 'bash /tmp/…sh "$V" "$BODY" "$GITEE_TOKEN" "$GITCODE_TOKEN" "$NAME"'`——本地与**服务器**的 `ps` 都能看到 argv,而附件上传要跑几小时,argv 就挂几小时;② 脚本还把这一行**连同 token 明文一起 `echo` 出来**,那行会被复制进 shell 历史 / 聊天 / issue;③ gitee 的 token 写在 URL 里(`?access_token=…`)→ 进服务器上 `curl` 的 argv 与访问日志。同一类问题本仓在 P2-6(`sync passwd` 经 argv 传密)已经修过一次,**发布链路是漏网的那一处** | 凭据改为经 **ssh stdin** 落到服务器上一个 `umask 077` 的临时文件,内层脚本 `source` 之后**立即 `rm`**;两个平台都改用 HTTP 头鉴权;传 python 的 token 也从 argv 改成环境变量;回显的命令里不再有 token |
|
|
419
|
+
|
|
420
|
+
### gitee 头鉴权是**实测**过的,不是照猜
|
|
421
|
+
`access_token` 查询串换 `Authorization: token …` 头之前先验证:`GET /api/v5/user`(需鉴权)
|
|
422
|
+
用真 token 返回 **200**、用假 token 返回 **401**、匿名返回 **401**——三种对照齐了才动手。
|
|
423
|
+
只测「公开仓库的 GET 返回 200」是不够的:那可能只是匿名可读,**证明不了头鉴权生效**。
|
|
424
|
+
|
|
425
|
+
### 测试方式:桩替 + 金丝雀
|
|
426
|
+
不能真去碰服务器,所以把 `git` / `ssh` / `scp` / `curl` 换成桩(记录 argv),喂两个金丝雀 token,
|
|
427
|
+
断言它们**一次都不出现在任何 argv 或脚本回显里**,同时断言凭据**确实**经 ssh stdin 送达
|
|
428
|
+
(否则"哪儿都没传"也能让前一条通过)。另加静态守卫:URL 里不得有 `access_token=`、
|
|
429
|
+
内层脚本不得从**任何**位置参数取 token、`source` 之后必须立即 `rm`。
|
|
430
|
+
5 个突变全部被预期断言捕获。
|
|
431
|
+
|
|
432
|
+
> 又一处自我纠错:守卫第一版只挡 `GITEE_TOKEN="$3"`,而突变把 token 挪到 `$4` 就绕过去了
|
|
433
|
+
> (**守卫漏了,实测发现**)。已改成 `_TOKEN="$<任意数字>"` 全挡。
|
|
434
|
+
|
|
435
|
+
### 改这里时顺带发现的两个问题
|
|
436
|
+
|
|
437
|
+
1. **内层脚本根本没被送到服务器**:脚本只 `scp` 了发布文案,而生成的
|
|
438
|
+
`/tmp/mirror-release-$V.sh` 留在本地,回显的命令却让操作者去**服务器上**执行它——
|
|
439
|
+
除非有人手工拷过(且这一步从未写进清单),那条命令是跑不起来的。已补 `scp`。
|
|
440
|
+
2. **`/tmp` 在两个世界里不是同一个地方**:Windows 上 Node 把 `"/tmp"` 解析成 `D:\tmp`,
|
|
441
|
+
而 bash 的 `/tmp` 在别处。测试第一版因此**在 Windows 腿红**(`ENOENT: D:\tmp\…`)。
|
|
442
|
+
已给本地暂存目录加 `MIRROR_TMP` 开关(远端路径保持 `/tmp`,那里才是 Linux 服务器)。
|
|
443
|
+
|
|
444
|
+
> 测试的分层也随之定下来(**与第 75 节同口径**):**静态守卫全平台**(直接读脚本源码,
|
|
445
|
+
> 钉住"凭据不经过 argv/URL/回显"这些写法),**行为验证仅在有 bash 的平台**
|
|
446
|
+
> (Windows 的 `/tmp` 语义与 bash 不同,强行跑只会得到与实现无关的红灯)。
|
|
447
|
+
|
|
448
|
+
## 3.23 普查(第二十四批:路径穿越 B-SR-1——**当时未发现;下一批找到了,见 §3.24**)
|
|
449
|
+
|
|
450
|
+
> **更正**:本节当时列的七个攻击面里,**技能安装**那一面我只查到 `copySkillIntoUser` 内部有
|
|
451
|
+
> `validateSkillDir` 兜底就判定"已设防",**没有继续追"传进来的 name 是谁给的"**。
|
|
452
|
+
> 下一批顺着这条链往上追,发现 registry 索引名可以绕过 frontmatter 校验直接进 `rmSync`
|
|
453
|
+
> ——**实测可递归删除 skills 目录、甚至整个 `MINGDAO_HOME`**(§3.24)。
|
|
454
|
+
> 教训:**"这里有校验"不等于"到这里的数据已经被校验过"**,要一直追到数据源头。
|
|
455
|
+
|
|
456
|
+
报告只给了代号 `B-SR-1 | 安全/注入 | 路径穿越`,**没有位置**。逐个攻击面查下来,**每一处都已设防**。
|
|
457
|
+
但"某处有守卫"这种结论不写成断言,下次重构就会被悄悄拆掉,所以本轮把结论落成了回归测试。
|
|
458
|
+
|
|
459
|
+
| 攻击面 | 输入来源 | 现有防线 | 结论 |
|
|
460
|
+
| --- | --- | --- | --- |
|
|
461
|
+
| 执行账本 runId | 命令行 / 配置 | `isValidRunId` 白名单正则,`readRun`/`verifyRun`/`exportRun` 全部前置校验 | 已设防 |
|
|
462
|
+
| 技能名 | 命令行 / **远端 registry 索引** / `SKILL.md` frontmatter | `assertSafeSkillName`(拒 `..`/`.`/分隔符)+ `validateSkillMarkdown` 格式校验 | 已设防 |
|
|
463
|
+
| 技能目录安装 | **下载/克隆来的** `SKILL.md` | `copySkillIntoUser` 在**任何破坏性操作之前**调 `validateSkillDir` | 已设防(见下) |
|
|
464
|
+
| 工作空间名 | 命令行 / Web API | 拒绝含 `/` `\` 的名称 | 已设防 |
|
|
465
|
+
| 会话文件参数 | Web API(`?file=`、`body.file`) | 一律 `path.basename` | 已设防 |
|
|
466
|
+
| 目录浏览 `fs-browse` | Web API(`?dir=`) | **先 `path.resolve`**,再按 `path.sep` 做**分段边界**前缀比较(不是裸 `startsWith`),Windows 下大小写归一 | 已设防 |
|
|
467
|
+
| 会话检索 / 记忆文件 | 内部 | 会话名与 `.mingdao` 固定子路径 | 无外部输入 |
|
|
468
|
+
|
|
469
|
+
**最值得记的是技能目录那一处**——它差点是"看着有守卫、实际没用"的样板:
|
|
470
|
+
`copySkillIntoUser` 先用**未校验的** `meta.name` 拼出 `target = path.join(userSkillsDir(), name)`,
|
|
471
|
+
而 `fs.rmSync(target, {recursive:true, force:true})` 就在后面几行。也就是说,
|
|
472
|
+
一条 `name: ../../X` 的远端技能 = **任意目录删除**。拦住它的,只有中间那次
|
|
473
|
+
`validateSkillDir`(校验 frontmatter.name 格式)。这条链上**没有冗余防线**,
|
|
474
|
+
所以本轮专门为它写了端到端断言:金丝雀目录**正好放在穿越目标位置**,
|
|
475
|
+
再用恶意名字的目录去安装,断言金丝雀仍在。
|
|
476
|
+
|
|
477
|
+
> 顺带记一次**测试自身的假绿**(两处,都被变异验证抓出来):
|
|
478
|
+
> ① 金丝雀第一版放在 `../CANARY-<home>`,而实际穿越目标是 `../../X`(=`<parent-of-home>/X`)——
|
|
479
|
+
> 位置不对,即使守卫被拆掉 `rmSync` 也只是删了个不存在的东西,断言照样通过;
|
|
480
|
+
> ② 账本那条只断言"读一个**不存在**的路径返回空"——守卫在不在都返回空。
|
|
481
|
+
> 现在改成"穿越目标位置**真的放一个文件**,再看会不会被读到"。
|
|
482
|
+
> **假绿比红灯更危险:它让一个从未生效的断言一直装作在保护你。**
|
|
483
|
+
|
|
484
|
+
## 3.24 已修复(第二十五批:**B-SR-1 实测复现**——registry 索引名拼路径导致递归删除用户目录)
|
|
485
|
+
|
|
486
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
487
|
+
| --- | --- | --- | --- |
|
|
488
|
+
| B-SR-1 | `src/skill-registry.js#installFromRegistry` + `src/skill-lib.js#copySkillIntoUser` | **实测复现,且是本轮最严重的一处**。拼路径用的是**索引里的 name**,而校验的是**下载文件里的 `frontmatter.name`**——两个独立输入,可以不一致。入口那层 `/^[A-Za-z0-9_.-]+$/` 看着像白名单,**但它允许 `.` 与 `..`**。复现(修复前,本机):自建 registry 索引放一条 `{"name": "."}`、SKILL.md 里写合法的 `name: innocent-name`,然后 `installFromRegistry('.')` →<br>· `target = path.join(userSkillsDir(), '.')` = **整个 skills 目录** → `rmSync(target,{recursive:true})` **把用户所有已装技能递归删除**,还返回成功;<br>· 换成 `{"name": ".."}`(服务器做路径规范化时)→ `target` = **整个 `MINGDAO_HOME`** → `config.json` / **凭据** / 会话 / 账本一起清空 | 两层防线:① `installFromRegistry` **先**校验索引名(在网络请求之前就拒,也省掉白下载一轮);② 安装目录改用**校验过的 `frontmatter.name`**(`check.name`),而不是索引名;③ `copySkillIntoUser` 在拼路径**之前**加一道与调用方无关的兜底校验——`rmSync` 就在它下面几行,这里原本**没有任何冗余防线** |
|
|
489
|
+
|
|
490
|
+
### 为什么值得单独记一笔
|
|
491
|
+
这一处不是"理论风险":**索引是网络内容**(自建 registry、被篡改的镜像、未签名的第三方索引),
|
|
492
|
+
而索引里的 `name` 不在任何哈希覆盖范围内(每个文件的 sha256 是**对着索引**校验的,
|
|
493
|
+
索引本身不签名)。也就是说,一条 `{"name": ".."}` 就能让一次"安装技能"变成
|
|
494
|
+
**清空用户全部配置与凭据**,并且全程显示成功。
|
|
495
|
+
|
|
496
|
+
### 两次自我纠错(都值得留着)
|
|
497
|
+
1. **§3.23 的结论定得太早**:我当时查到 `copySkillIntoUser` 内部有 `validateSkillDir` 就收手了,
|
|
498
|
+
没有继续追"这个 name 是谁传进来的"。**"这里有校验"≠"到这里的数据已经被校验过"**——
|
|
499
|
+
要顺着数据流追到源头。
|
|
500
|
+
2. **注释又骗了扫描器一次**(本项目第二次):为 `copySkillIntoUser` 写的结构守卫要比较
|
|
501
|
+
"名字校验"与"`fs.rmSync`"的先后顺序,而我在**注释里**写着"rmSync 就在它后面几行",
|
|
502
|
+
于是 `indexOf('fs.rmSync(target')` 先命中了注释。剥掉注释再比才正确
|
|
503
|
+
(上一次是 `redirect:'follow'`)。
|
|
504
|
+
|
|
505
|
+
## 3.25 已修复(第二十六批:词表读失败终生锁死,且降级无声)
|
|
506
|
+
|
|
507
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
508
|
+
| --- | --- | --- | --- |
|
|
509
|
+
| B-TOK-1 | `src/tokenizer.js#loadData` | **实测确认**,报告说的"永不重试"成立:原实现是 `if (data || loadError) return data;`——**一次读失败就终生锁死**,之后每次计数都走启发式估算(该文件自己写明校准误差可达 **±2 倍**),而用户**完全看不到**:上下文预算裁剪、自动压缩、费用估算全都悄悄偏了。同一个文件里 `customTokenizerNames()` 的注释明确写着「下次重试」,这条路径却违反了它 | 按错误性质区分:**ENOENT**(打包缺失)latch 住不再重试;**其余**(EBUSY/EAGAIN/权限/瞬时 IO)**允许重试**,带 60 秒冷却窗口避免每步读坏盘;无论哪种,降级**说出来一次**并点明后果与修法。「自动恢复」这一点很关键:`mingdao doctor` 之类不再需要重启进程 |
|
|
510
|
+
|
|
511
|
+
补充:新增 `tokenizerDegraded() / tokenizerLoadError() / resetTokenizerState()`,冷却窗口可用
|
|
512
|
+
`MINGDAO_TOKENIZER_RETRY_MS` 覆盖(诊断时可调小以便"修好后立刻自愈")。
|
|
513
|
+
|
|
514
|
+
> **测试必须放子进程**:词表在 smoke 主进程里早就被别处加载过(`data` 非空),
|
|
515
|
+
> 主进程里打桩 `fs.readFileSync` 根本走不到失败分支——第一版就是这么假绿的。
|
|
516
|
+
> 这也顺带解释了为什么这个缺陷能长期存在:**任何"加载一次就常驻"的路径,都很难在同一个进程里被复现**。
|
|
517
|
+
|
|
518
|
+
> 又一次变异验证抓出的假绿:第一版只断言了「冷却窗口内不重试」,
|
|
519
|
+
> 把实现改回"终生锁死"**照样通过**——因为显式 reset 清掉了状态,掩盖了差异。
|
|
520
|
+
> 补上「冷却过后**不做任何重置**也必须自动恢复」这条断言后,该突变才被拦下。
|
|
521
|
+
> **断言要落在"这条修复独有的行为"上**,否则测的是别的东西。
|
|
522
|
+
|
|
523
|
+
## 4. 其余登记项(**第三方结论,我未逐条复核**)
|
|
524
|
+
|
|
525
|
+
### 4.1 自评报告(`MingDao-harness-v0.6.1-技术评估报告.md`)
|
|
526
|
+
|
|
527
|
+
| 级别 | 位置 | 摘要 |
|
|
528
|
+
| --- | --- | --- |
|
|
529
|
+
| ~~P2-1~~ | ~~`pack.json` 的 `permissions`~~ | ✅ **已修**(见 §3.11) |
|
|
530
|
+
|
|
531
|
+
> 注意:**两份报告的编号会撞车**(自评报告的 P2-5 是「终端注入」,第三方代码审计报告的
|
|
532
|
+
> P2-5 是「MCP 预设参数未替换」)。引用编号时务必带上来源,否则会对错条目。
|
|
533
|
+
| ~~P2-2~~ | ~~权限 `deny`~~ | ✅ **已修**(见 §3.11) |
|
|
534
|
+
| ~~P2-3~~ | ~~`git` 参数黑名单~~ | ✅ **已修**(见 §3.11) |
|
|
535
|
+
| ~~P2-4~~ | ~~Web 会话忙锁~~ | ✅ **已修**(见 §3.11) |
|
|
536
|
+
| ~~P2-5~~ | ~~终端渲染~~ | ✅ **已修**(见 §3.1,`io.print` 统一过 `sanitizeKeepingSgr`) |
|
|
537
|
+
| ~~P2-6~~ | ~~`sleeperAlive`~~ | ✅ **已修**(见 §3.8) |
|
|
538
|
+
| P2-7 | 文件锁 | ✅ **部分已修**(陈旧判据与默认值,见 §3.10);⚠️ `Atomics.wait` 阻塞面未解决 |
|
|
539
|
+
| ~~P2-8~~ | ~~出网闸门~~ | ✅ **已修**(见 §3.14) |
|
|
540
|
+
| ~~P2-9~~ | ~~项目记忆~~ | ✅ **已修**(见 §3.2) |
|
|
541
|
+
| P2-10 | `killTask` | 只发 SIGTERM 且立即改状态(Windows 无进程组语义) |
|
|
542
|
+
| P2-11 | 调度 `runOnce` | 轮询期间不检查租约,最长空转 2 小时 |
|
|
543
|
+
| P3-1 | 避峰备注 | 写「北京时间」却打印 UTC(错 8 小时) |
|
|
544
|
+
| P3-2 | `kind:'every'` | `nextRunAt` 缺失时每 1 秒空转 |
|
|
545
|
+
| P3-3 | git 工具 | 默认加 `--stat` 与模型显式 `--no-stat` 冲突 |
|
|
546
|
+
| ~~P3-4~~ | ~~模型默认值~~ | ✅ **已修**(见 §3.4;实测 12 处,非 8 处) |
|
|
547
|
+
| P3-5 | Pack lint | 去注释正则会把字符串里的 `//` 一起删掉(漏报) |
|
|
548
|
+
|
|
549
|
+
### 4.2 第三方代码审计报告(`MingDao-Harness-v0.6.1-代码审计报告.md`)
|
|
550
|
+
|
|
551
|
+
| 级别 | 位置 | 摘要 |
|
|
552
|
+
| --- | --- | --- |
|
|
553
|
+
| P2-1 | `src/memory.js:96` | `removeMemoryLines` 写回无尾换行,下次 append 拼出坏行 |
|
|
554
|
+
| P2-2 | `src/audit.js:36-45`、`memory.js:101/117` | 截断依赖**进程内**计数 → CLI 下截断是死代码,日志无界增长 |
|
|
555
|
+
| P2-3 | `src/audit.js:41` | 截断用 `writeFileSync` 而非原子写,崩溃丢事件 |
|
|
556
|
+
| ~~P2-4~~ | ~~`src/commands/pack.js:31/166`~~ | ✅ **已修**(见 §3.12,并修掉连带发现的文档/实现不一致) |
|
|
557
|
+
| P2-5 | `src/mcp-presets.js:43-48` | sqlite 预设必填参数含 `{dir}` 未替换,静默落到 cwd |
|
|
558
|
+
| ~~P2-6~~ | ~~`src/commands/sync.js:81-88`~~ | ✅ **已修**(见 §3.1) |
|
|
559
|
+
| P2-7 | `src/skill-registry.js:56` | `redirect:'follow'` 无逐跳复检,与 `skill-lib.js` 两种口径 |
|
|
560
|
+
| P2-8 | `src/skill-registry.js:144-150` | sha256 校验可选,缺字段仍打印「✓ 已安装」 |
|
|
561
|
+
| ~~P2-9~~ | ~~`src/tools/index.js:332-374`~~ | ✅ **已修**(见 §3.8) |
|
|
562
|
+
| P2-10 | `src/routing.js:17`、`config.js:160` | 路由默认仍是改名前的旧模型名 |
|
|
563
|
+
| ~~P2-11~~ | ~~`src/agent.js:104` vs `:34`~~ | ✅ **已修**(见 §3.12) |
|
|
564
|
+
| P2-12 | `src/commands/repl.js:677-685` | 自动标题无 try/catch(`cli.js` 已为同类问题加过) |
|
|
565
|
+
| P2-13 | `src/commands/diagnose.js:101` | 诊断包未按 0600 落盘,脱敏只按字段名匹配 |
|
|
566
|
+
| P2-14 | `src/autostart.js:62-77/86` | plist/desktop 字符串插值未转义,路径含 `&<>` 时静默失效 |
|
|
567
|
+
|
|
568
|
+
### 4.3 `audit-report.md`(第三方,口径与前两份不同)——**已提取索引**
|
|
569
|
+
|
|
570
|
+
自述 243 文件全覆盖,P0×17 / P1×15 / P2×8,五维综合 6.25/10(安全最低 5.0)。
|
|
571
|
+
**明细不在本文件里**:§4.3 明确指向 `30-defects/defect-register.md`(**未提供给我们**)。
|
|
572
|
+
但 §4.2 给出了 22 个高级问题的**代码索引**,足够排期——已抄录如下并标注状态:
|
|
573
|
+
|
|
574
|
+
| 代码 | 类别 | 摘要 | 状态 |
|
|
575
|
+
| --- | --- | --- | --- |
|
|
576
|
+
| B-CMD-3 | 凭据泄露 | `sync passwd` 新密码走命令行 | ✅ 已修(§3.1) |
|
|
577
|
+
| ~~B-CMD-1/2~~ | 资源泄漏 | readline 未 close | ✅ **已核实为非缺陷**(§3.20 附:全仓仅 3 处 createInterface,均已有 close;EOF 下 question 正常回调) |
|
|
578
|
+
| B-ATW-1/2 | 环境兼容 | SharedArrayBuffer / `Atomics.wait` 阻塞 | ⚠ 已登记(§3.10:正确性已修,**阻塞面未解决**) |
|
|
579
|
+
| B-CT-1 | 性能 | 「WeakMap 缓存永远 miss,每步全量 BPE」 | ✅ **已核实并按其真实影响处理**(§3.15):**"永远 miss"不成立**——同一计数器下 200 次调用 0.0ms;真问题是计数器 identity 不稳定,已按模型名缓存 |
|
|
580
|
+
| ~~B-CON-1~~ | 安全/注入 | ReDoS | ✅ **已核实并修复**(§3.16:约束 pattern 的嵌套量词,实测 29 字符 4.8 秒) |
|
|
581
|
+
| ~~B-HK-1~~ | 安全/注入 | `shell:true`(`hooks.js`) | ✅ **已核实为非缺陷**(§3.16:hooks 来自用户自己的 config,Pack 无法贡献) |
|
|
582
|
+
| B-SR-1 | 安全/注入 | 路径穿越 | ⚠ 待核实(未指明位置) |
|
|
583
|
+
| A-PI-1 | 安全 | 「无 Key 强制」 | ✅ v0.6.0 C4 已处理(本地/内网端点免 Key) |
|
|
584
|
+
| ~~A-LG-1~~ | 数据完整性 | 哈希链盲区 | ✅ **已核实并修复**(§3.17:盲区是**尾部截断**,实测「只留前 3 行」旧实现报 ok:true。已加封条侧车) |
|
|
585
|
+
| B-CS-1 | 数据完整性 | 竞态丢数据 | ⚠ 待核实 |
|
|
586
|
+
| B-WS-1/2 | 数据完整性 | 静默吞写入 | 🟡 **账本这一处已修**(§3.17:写失败留痕 + 收尾提示);**其余 `catch {}` 包写操作的位置未修**(§3.17 末段列出了具体文件) |
|
|
587
|
+
| B-TOK-1 | 数据完整性 | 词表永不重试 | ⚠ 待核实 |
|
|
588
|
+
| B-SL-1 | 资源泄漏 | 临时目录 | ⚠ 待核实 |
|
|
589
|
+
| ~~B-UI-1 / B-CLI-1 / B-REPL-1~~ | EPIPE | 标准输出 EPIPE 未处理 | ✅ **已核实并修复**(§3.20:实测裸 write 会带堆栈崩溃,已加两种策略的兜底) |
|
|
590
|
+
| B-CMD-4 | 凭据泄露 | skill token | ⚠ 待核实 |
|
|
591
|
+
| D-REL-1/2 | 凭据泄露 | gitee token(发布链路) | ⚠ 待核实(本仓已做「凭据不进产物」核查,见 RELEASE-CHECKLIST §1.2) |
|
|
592
|
+
|
|
593
|
+
> **要真正排期这 22 项,需要那份 `defect-register.md`**(含 file:line);
|
|
594
|
+
> 上表的"待核实"不是"已确认存在",而是**该报告未给出可复现位置**、我尚未逐条定位。
|
|
595
|
+
> 已核实的 B-CT-1 说明了一点:**这份报告的标题级结论可能夸大**("永远 miss"实测不成立),
|
|
596
|
+
> 引用它的条目时应先复现再排期。
|
|
597
|
+
|
|
598
|
+
## 5. 处理顺序与进度
|
|
599
|
+
|
|
600
|
+
**总口径**:三份第三方报告的可执行条目,**已核实并处理 18 批**(§2.1–§3.17)。
|
|
601
|
+
每一批都要求:先复现 → 再定级 → 修复 → 回归断言 → **变异验证断言本身**。
|
|
602
|
+
「先复现」对两个方向同样适用:B-CT-1 标题夸大(实测不复现),B-CON-1 / A-LG-1 实测确实严重。
|
|
603
|
+
|
|
604
|
+
**已闭合的类别**:
|
|
605
|
+
|
|
606
|
+
- 三个过程缺陷(§2.1)、缓存跨代理污染(§2.2)、**项目级 Pack 信任门**(§3)
|
|
607
|
+
- **凭据泄露**:`sync passwd` 命令行传密(§3.1 P2-6)、诊断包结构脱敏(§3.3 P2-13)
|
|
608
|
+
- **静默失效**:出网闸门丢 Request 语义(§3.1 P2-8)、旧模型名静默失效(§3.4)、
|
|
609
|
+
自启文件未转义(§3.5)、MCP 预设参数(§3.6)、**账本写失败无声 + 尾部截断不可检出**(§3.17)
|
|
610
|
+
- **终端注入**:控制序列过滤(§3.1 P2-5)
|
|
611
|
+
- **记忆注入**:项目记忆持久化围栏(§3.2 P2-9)
|
|
612
|
+
- **日志/调度/锁**:写入与轮转按大小触发(§3.7)、进程归属校验与整组清理(§3.8)、
|
|
613
|
+
`/proc` 命令行归一(§3.8 附)、锁陈旧判据看持有者 pid(§3.10)
|
|
614
|
+
- **能力声明不一致**:deny 按段匹配 / git 缩写前缀 / permissions 说实话 / 忙锁键迁移(§3.11)
|
|
615
|
+
- **安全**:SSRF 逐跳复检单一来源(§3.12)、token 计数器 identity(§3.13)、
|
|
616
|
+
约束 pattern 防灾难性回溯(§3.16)、`hooks.js` shell:true 核实为**非缺陷**(§3.16)
|
|
617
|
+
|
|
618
|
+
**未闭合(如实登记,不假装完成)**:
|
|
619
|
+
|
|
620
|
+
1. ~~**`catch {}` 里还包着写操作的位置**~~ ✅ **已穷举并清完**(§3.17–§3.21):
|
|
621
|
+
`ledger` / `task-state` / `cachestats` / `session-index` / `workspace` / `schedule`(pidfile) /
|
|
622
|
+
`memory`(去重) / `audit` **已修**;`titles` 复核为**非缺陷**(调用点已判空)、
|
|
623
|
+
`compact` 复核为**正常重试写法**。剩余 18 处已在 §3.21 的白名单里**逐条写明可以吞的理由**,
|
|
624
|
+
并由测试常驻约束——新增即失败。**这一类不再靠人工记忆。**
|
|
625
|
+
2. **`withFileLockSync` 的阻塞面**(自评 P2-7,§3.10):正确性已修,但它仍是同步 `Atomics.wait`——
|
|
626
|
+
同步锁阻塞事件循环;异步版本 + 24 个调用点迁移(schedule 12 / workspace 7 / tasks 2 /
|
|
627
|
+
cachestats / sync)**未做**。
|
|
628
|
+
3. ~~**EPIPE 未处理**(B-UI-1 / B-CLI-1 / B-REPL-1)~~ ✅ **已修**(§3.20:实测复现并加了两种策略的兜底)。
|
|
629
|
+
4. ~~**readline 未 close**(B-CMD-1/2)~~ ✅ **已普查完毕**:全仓仅 3 处 `createInterface`
|
|
630
|
+
(`ui.js` / `commands/sync.js` / `commands/skill.js`),**都已有 `close()`**;
|
|
631
|
+
另外实测本版 Node 下 `rl.question` 在 stdin EOF 时会正常回调(不会永久挂起),
|
|
632
|
+
`askAutoStart` 那条路径不存在「泄漏 + 卡死」。**结论:非缺陷,如实登记为已核实。**
|
|
633
|
+
5. ~~**路径穿越**(B-SR-1)~~ ✅ **已定位并修复**(§3.24:**实测复现**——registry 索引名可绕过
|
|
634
|
+
frontmatter 校验直接进 `rmSync`,`{"name":".."}` 会清空整个 `MINGDAO_HOME`)。
|
|
635
|
+
注意 §3.23 当时误判为"未发现",§3.24 已更正并说明原因。
|
|
636
|
+
6. ~~**词表永不重试**(B-TOK-1)~~ ✅ **已定位并修复**(§3.25:`loadData` 一次失败终生锁死 + 无声降级)。
|
|
637
|
+
**临时目录**(B-SL-1):已普查全仓 3 处 `mkdtempSync`(`skill-lib.js` ×2 有明确 `rmSync`、
|
|
638
|
+
`skill-registry.js` 有 `finally` 清理),**未发现泄漏**;报告未给位置,故只能给到"普查结论"。
|
|
639
|
+
**skill token**(B-CMD-4):**仍未定位**(全仓搜不到任何 "skill token" 相关代码,
|
|
640
|
+
猜测指技能来源凭据之类,但无从核实)。
|
|
641
|
+
~~gitee token 发布链路(D-REL-1/2)~~ ✅ **已修**(§3.22)。
|
|
642
|
+
7. **§4.3 `audit-report.md` 其余约 15 项**:该报告只给了 22 个**代号**、
|
|
643
|
+
明细在**未提供**的 `30-defects/defect-register.md` 里,因此多数条目连位置都不知道。
|
|
644
|
+
已核实的 4 条(B-CT-1 / B-CON-1 / B-HK-1 / A-LG-1)说明它**既有夸大也有真货**,
|
|
645
|
+
所以对它既不能整份照单排期、也不能整份忽略——只能逐条复现。
|
|
646
|
+
8. **账本可选签名**(`--sign-key`):仍未实现。封条只提升到「防误删/漏写」,
|
|
647
|
+
**不等同于审计级不可否认**,这一点已写进导出物。
|