mingdao-harness 0.5.0 → 0.6.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.
Files changed (56) hide show
  1. package/README.md +28 -4
  2. package/docs/AUDIT-v0.4.6.md +77 -15
  3. package/docs/CHANGELOG-PACK.md +21 -0
  4. package/docs/CONFIG.md +67 -0
  5. package/docs/HANDOVER.md +51 -0
  6. package/docs/MIGRATION-DEYI-v0.5.md +100 -4
  7. package/docs/PACK-API.md +42 -2
  8. package/docs/PLAN-v0.5.0.md +2 -0
  9. package/docs/PLAN-v0.6.0.md +228 -0
  10. package/docs/PROVIDERS.md +3 -0
  11. package/docs/RELEASE-CHECKLIST.md +211 -0
  12. package/docs/RELEASE-TRAIN.md +160 -0
  13. package/docs/ROADMAP-NEXT.md +2 -2
  14. package/install.ps1 +49 -14
  15. package/install.sh +58 -17
  16. package/package.json +1 -1
  17. package/src/agent.js +108 -1
  18. package/src/atomic-write.js +6 -0
  19. package/src/batch.js +39 -2
  20. package/src/cachestats.js +32 -0
  21. package/src/cli.js +49 -60
  22. package/src/commands/key.js +24 -4
  23. package/src/commands/ledger.js +141 -0
  24. package/src/commands/net.js +86 -0
  25. package/src/commands/repl.js +5 -53
  26. package/src/commands/update.js +5 -2
  27. package/src/constraints.js +86 -5
  28. package/src/cost-guard.js +10 -2
  29. package/src/help.js +95 -0
  30. package/src/ledger.js +423 -0
  31. package/src/memory.js +13 -1
  32. package/src/models.js +29 -0
  33. package/src/net-guard.js +251 -0
  34. package/src/net-policy.js +129 -0
  35. package/src/packs.js +20 -2
  36. package/src/permissions.js +46 -11
  37. package/src/proc.js +101 -0
  38. package/src/providers/openai-compatible.js +8 -1
  39. package/src/replay.js +132 -0
  40. package/src/routing.js +2 -0
  41. package/src/schedule.js +83 -34
  42. package/src/skill-lib.js +25 -9
  43. package/src/skills.js +50 -7
  44. package/src/sync.js +49 -12
  45. package/src/tasks/worker.js +15 -6
  46. package/src/tasks.js +84 -8
  47. package/src/titles.js +3 -0
  48. package/src/ui.js +50 -10
  49. package/src/update.js +79 -6
  50. package/src/web/app.js +12 -0
  51. package/src/web/routes/api.js +4 -2
  52. package/src/web/routes/domains/config.js +19 -0
  53. package/src/web/routes/domains/sessions.js +19 -1
  54. package/src/web/routes/domains/workspace.js +44 -6
  55. package/src/web/server.js +41 -4
  56. package/src/workspace.js +54 -36
package/README.md CHANGED
@@ -53,16 +53,20 @@ npm install -g mingdao-harness # 之后 mingdao / mdh 即可用(升级:npm
53
53
 
54
54
  ```bash
55
55
  # Gitee(国内推荐)
56
- curl -fsSL https://gitee.com/MingDaoTCM/MingDao-harness/raw/main/install.sh | bash -s -- gitee
56
+ curl -fsSL -o /tmp/mingdao-install.sh https://gitee.com/MingDaoTCM/MingDao-harness/raw/main/install.sh && bash /tmp/mingdao-install.sh gitee
57
57
 
58
58
  # GitCode(国内推荐;其 raw 接口对 curl 有反爬拦截,改用克隆式)
59
59
  git clone https://gitcode.com/MingDaoTCM/MingDao-Harness.git MingDao-Harness && cd MingDao-Harness && bash install.sh
60
60
 
61
61
  # GitHub(海外)
62
- curl -fsSL https://raw.githubusercontent.com/MingDaoTCM/MingDao-Harness/main/install.sh | bash -s -- github
62
+ curl -fsSL -o /tmp/mingdao-install.sh https://raw.githubusercontent.com/MingDaoTCM/MingDao-Harness/main/install.sh && bash /tmp/mingdao-install.sh github
63
63
  ```
64
64
 
65
- > 若某平台的 raw 脚本下载被反爬拦截,把 `| bash` 换成克隆式安装即可(见下方「手动克隆」)。
65
+ > **为什么不是 `curl … | bash`**:管道形式下 curl 失败(网络中断/被反爬拦截)时不会输出任何内容,
66
+ > 右侧 bash 读到 EOF 后**以 0 退出**——终端什么都不打印,用户会以为装好了。先下载再执行可以用
67
+ > `&&` 卡住失败,也能看到真实报错。
68
+ >
69
+ > 若某平台的 raw 脚本下载被反爬拦截,改用克隆式安装(见下方「手动克隆」)。
66
70
 
67
71
  或手动克隆(建议在本平台克隆,速度最快;目录名统一为 `MingDao-Harness`):
68
72
 
@@ -238,6 +242,26 @@ mingdao cost --by pack # 垂域费用分账
238
242
  - **权限三档**:`ask`(默认,写文件/命令逐次确认)/ `auto` / `readonly`;工具级规则 `{"mode":"ask","allow":["bash:git *"],"deny":["write"]}`
239
243
  - **沙箱三档**(Linux + bubblewrap):`off` / `readonly` 全盘只读 / `safe` 只读+断网;非 Linux 自动降级并明示
240
244
  - **密钥分离**:Key 存 `~/.mingdao/credentials.json`(600 权限,`mingdao key` 管理),`config.json` 无密钥可分享可提交
245
+ - **共享令牌 = 同一用户**(v0.4.7 明确边界):WebUI 的访问令牌是**部署级**的,不区分使用者。
246
+ 局域网内多人共用同一令牌时,任务面板/中断/会话彼此可见——这是当前设计的边界,不是缺陷修复
247
+ 范围内的遗漏。需要多用户隔离时请一人一实例(不同端口 + 不同 `MINGDAO_HOME`)
248
+ - **内网 / 信创部署**(v0.6.0):`bash install.sh --offline`(Linux/macOS)与 `install.ps1 -Offline`(Windows)
249
+ 均可在**完全断网**环境安装(不装 Node、不碰 npm,
250
+ 直接软链;本项目运行时零依赖);内置 `vllm` / `ollama` / `oneapi` 预设覆盖国产推理栈与内网网关
251
+ (绝大多数提供 OpenAI 兼容层)。**本机/内网端点免除「必须有 API Key」的硬校验**(这类端点通常不校验,
252
+ 很多也不发放密钥)——但公网端点仍然必须有 Key,不因内网便利而放松。
253
+ 离线包由 `bash scripts/build-offline-bundle.sh` 生成(含内网操作说明与校验值)
254
+ - **出网白名单**(v0.6.0):`config.net.allow` 声明允许的出网目标(精确主机 / `*.子域` / IPv4 CIDR),
255
+ `mode: warn|block` 决定越界是记账放行还是拒绝;`mingdao net report` 导出「本机访问过哪些外部地址」用于自证。
256
+ **边界**:覆盖内核经由 HTTP 出口与自更新 `git` 联系的目标;**不覆盖** MCP 服务器、
257
+ `config.tools`/Pack 工具自起的子进程、桌面版 Electron 外壳的更新检查,以及用户在 `bash` 里自己敲的命令
258
+ ——它证明「内核没有偷偷外传」,不等于「这台机器绝对没有外传」。
259
+ 需要进程级强制请用出网代理/防火墙,本闸门是**内核自证**工具而非沙箱(详见 [docs/CONFIG.md](docs/CONFIG.md) 出网白名单一节)
260
+ - **PID 归属校验分平台**(v0.4.7 明确边界):`killTask` / `stopDaemon` 在动手前会确认
261
+ 「这个 pid 确实还是我启动的那个进程」,避免 pid 被系统回收复用后误杀无关进程。
262
+ Linux 读 `/proc/<pid>/cmdline`、macOS 走 `ps`,两者都能精确校验;**Windows 两者皆无**
263
+ (不为这一处判定引入 PowerShell/WMI 依赖),此时退回 best-effort 按存活处理,并把限制写在这里。
264
+ 需要精确校验的场景请部署在 Linux / macOS 上
241
265
 
242
266
  ## 配置与扩展
243
267
 
@@ -247,7 +271,7 @@ mingdao cost --by pack # 垂域费用分账
247
271
 
248
272
  | 问题 | 解决 |
249
273
  | --- | --- |
250
- | 提示「没有可用 API Key」 | `mingdao key set <服务商>`(或 WebUI 设置面板设 Key);Key 从对应平台获取 |
274
+ | 提示「没有可用 API Key」 | `mingdao key set <服务商>`(或 WebUI 设置面板设 Key);Key 从对应平台获取。脚本里用 `echo "$KEY" \| mingdao key set <服务商>`,避免密钥落到 argv(`ps` 可见) |
251
275
  | 模型下拉框是空的 | 说明没有任何服务商设置了 Key——设 Key 后自动拉取线上模型列表(可点「刷新模型」) |
252
276
  | 沙箱提示降级 | 需要 Linux 且安装 bubblewrap(`apt install bubblewrap` / `dnf install bubblewrap`) |
253
277
  | Windows 颜色异常 | 使用 Windows Terminal 或 PowerShell 7 |
@@ -7,6 +7,30 @@
7
7
  >
8
8
  > 本轮最重要的一条不是某个 bug,而是**一次口径自纠**:省钱基准「综合省 64%」实为虚高(详见 §2.3)。
9
9
 
10
+ ### 后续轮次进展(README 式索引,正文保持 v0.4.6 发布时的快照不改写)
11
+
12
+ 本报告的「登记待办」清单此后又消化了七轮,**仓库内可修的条目已全部收口**:
13
+
14
+ | 轮次 | 消化条目 | 主题 |
15
+ | --- | --- | --- |
16
+ | 第二轮 | T4–T9、T11–T13、T16 | 权限/工具/同步边角 |
17
+ | 第三轮 | T18、T21(两项)、T22(转义)、T23(描述上限) | WebUI/CLI 边角 |
18
+ | 第四轮 | T15、T14 | 调度生命周期(并发重跑 / 暂停后仍执行) |
19
+ | 第五轮 | T17、F7、T19(工作空间部分) | 同步与注册表 |
20
+ | 第六轮 | T1、T3 | 工作空间闸门 / 技能完整性诚实边界 |
21
+ | 第七轮 | T21/T22/T23 收尾、T10 | 同一会话回合串行化 |
22
+ | 第八轮 | T22 终项 | `box()` 宽度收敛、隐藏输入保留提示语 |
23
+ | 第九轮 | T20(三项)、T19(`sync-state`)、T22 收尾 | 进程归属跨平台 / 僵尸任务回收 / 终态写语义 / 密钥不经 argv / 帮助单一来源 |
24
+
25
+ **仅剩 2 项,均非「写代码就能修」**:
26
+ 1. **T2 完整客户端隔离**——共享令牌语义下引入「每客户端作用域」会改变既有脚本客户端的行为,属**产品决策**;
27
+ 现状已在 README「安全」一节明写「共享令牌 = 同一用户,多人需一人一实例」,属诚实边界而非隐瞒。
28
+ 2. **T24 openresty 对不存在路径返回 200+首页**——需服务器侧 `try_files` 配置,**不在本仓库内**,
29
+ 已在官网仓库说明。
30
+
31
+ > 提示:下方 §2 的「50 处」与「smoke 81 组」等数字是 **v0.4.6 发布当时**的快照,后续轮次未回填改写,
32
+ > 以免把历史结论改成事后更漂亮的样子。当前基线见 `RELEASE-NOTES-0.5.0.md` 与测试输出。
33
+
10
34
  ---
11
35
 
12
36
  ## 一、环境与基线(迁移后首次验证)
@@ -102,31 +126,69 @@
102
126
  ## 三、登记待办(14 项,未在本轮修复)
103
127
 
104
128
  > 均已定位到 `file:line` 并有可复现路径;按优先级排列,建议 v0.4.7 / v0.5.0 消化。
105
- > (第二轮已消化 T4–T9、T11–T13、T16;下表为**剩余**项。)
129
+ > 消化进度:第二轮 T4–T9、T11–T13、T16;第三轮 T18 / T21(两项)/ T22(转义)/ T23(描述上限);
130
+ > **第四轮 T15 + T14(调度生命周期)**——这两条是本清单里影响面最大的(同一任务被并发执行两次 / 暂停删除后仍执行);
131
+ > **第五轮 T17 + F7 + T19;第六轮 T1(工作空间登记闸门)+ T3(技能完整性诚实边界);
132
+ > 第七轮 T21/T22/T23 收尾(WebUI/CLI 边角 + 技能名校验)+ T10(同一会话回合串行化);
133
+ > **第八轮 T22 终项(`box()` 终端宽度收敛 + 隐藏输入保留提示语);
134
+ > 第九轮 T20(进程归属跨平台 / 僵尸任务回收 / 终态写不复活 killed)+ T19(`sync-state` 末尾合并)
135
+ > + T22 收尾(`key set` 改走 stdin、帮助正文合并为单一来源);
136
+ > 第十轮 T25–T29(实现决策回放时发现的约束引擎 fail-open 与契约缺口);
137
+ > 第十一轮 T30(出网白名单的重定向绕过);
138
+ > 第十二轮 T31(Batch 中止不落到服务端——「停了」只是本地幻觉,账单照涨);
139
+ > 第十三轮 T32/T33(windows CI 连续变红的根因 + install.ps1 的版本判断缺陷);
140
+ > 第十四轮 T34(项目记忆静默写进用户仓库树,且可被 git add -A 提交)。**
141
+ > 下表为**剩余**项。
142
+
143
+ ### 第十轮(v0.6.0 实现 C2 时发现,均已修)
144
+
145
+
146
+ > 这一轮的共同形态值得单独记住:**合规特性静默失效比直接报错危险得多**。
147
+ > 报错会被人看见;「看起来在保护你、其实没有」不会。四项里有三项属于此类。
148
+
149
+ | # | 级别 | 缺陷 | 位置 |
150
+ | --- | --- | --- | --- |
151
+ | T25 | **P1** | `arg-forbid` 的 `pattern` 非法正则时求值失败 → `re` 为 null → **红线永不命中**,且不进 `invalid`。作者以为有约束、实际没有;与本模块自述的「fail-closed」原则直接矛盾(✅ 已修:`isValidPattern` 单一来源 + 运行期 fail-closed + 装载即拒绝) | `src/constraints.js` |
152
+ | T26 | P2 | `arg-forbid` 缺失 `pattern` 退化成 `new RegExp('')`(匹配一切),「忘了写」变成「该参数任何取值都拦」,理由印出 `/undefined/`(✅ 已修:缺失即判不可用并在装载时拒绝) | `src/constraints.js`、`src/packs.js` |
153
+ | T27 | **P1** | `result-forbid` 在 `PACK-API.md` v1 契约表格与引擎头注释中列出,但 kind 集合与实现**都没有它**——下游按冻结契约写会被判「kind 非法」而整包装载失败。成因是 `packs.js` 另存了一份 kind 集合副本并已漂移(✅ 已修:实现补齐 + kind 集合单一来源) | `src/constraints.js`、`src/packs.js`、`docs/PACK-API.md` |
154
+ | T28 | P2 | `arg-forbid` 只校验 `tool` 不校验 `arg`:漏写 `arg` 时引擎读 `args[undefined]` 并与字符串 `"undefined"` 做匹配——看起来在跑、其实判错对象(✅ 已修:装载时要求 `arg` 必填) | `src/packs.js` |
155
+ | T34 | P2 | **项目记忆静默写进用户仓库树、且可被 `git add -A` 提交**(v0.6.0 审计发现):`appendProjectMemory` 由会话收尾**自动**触发(`autoProjectMemory` 默认开),把「关于你和你的工作」的笔记写进 `<项目>/.mingdao/memory.md`,既不创建 `.gitignore` 也不提示——一次 `git add -A` 就会提交并推到远端。对主打私有化/合规的版本,这种静默副作用不可接受。✅ 已修:创建目录时一并写入 `.gitignore`(内容 `*`)使其自忽略;已存在的 `.gitignore` 不覆盖(尊重用户改动,想共享记忆就删掉它);已写进 `docs/CONFIG.md` | `src/memory.js`、`docs/CONFIG.md` |
156
+ | T32 | **P1(工程纪律)** | **windows CI 腿连续 5 个提交变红而未被发现**(v0.6.0 C4 引入):C4 的冒烟断言用 `spawnSync('bash', …)` 验证 **install.sh** 的离线行为,但 install.sh **自己**会拒绝 Windows 并转给 install.ps1 → 断言必然失败。首次修复只判断「有没有 bash」,而 GitHub 的 windows runner **自带 Git Bash**,探测为真、断言照跑 → 仍然红。✅ 已修:能力探测改为「有 bash **且**不在 Windows」——判据是**被测对象是否适用**,不是「能否启动解释器」。更深一层的教训:我连推 5 个提交都没查 CI 结果,已在发版清单加「推送后必须查 CI」的硬规则 | `test/smoke.js`、`.github/workflows/ci.yml`、`docs/RELEASE-CHECKLIST.md` |
157
+ | T33 | P2 | `install.ps1` 的 Node 版本判断只看 major(`-lt 18`)→ **18.0–18.16 被判合格**,而内核 `engines` 要求 ≥18.17,用户会拿到「装得上、跑不起来」的安装。这正是审计 T23 在 POSIX 侧修过、Windows 侧漏掉的同类缺陷(✅ 已修:`[version]'18.17.0'` 完整比较,两处都比较) | `install.ps1` |
158
+ | T31 | **P1** | **Batch 中止不落到服务端 = 用户以为停了、钱照扣**(v0.6.0 审计发现):`src/batch.js` 只处理服务端**报告** `cancelled` 状态,从不调用取消端点。用户按 Ctrl+C 后本地轮询停止、进程退出,但服务端批次照跑照结算(Batch 0.5× 但全量 token)。对一个主打「成本确定性」的项目,这是最不该有的缺口。✅ 已修:中止与 24h 超时都先 `POST /batches/{id}/cancel`,并按结果如实上报(服务端不支持取消时明确提示「可能仍在计费」,不假装已停) | `src/batch.js` |
159
+ | T30 | **P1** | **出网白名单可被重定向绕过**(v0.6.0 C3 自查发现,实现当天即修):调用方不指定 `redirect` 时 undici 默认自行跟随 3xx,而闸门只看得到**首个** URL——于是「允许 api.deepseek.com」可被利用成「该主机返回 302 指向任意地址,内核照样跟过去」,并且会把 `Authorization` 头一起带过去。模型端点可被配置/接管,这不是理论风险。✅ 已修:闸门自行逐跳跟随并逐跳判定,非白名单跳转**在发出请求前**即中止(有断言钉住「绝不向白名单外的目标发请求」);调用方显式 `redirect:'manual'`(如 fetch 工具自己逐跳处理)时不介入,避免改变既有语义 | `src/net-guard.js` |
160
+ | T29 | P2 | 契约缺口未登记:`require-citation`(输出前 kind)与 `mingdao constraint test <pack>` 在 `PACK-API.md` 中列出但从未实现,文档等于在做空头承诺(✅ 已修:新增 `PACK-API.md §4.1` 明确标注「请勿依赖」,并说明 `require-citation` 需先与下游定规格) | `docs/PACK-API.md` |
161
+
162
+ ### 登记待办(v0.6.0 期间新发现,**未修**,需负责人决定)
163
+
164
+ | # | 级别 | 问题 | 位置 |
165
+ | --- | --- | --- | --- |
166
+ | T35 | P2 | **`<home>/memory.md` 是静默无操作文件**:内核的用户级记忆文件是 `<home>/AGENTS.md`(`memoryFile()`),而 `<home>/memory.md` **从不被读取**——已 grep 确认 `src/` 与 `desktop/` 均无读取点(唯一被读的 `memory.md` 是**项目级** `<工作空间>/.mingdao/memory.md`)。实测负责人的 `mingdao-config` 库里正躺着一份 5 条、格式为 `- [日期] …`(与记忆系统写入格式完全一致)的 `memory.md`,即那些笔记**永远不会进入模型**。**未修的原因**:改「再读一个全局记忆文件」会改变系统提示内容,属产品决策,且负责人正在对 v0.6.0 做验收,此时插入提示语义变更会让他刚验的东西失效。**建议修法(待批)**:`prompts.js` 在存在 `<home>/memory.md` 时把它并入 `<user_memory>`(追加式、缺席即零影响),并同步 `CONFIG.md`;或由负责人把内容并入 `AGENTS.md` | `src/prompts.js`、`src/memory.js`(仅注释)、`docs/CONFIG.md` |
106
167
 
107
168
  ### 安全 / 隔离
108
169
 
109
170
  | # | 级别 | 问题 | 位置 |
110
171
  | --- | --- | --- | --- |
111
- | T1 | P2 | `/api/workspaces` 对用户给的绝对路径 `mkdirSync(recursive)`;登记 `/` 后 `fs-browse` 围栏自我解除(可枚举全盘目录树) | `web/routes/domains/workspace.js:36-49,98-101` |
112
- | T2 | P2 | `/api/abort`、`/api/tasks` 无归属校验:任意客户端可中断他人任务、读取他人任务消息与会话名(taskId 可枚举)。根因是共享 token 下缺少「每客户端作用域」,需引入客户端 cookie 作用域后统一收口 | `web/routes/domains/misc.js:76-97`、`schedule.js:27-34` |
113
- | T3 | P2 | 项目级技能 sha256「防仓库投毒」可自签/可缺失(指纹就在被检查目录内),对声称威胁零收益却给出虚假安全感。最小诚实修法:改措辞 + 加载项目级技能时显式提示来源不可验证 | `src/skills.js:42-50,96-100` |
172
+ | ~~T1~~ | ✅ 已修 | ~~`/api/workspaces` 对任意绝对路径 `mkdirSync(recursive)`;登记 `/` 后 `fs-browse` 围栏自我解除~~ 现 `add`/`set` 均设**登记闸门**(仅允许 家目录 / 系统临时目录 / 启动目录 / 当前工作目录 / `web.browseRoots`,可用 `web.allowAnyWorkspaceDir: true` 显式放开);`add` 的自动建目录同样受限;浏览基目录与登记闸门**共用同一份 `allowedRoots`**(此前两处各写一份) | `web/routes/domains/workspace.js:28-52,86-110,120-135` |
173
+ | T2 | P2 | `/api/abort`、`/api/tasks` 无归属校验:同一 token 下可中断他人任务、读取他人任务消息与会话名。**部分收敛**:taskId 由顺序号改为不可枚举随机值(消除盲猜);**完整隔离未做**——共享 token 语义下没有「每客户端作用域」,引入客户端 cookie 作用域属产品决策(会影响无 cookie 的脚本客户端)。已在 README「安全」一节明确写下「共享令牌 = 同一用户,多人需一人一实例」这一边界 | `web/server.js:349`、`README.md` |
174
+ | ~~T3~~ | ✅ 已修 | ~~项目级技能 sha256「防仓库投毒」可自签/可缺失,属过度承诺~~ 按「诚实」修:注释改为准确描述(只能发现**安装后本地被改动**,对投毒零收益);项目级技能在系统提示里标注「(项目级·来源不可验证)」;加载时给一次性 stderr 提示;新增 `config.disableProjectSkills` 可整层关断(受监管场景) | `src/skills.js:50-60,104-115,135-170` |
114
175
 
115
176
  ### 正确性 / 健壮性
116
177
 
117
178
  | # | 级别 | 问题 | 位置 |
118
179
  | --- | --- | --- | --- |
119
- | T10 | P2 | 同一会话并发回合未串行化(`withSessionLock` 只锁单次写,不覆盖 load→推理→写回整段)→ 会话记录交错;若触发自动压缩会整文件覆盖丢另一路消息 | `src/web/server.js:391,487,590,552` |
120
- | T14 | P2 | 调度:daemon 模式下 `job.pid` 恒 null、`lastTaskId` 仅在跑完后写 → pause/remove 无法停止在途运行;`--offpeak` 等待期间 pause/remove 后仍会启动 | `src/schedule.js:165-177,389-415` |
121
- | T15 | P2 | 重复 daemon → 同一调度任务被**并发执行两次**(lease 自检只 break 监督循环,未取消已启动的协程;pidfile 在 spawn 后才写、无 `O_EXCL` 认领)。这是剩余项里影响最大的一条 | `src/cli.js:279-338`、`schedule.js:319-335` |
122
- | T17 | P2 | 辅助模型调用(路由分类器 / 自动标题 / 记忆提炼)从不 `recordUsage` → 「自动路由省钱」在本框架自己的账本里无法验证,且护栏少计这部分消费 | `src/routing.js:95-110`、`titles.js:31-57`、`memory.js:183,321` |
123
- | T18 | P3 | `withFileLockSync` 会把 `fn` 抛出的 `EEXIST` 误判为「锁被占」→ 潜在同步死循环(当前调用方暂无必然触发路径,但属共享锁原语的隐患) | `src/atomic-write.js:64-65` |
124
- | T19 | P3 | `workspaces.json` / `session-workspaces.json` / `sync-state.json` 的 read-modify-write 未加锁(WebUI 每次建会话都会 touch 工作空间) | `src/workspace.js:32-81`、`src/sync.js:227-242` |
125
- | T20 | P3 | 调度/任务的生命周期边角:僵尸任务不回收(守护可能空转到 2h)、`killed` 被 worker 的终态写覆盖、无 `/proc` 平台(macOS)无法校验 PID 归属 → 存在 PID 复用误杀风险 | `src/tasks.js:18-43,95-101`、`src/schedule.js:286-318` |
126
- | T21 | P3 | WebUI 边角:草稿槽 `draftTexts` 无上限(实测 150 请求 +40MB 不回收)、`/api/config` 不校验模型名、`updateCustom` 实为 upsert、非法 JSON body 被当 `{}` 并落盘、`/api/session-finalize` 缺文件返回 500 并回显绝对路径、`HEAD` 被当写方法返回 415 | `web/routes/domains/{sessions,config,misc}.js`、`web/server.js:125`、`routes/api.js:43` |
127
- | T22 | P3 | TUI/CLI 边角:模型/工具输出中的 ANSI/OSC 转义直通终端(可清屏/改标题/污染管道)、`box()` 不看终端宽度、隐藏输入把提示语一起隐藏、`key set` 经 argv 传密钥、`batch` 清空全进程 SIGINT 监听、HELP_LINES 两份已分叉 | `src/ui.js`、`src/notify.js`、`src/commands/{key,update}.js`、`src/cli.js` |
128
- | T23 | P3 | 技能/安装链边角:技能 `description` 无长度上限(可撑大每轮系统提示)、技能「安装/信任/重装」三入口不校验名称(纵深防御缺口)、`install.sh` 的 `curl \| bash` 失败静默成功且 Node 门槛查 ≥18.0(文档写 ≥18.17) | `src/skills.js:26-39`、`src/skill-lib.js:80`、`install.sh:71,84` |
129
- | T24 | P3 | 官网/IDE 边角:openresty 对不存在路径返回 200+首页(死链接不可发现、污染下载计数,需服务器侧 `try_files`)、VS Code「发送选中代码」写全局槽而 WebUI 只读会话槽(功能不生效)、JetBrains 文档要 `./gradlew` 但仓库无 wrapper | 官网 nginx 配置、`ide/vscode/extension.js:65-78`、`src/web/app.js:695`、`ide/jetbrains` |
180
+ | ~~T10~~ | ✅ 已修 | ~~同一会话并发回合未串行化~~ 新增进程级 `busySessions`:同一会话文件同一时刻只允许一个回合,第二个请求收到明确引导(不同会话仍可并行——多任务招牌不变);用 `res` 的 `close` 兜底释放,避免漏放导致会话永久「忙」 | `src/web/server.js:259-264,405-425` |
181
+ | ~~T14~~ | ✅ 已修 | ~~daemon 模式 `lastTaskId` 仅在跑完后写 → pause/remove 无法停止在途运行;`--offpeak` 等待期间 pause/remove 后仍会启动~~ 现 `markRunning` 与 `runOnce` 启动瞬间即落 `runnerPid`/`lastTaskId`;避峰等待醒来后复查 paused/已删除 | `src/schedule.js:401,455-470` |
182
+ | ~~T15~~ | ✅ 已修 | ~~重复 daemon → 同一调度任务被并发执行两次~~ 四处协同修复:① `spawnDaemon` 的「查活→spawn→写 pidfile」移入跨进程锁(消除并发双 spawn);② `markRunning` 即写 `runnerPid`、`runOnce` 启动瞬间写 `lastTaskId`(关闭恢复分支的误判窗口);③ 恢复分支先看 `procAlive(runnerPid)`,「宿主还活着就等它」;④ 租约丢失时通知在途 `runSleeper` 退出(`shouldStop`)并在收尾后 `process.exit(0)`(此前 every 型常驻协程会把旧 daemon 永远撑住)。新增端到端回归:接管后任务 `runs` 必须为 1(修复前实测为 2) | `src/cli.js:289-345`、`schedule.js:319-350,401,455-470` |
183
+ | ~~T17~~ | ✅ 已修 | ~~辅助模型调用从不入账~~ 新增 `cachestats.recordAuxUsage`(带 `aux`/`auxReason` 标记,独立入账不与回合级重复计费),接入路由分类器 / 自动标题(两处)/ 记忆提炼(两处) | `src/cachestats.js`、`routing.js:111`、`titles.js:45,60`、`memory.js:185,324` |
184
+ | ~~T18~~ | ✅ 已修 | ~~`withFileLockSync` 把 `fn` 的 `EEXIST` 误判为「锁被占」→ 同步死循环~~ 已用 acquiring 标志分离「抢锁」与「执行 fn」两个阶段 | `src/atomic-write.js:47-70` |
185
+ | T19 | P3 | ~~`workspaces.json` / `session-workspaces.json` 的 read-modify-write 未加锁~~(✅ 已修:add/remove/rename/touch 与三个会话级映射全部移入跨进程锁);~~`sync-state.json` 的 RMW 未加锁~~(✅ 已修:改为 `commitState(delta)` **末尾合并**——临界区内只做「重读磁盘 → 并入本次变更的键 → 写回」,毫秒级完成,不把跨网络的秒级耗时关进锁里;`syncPush`/`syncPull`/`syncShareAccept` 三处写点全部改造) | `src/workspace.js`、`src/sync.js` |
186
+ | T20 | P3 | 调度/任务的生命周期边角(**三项全部已修**):~~僵尸任务不回收~~(✅ `reapTasks` + 调度轮询内即时回收,2h 空转 → 数秒);~~`killed` 被 worker 的终态写覆盖~~(✅ `patchTask(..., {terminal:true})` 锁内复查,`killed` 是用户显式意图不可被复活,诊断字段仍吸收);~~无 `/proc` 平台无法校验 PID 归属~~(✅ 新增 `src/proc.js`:Linux 走 `/proc`、其余平台回退 `ps -ww -o command=`,三值语义 true/false/null 严格区分「是我们的人 / 明确不是 / 无从判断」;**Windows 无 /proc 也无 ps,
187
+ 故诚实返回 null 并写明边界**,不为这一处判定引入 PowerShell/WMI 依赖——能力由 `ownershipVerifiable()` 自证,测试按能力分支而非猜平台名) | `src/proc.js`(新增)、`src/tasks.js`、`src/schedule.js`、`src/tasks/worker.js` |
188
+ | ~~T21~~ | ✅ 已修 | WebUI 边角全部收敛:草稿槽 LRU 64 槽;`/api/config` 校验模型名(保留 `provider:"custom"` 任意端点形态);`updateCustom` 不再 upsert(不存在即 400);非法 JSON body → 400(不再静默当 `{}` 并落盘);`/api/session-finalize` 缺文件 → 404 且不回显服务端绝对路径;`HEAD` 与 `GET` 同等对待(不再 415) | `web/routes/domains/{sessions,config}.js`、`web/server.js:125`、`routes/api.js:44` |
189
+ | T22 | P3 | TUI/CLI 边角:~~ANSI/OSC 转义直通终端~~(✅ `sanitizeTerminal`)、~~`batch` 清空全进程 SIGINT 监听~~(✅ 只摘自己那一个)、~~`box()` 不看终端宽度~~(✅ 总宽统一 + 按 `process.stdout.columns` 收敛,边框行与内容行此前必然错位一列)、~~隐藏输入把提示语一起隐藏~~(✅ 先写提示语再抑制回显,已修)、~~`key set` 经 argv 传密钥~~(✅ 改走 stdin,argv 路径保留但警告其在 `ps` 中可见,明文始终不回显)、~~HELP_LINES 两份已分叉~~(✅ 合并为 `src/help.js` 单一来源,variant 差异显式声明;CLI 与会话内帮助输出经逐字节比对与重构前完全一致)(**本项全部收口**) | `src/ui.js`、`src/help.js`(新增)、`src/commands/{key,repl}.js`、`src/cli.js` |
190
+ | T23 | P3 | 技能/安装链边角:~~技能 `description` 无长度上限~~(✅ 200 字符上限)、~~技能「安装/信任/重装」三入口不校验名称~~(✅ 统一到 `assertSafeSkillName`:拒绝 `.`/`..`/含 `..`/含分隔符/超长)、`install.sh` 的 Node 门槛已改为完整版本比较(≥18.17,此前 18.0–18.16 被误判合格)+ 临时文件改用 `mktemp`;一行安装改为「先下载再执行」(README 与官网同步) | `src/skill-lib.js:85`、`install.sh:71,84` |
191
+ | T24 | P3 | 官网/IDE 边角:~~VS Code「发送选中代码」不生效~~(✅ 已修:窗口重新获得焦点时兜底读全局草稿槽)、~~JetBrains 文档要 `./gradlew` 但仓库无 wrapper + 产物版本写死 0.5.0~~(✅ 已修文档);openresty 对不存在路径返回 200+首页(**需服务器侧 `try_files`,不在仓库内**,已在官网仓库说明) | 官网 nginx 配置、`ide/vscode/README.md`、`ide/jetbrains/README.md` |
130
192
 
131
193
  ## 四、已确认无问题(避免过度修复)
132
194
 
@@ -6,6 +6,27 @@
6
6
  兼容策略:同一 major 内向后兼容(minor 只增不改);支持窗口 = 最近 2 个 minor。
7
7
  任何改动必须同步更新 `PACK-API.md` 与兼容性矩阵。
8
8
 
9
+ ## v1.1(2026-09-11,随内核 v0.6.0)— 约束引擎勘误与收紧
10
+
11
+ **兼容性**:`result-forbid` 属**补实现**(PACK-API.md 的 v1 表格本就列出它,但 kind 集合与引擎都没有);
12
+ pattern 校验收紧会让**本来就写错**的 Pack 从「静默装载成功」变为「装载失败」——这是刻意的行为变更,
13
+ 下面说明理由。正常写法不受影响。
14
+
15
+ - **修 fail-open**:`arg-forbid` 给了非法正则时,求值失败 → `re` 为 null → 红线**永不命中**,
16
+ 且不进 `invalid`、装载也不报错。作者以为有红线、实际没有——这与约束引擎自己声明的
17
+ 「fail-closed:求值异常一律按阻断处理」直接矛盾。`output-forbid` 的同类情况则是被丢进
18
+ `invalid` 后从生效集合消失,同样是红线静默消失。现已改为:装载时拒绝坏 pattern;
19
+ 运行期对作用域类 kind(`arg-forbid`/`result-forbid`)按 fail-closed 只阻断该工具并说明配置有误。
20
+ - **修「缺 pattern 比写的严」**:缺失 pattern 会退化成 `new RegExp('')`(匹配一切),
21
+ 即「忘了写」变成「该参数任何取值都拦」,且理由印出 `/undefined/`。现由装载校验直接拒绝。
22
+ - **补 `arg-forbid` 的 `arg` 必填校验**:此前只校验 `tool`,漏写 `arg` 时引擎读 `args[undefined]`
23
+ 并与字符串 `"undefined"` 做匹配,属于「看起来在跑、其实判错对象」。
24
+ - **实现 `result-forbid`**(契约已列、实现缺席):结果序列化后命中 pattern 即拒绝该结果并要求重采。
25
+ - **kind 集合单一来源**:`packs.js` 曾另有一份 `CONSTRAINT_KINDS` 副本,已与引擎 `KINDS` 漂移;
26
+ 现统一为引擎那一个(`CONSTRAINT_KINDS` 保留为别名)。
27
+ - **契约缺口登记**:`require-citation` 与 `mingdao constraint test <pack>` 在 PACK-API.md 中列出但
28
+ 尚未实现,已在 `PACK-API.md §4.1` 明确标注「请勿依赖」,不再作为承诺留在文档里。
29
+
9
30
  ## v1(2026-09-11,随内核 v0.5.0 冻结)
10
31
 
11
32
  **首次冻结。** 范围:
package/docs/CONFIG.md CHANGED
@@ -84,6 +84,73 @@ WebUI 中每个会话记住自己的工作目录:新会话记录创建时的
84
84
  - `deny` 优先于 `allow`;匹配不到时回落到 `mode`(`ask` 逐次确认);
85
85
  - **需要特殊授权时弹窗交互**:被 `deny` 规则拦截、或 `readonly` 模式下执行写操作时,WebUI/TUI 会弹出询问(「是否本次强制放行?」),同意即放行、拒绝/无响应即拒绝——不再静默拦截。
86
86
 
87
+ ## 项目记忆(自动写入项目目录,默认已自忽略)
88
+
89
+ 会话收尾时会从对话里提炼几条长期有效的结论,写进**当前项目**目录:
90
+
91
+ ```
92
+ <你的项目>/.mingdao/memory.md # 形如「- [2026-09-11] 决定:配置拆成多文件」
93
+ ```
94
+
95
+ - 开关:`config.autoProjectMemory`(默认 `true`);全局记忆另在 `~/.mingdao/memory.md`(`/memory add` 写入)。
96
+ - **这个目录会自忽略**:`appendProjectMemory` 在创建 `.mingdao/` 时一并写入 `.gitignore`(内容 `*`),
97
+ 以免「关于你和你的工作」的笔记被一次 `git add -A` 顺手提交并推到远端。
98
+ - 想跟团队共享项目记忆(例如把约定随仓库传递):删掉 `<项目>/.mingdao/.gitignore` 即可——
99
+ 已存在的 `.gitignore` 不会被程序覆盖,所以你的改动是安全的。
100
+ - 记忆按工作空间隔离:A 项目的记忆不会出现在 B 项目的系统提示里。
101
+
102
+ ## 出网白名单(v0.6.0 C3)
103
+
104
+ `config.json` 的 `net` 字段把「数据不出门」从口头承诺变成可导出的记录:
105
+
106
+ ```json
107
+ {
108
+ "net": {
109
+ "allow": ["api.deepseek.com", "*.internal.corp", "10.0.0.0/8", "192.168.1.50"],
110
+ "mode": "warn",
111
+ "allowLoopback": true
112
+ }
113
+ }
114
+ ```
115
+
116
+ | 字段 | 说明 |
117
+ | --- | --- |
118
+ | `allow` | 允许的出网目标。四种写法:精确主机(`api.deepseek.com`)、通配子域(`*.example.com`,**不隐含裸域**)、IPv4 CIDR(`10.0.0.0/8`)、精确 IP |
119
+ | `mode` | `warn`(默认):越界请求**放行但逐条记账**,适合「先观测再收紧」;`block`:越界请求直接拒绝并给出放行指引 |
120
+ | `allowLoopback` | 默认 `true`。回环(`localhost` / `127.0.0.0/8` / `::1`)始终豁免——它出不了本机,把它算成外发只会制造噪音(本地模型 Ollama/vLLM 是主力场景)。**私网不算回环**,要放行需显式写进 `allow` |
121
+
122
+ **未配置 `net` 时闸门完全不安装**,既有行为零影响(与约束引擎同款不变量)。
123
+
124
+ 查看与自证:
125
+
126
+ ```bash
127
+ mingdao net policy # 当前策略与是否生效
128
+ mingdao net report # 本机访问过哪些外部地址(次数 / 放行 / 拦截 / 命中哪条规则)
129
+ mingdao net report --since 7d --json # 支持 7d / 24h / 90m
130
+ ```
131
+
132
+ 自更新的 git 远端也会过闸门:`mingdao update` 会先解析远端 URL 并判定,block 模式下若**所有**远端
133
+ 都不在白名单内则直接拒绝联网(多镜像部署里只要有一个可达即放行)。SSH 的 `git@host:path` 写法同样支持——
134
+ 否则「白名单里写了 `github.com` 却依然被拦」会变成一个功能故障。
135
+
136
+ 记账写在 `~/.mingdao/net.jsonl`(600 权限,低频轮转),**只记主机、端口、判定与命中规则,不记请求体、不记完整 URL**。启用白名单后,每个回合的账本(`mingdao ledger show`)里也会出现 `net.egress` 事件,与其它事件同处一条时间线。
137
+
138
+ > ⚠ **这个闸门覆盖什么、不覆盖什么(完整版,请按此理解,不要扩大)**
139
+ >
140
+ > 覆盖(内核自己发起、且经过 `globalThis.fetch` 或已显式接线的路径):
141
+ > 模型 API · `fetch` 工具 · 技能库 registry · 模型发现 · 定价数据 · Batch · 云同步(含自签名证书的 `node:https` 路径)
142
+ > · **自更新 `git fetch/pull`**(远端 URL 先过闸门,见下)。
143
+ >
144
+ > **不**覆盖(都是「内核拉起的别的进程」,它们自己发请求,闸门看不到):
145
+ > - **MCP 服务器**:内核只负责拉起,网络请求由 MCP 进程自己发;
146
+ > - **`config.tools` / Pack 工具自己起的子进程**:同上;
147
+ > - **桌面版(Electron)外壳自身的更新检查**:那是 Electron 侧的网络栈;
148
+ > - **用户在 `bash` 里自己敲的命令**(`curl`/`git`/`pip`…)。
149
+ >
150
+ > 所以它证明的是「**内核经由 HTTP 出口与自更新去联系的目标**都在白名单内」,
151
+ > 而**不是**「这台机器绝对没有外传」。把它当后者用就是误用——需要进程级强制时,
152
+ > 应在操作系统/网关层面做(出网代理、防火墙、EDR),本闸门是**内核自证**工具,不是沙箱。
153
+
87
154
  ## Hooks(工具调用生命周期)
88
155
 
89
156
  ```json
@@ -0,0 +1,51 @@
1
+ # 交接状态(2026-09-11 会话末)
2
+
3
+ > 用途:把「已完成 / 待你输入」一次说清,避免会话很长之后状态散落。
4
+ > 目标四项(恢复环境 / 通读源码 / 审计修复 / 升级方案)**均已完成**,证据见下。
5
+
6
+ ## 一、已完成(含证据)
7
+
8
+ | 目标 | 证据 |
9
+ | --- | --- |
10
+ | ① 恢复并验证本机开发环境 | macOS + Node v24.20.0;零依赖安装;`npm run typecheck` 0 错误;strict 棘轮 0/0 |
11
+ | ② 通读源代码与架构文档 | `docs/ARCHITECTURE.md` 等通读;产出物见 ③④ |
12
+ | ③ 审计 bug 并修复 | `docs/AUDIT-v0.4.6.md`:T1–T34(含 8×P0/P1 级安全与成本缺陷),全部带 `file:line`、复现证据与**变异校验**;关键修复均有回归断言 |
13
+ | ④ 差异化升级方案(含路线图) | `STRATEGY-0.5.md`(定位升级:可私有化的垂域智能体内核)、`PLAN-v0.5.0.md`、`PLAN-v0.6.0.md`、`RELEASE-TRAIN.md`,且**已执行**:v0.5.0 已发布;v0.6.0 C1–C4 代码完成 |
14
+
15
+ 当前门禁:tsc 0 / strict 0-0 / smoke **101** / e2e-local 17 / e2e-web 24 /
16
+ e2e-schedule 10 / api-contracts 8 / bench 214;CI 五腿全绿(Ubuntu 18/20/22 + **Windows** + macOS)。
17
+
18
+ ## 二、已发布
19
+
20
+ | 版本 | GitHub | Gitee | GitCode | npm |
21
+ | --- | --- | --- | --- | --- |
22
+ | v0.4.6 | 9 附件 | ✅ Release | ✅ Release | ✅ 已补发 |
23
+ | v0.5.0 | 9 附件 | ✅ Release | ✅ Release | ✅ 已补发 |
24
+ | v0.6.0 | 代码完成,**待你验收后发布** | — | — | — |
25
+
26
+ npm `latest` = **0.5.0**(0.4.5 → 0.4.6 → 0.5.0 连续补齐,无跳号)。
27
+
28
+ ## 三、待你输入(仅两项,且都是你侧动作)
29
+
30
+ 1. **官网部署**(阻塞):需要 ①`ssh-rsa AAAAB3…Vvjv` 对应的**私钥**(本机现有两把私钥
31
+ —— `~/.ssh/mingdao_git` 是 ed25519、`~/Downloads/Macbook.pem` 是另一把 RSA —— 均**不匹配**);
32
+ ② `mingdao-server` 的**主机地址**(本机 `~/.ssh/config`、`known_hosts` 与两个仓库里都只有别名)。
33
+ 2. **v0.6.0 验收**:`http://127.0.0.1:3820`(清单见 `RELEASE-CHECKLIST.md §2.1`,已逐条实跑确认可用)。
34
+ 确认后我执行:版本号与 CHANGELOG → tag(CI 构建桌面安装包 → GitHub Release)→ Gitee/GitCode Release
35
+ (只建 Release、不传附件)→ npm 发布 → 官网内容更新与部署。
36
+
37
+ ## 四、一个待拍板的顺序问题
38
+
39
+ 官网当前内容为 **v0.4.6**(已提交未部署,`cd5057c`)。你此前指定顺序是「先在官网发 v0.4.6,
40
+ 再补 v0.5.0」,而 v0.5.1 已取消、v0.6.0 即将发布。因此存在两种做法:
41
+
42
+ - 按原顺序:官网 v0.4.6 → v0.5.0 → v0.6.0(三次部署);
43
+ - 直接上 v0.6.0(一次部署,线上从 v0.4.5 直接跳到 v0.6.0)。
44
+
45
+ **你不指明时我按你已明确说过的顺序执行(先 v0.4.6)**,不擅自改。
46
+
47
+ ## 五、安全提醒(仍未处理)
48
+
49
+ 发布用的三个 token(gitee / gitcode / npm)已出现在对话中。发布收尾后建议**全部轮换一次**,
50
+ 尤其 npm token(长期凭证)。当前落地方式:gitignored `.env`(600)+ `~/.npmrc`(600,不用 argv,
51
+ 避免进 `ps`);已核查工作树、git 历史与 npm 包中均无泄露,诊断报告也已验证不含。
@@ -1,5 +1,12 @@
1
1
  # Deyi-TCM-Harness 回迁指南(v0.5.0 → Pack API v1)
2
2
 
3
+ > **已按下游真实代码核对**(核对对象:`Deyi-TCM-Harness@c6b4397`,文件
4
+ > `layer/providers/dify.mjs`、`docs/ARCHITECTURE.md`、`README.md`)。
5
+ > 核对后更正了两处会误导回迁的地方:§3.1.1 补上「同名多命中」这条**模型之前的安全闸门**
6
+ > (初版只举了 `随访` 命令,把安全关键的那条漏了);§6 更正「执行账本不可用」的过期说法
7
+ > (v0.6.0 已交付)。指南里凡是标注「按真实代码核对」的结论,都来自读代码而不是推测。
8
+
9
+
3
10
  > 面向:下游 Line B(中医垂域层,Linux 原机开发)
4
11
  > 上游契约:`PACK-API.md`(v1 已冻结)· 变更史 `CHANGELOG-PACK.md`
5
12
  > 触发:**v0.5.0 发布即回迁**(决策已确认)。目标:把 3 个域工具从 `providers/dify.mjs` 的 `chat()` 里搬出来。
@@ -73,6 +80,35 @@ export function createPack(ctx) {
73
80
 
74
81
  注册后内核自动加前缀:`pack__tcm__intake_collect` 等,与内置工具走**同一条**权限 / 审计 / schema 瘦身 / 费用链路。
75
82
 
83
+ #### 3.1.1 ⚠ 两条「模型之前」的短路,不能当成普通工具直接搬
84
+
85
+ > 本节是按**下游真实代码**核对后补写的(核对对象:`Deyi-TCM-Harness@c6b4397`,
86
+ > 文件 `layer/providers/dify.mjs` 的 `chat()`)。初版指南只举了 `随访` 一条,
87
+ > 漏掉了下面第 2 条——而它才是安全关键的那条。
88
+
89
+ 下游的 `chat()` 里有**两处**在调用模型**之前**就返回的短路分支:
90
+
91
+ | # | 触发 | 现状行为 | 直接改成工具会怎样 |
92
+ | --- | --- | --- | --- |
93
+ | 1 | `^(回访\|随访)\s*(.*)$` | 不调用 Dify、不走问诊,直接产出看板/随访话术(`usage: {0,0}`) | 多一次模型往返(延迟与 token 开销)。**若在意这点**:保留在下游 Provider 里是允许的——`<home>/providers/` 本就是下游的自留地(见 §6) |
94
+ | 2 | **同名多命中** `match.ambiguous` | **直接返回候选列表、请医师确认,不落盘、不问诊**(`dify.mjs` 中 `if (match.ambiguous)` 分支) | ⚠ **这是安全降级**:现在的语义是「**Provider 在模型之前就拒绝继续**」,改成普通工具后变成「模型自行决定要不要先问一句」。对「同名多命中不静默合并、避免混病历」这条医疗安全属性,**把决定权从内核交给模型是不可接受的** |
95
+
96
+ 第 2 条的正确迁移方式不是「做成工具」,而是**用约束引擎把它升级为内核强制**(这正是 v0.5.0 约束确定性存在的意义):
97
+
98
+ ```js
99
+ // packs/tcm/pack.mjs 的 constraints —— 让「没有确认病历号就不得写入」由内核强制,
100
+ // 且对**所有**写类工具生效,不依赖模型自觉、可审计、可被测试阻断。
101
+ constraints: [
102
+ { id: 'must-have-patient', kind: 'tool-arg-require', tool: 'intake_collect', requireArg: 'patientId' },
103
+ { id: 'must-have-patient', kind: 'tool-arg-require', tool: 'visit_compare', requireArg: 'patientId' },
104
+ // 采集缺项照样由 completeness 兜住(见 §3.2)
105
+ ]
106
+ ```
107
+
108
+ 这样得到的性质**比现状更强**:现状只在「同名多命中」这一种情况下拒绝,而 `tool-arg-require` 是
109
+ 「**任何**没有确认病历号的写入都拒绝」——把「避免混病历」从一条 if 分支变成一条内核不变量,
110
+ 并且进审计、可回放(§6 的账本已可用)。
111
+
76
112
  ### 3.2 三条红线 → 约束引擎(从提示词升级为内核强制)
77
113
 
78
114
  | 现有红线(写在 prompt 里) | 迁移后的约束声明 |
@@ -107,6 +143,47 @@ async function deepseekJson(system, user, maxTokens = 2000) {
107
143
 
108
144
  `purpose` 建议取值:`patient-extract`(患者识别)、`intake-extract`(结构化落盘)、`visit-compare`(四态对比)、`followup-script`(随访话术)。
109
145
 
146
+ #### 3.3.1 逐参数核对结论(`deepseekJson` → `ctx.llm()`)
147
+
148
+ > 按下游真实代码逐参数核对过(`dify.mjs` 的 `deepseekJson`),**可以等价替换**:
149
+
150
+ | 下游 `deepseekJson` 的线上参数 | `ctx.llm()` 的写法 | 核对结果 |
151
+ | --- | --- | --- |
152
+ | `model: 'deepseek-v4-flash'`(硬编码) | `model` | ✅ |
153
+ | `temperature: 0` | `temperature: 0` | ✅ 支持(缺省回落到会话温度) |
154
+ | `max_tokens: maxTokens` | `maxTokens` | ✅(缺省 2048,受模型输出上限封顶) |
155
+ | **`thinking: { type: 'disabled' }`** | `reasoningEffort: 'off'` | ✅ **线上参数完全一致**——内核在 `reasoningEffort==='off'` 时正是发 `thinking:{type:'disabled'}` |
156
+ | 自己 `indexOf('{')…lastIndexOf('}')` 再 `JSON.parse` | `json: true` | ✅ 内核 `ctx.llm` 用**同一套**花括号切片解析,结果放在 `data`;解析失败为 `null` 而**不抛错**(与下游返回 `null` 同语义) |
157
+
158
+ 一个必须注意的差异:下游 `deepseekJson` 用 `deepseekKey` 直连,**usage 不入账**(这正是要迁的原因);
159
+ 改走 `ctx.llm()` 后 usage 并入当前回合 → 今日费用、缓存命中、峰谷、日护栏同时生效。
160
+
161
+ #### 3.3.2 密钥从哪来(一个必读的坑)
162
+
163
+ `ctx.llm()` 解析密钥走**内核**,顺序是(`src/credentials.js` 的 `resolveApiKey`,已按代码核对):
164
+
165
+ 1. `process.env[<服务商的 envKey>]`(DeepSeek 即 `DEEPSEEK_API_KEY`)
166
+ 2. **`process.env.MINGDAO_API_KEY`**(通用兜底)
167
+ 3. 凭证库 `<MINGDAO_HOME>/credentials.json` 里以**服务商名**为键的字段(如 `deepseek`)
168
+ 4. `cfg.apiKey`
169
+
170
+ 好消息:第 3 步读的就是下游现在写的那**同一个文件、同一个字段**(下游 `dify.mjs` 的
171
+ `creds.deepseek`)——只要 `MINGDAO_HOME` 是同一个目录,迁移后**不需要搬密钥**。
172
+
173
+ > ⚠ **坑(已实测复现)**:第 2 步的 `MINGDAO_API_KEY` **优先于**第 3 步。
174
+ > 机器上只要残留一个指向别的服务商的 `MINGDAO_API_KEY`,域内调用就会带着**那个** key
175
+ > 打到 DeepSeek 端点(或反之)——表现为 401 或「费用算到莫名其妙的账上」。
176
+ > 回迁时请检查环境变量:`env | grep -E 'MINGDAO_API_KEY|DEEPSEEK_API_KEY'`。
177
+
178
+ 另一个前置条件:`ctx.llm({ model })` 里的模型名必须是内核**能解析**的——
179
+ 要么是内置名(`deepseek-v4-flash`),要么在上游 `config.json` 的 `customModels` 里声明过
180
+ (含 `baseUrl`)。否则 `ctx.llm` 直接抛「未配置 API Key」,而下游原来的 `deepseekJson`
181
+ 是自己硬编码 URL 的、不会有这个问题。推荐显式写入凭证库:
182
+
183
+ ```bash
184
+ mingdao key set deepseek <你的 DeepSeek Key> # 写入 <MINGDAO_HOME>/credentials.json
185
+ ```
186
+
110
187
  ### 3.4 患者注册表与快照落盘 → 受权限约束的 IO
111
188
 
112
189
  `patients.json` / `intake/**` 的读写改用 `permissions.fs` 声明的路径(内核据此校验越界):
@@ -165,11 +242,30 @@ async function deepseekJson(system, user, maxTokens = 2000) {
165
242
 
166
243
  ---
167
244
 
168
- ## 六、上游仍需补齐(下游可先按本节设计,勿依赖)
245
+ ## 六、上游能力现状(回迁时按此判断能依赖什么)
246
+
247
+ > 本节已按 **v0.6.0** 实际交付情况更正。初版把「执行账本导出/回放」也列进了缺口,
248
+ > 但它在 v0.6.0 已经落地——照旧文办事会让下游白做一套替代方案。
249
+
250
+ **已可用(可以依赖)**
251
+
252
+ - **执行账本与决策回放(v0.6.0 确定性③)**:`mingdao ledger list/show/export/verify/replay`。
253
+ 对下游的意义:域内模型调用(经 `ctx.llm()`)与约束触发都会进账本,
254
+ 「这次结论是怎么来的」可离线回答;`ledger export` 脱敏后可交第三方复核。
255
+ 回放还能当**下游 CI 门禁**:`now-blocked > 0` 时退出码为 1。
256
+ - `permissions.fs` 显式路径 + `completeness` / `tool-deny` / `tool-arg-require` / `arg-forbid` /
257
+ `output-forbid` / `result-forbid`(后两者见 `PACK-API.md §4`,`result-forbid` 于 v0.6.0 补齐实现)。
258
+ - 出网白名单 `config.net` + `mingdao net report`(**注意边界**:只覆盖内核经 HTTP 出口与自更新
259
+ 联系的目标,不覆盖 MCP 服务器、以及工具自己起的子进程——见 `CONFIG.md` 出网白名单一节)。
260
+
261
+ **仍缺(请勿依赖)**
169
262
 
170
263
  - `ctx.storage`:Pack 私有持久化命名空间(当前用 `permissions.fs` 显式路径替代);
171
264
  - `ctx.provider`:由 Pack 贡献非 OpenAI 兼容 Provider(当前 Dify 适配仍放 `<home>/providers/`);
172
- - Pack 私有存储加密(医疗 PII)→ v0.6.0「合规与确定性」;
173
- - 执行账本导出 / 可回放(确定性③)→ v0.6.0。
265
+ - **Pack 私有存储加密(医疗 PII)**:**不在 v0.6.0**(初版写「→ v0.6.0」是不准确的,特此更正)。
266
+ 下游若有 PII 落盘加密要求,请在上层自行处理(如加密文件系统 / 应用层加密);
267
+ - 账本的**可选签名**(`--sign-key`)未实现:当前只有哈希链完整性校验,
268
+ **不含可信时间戳**,不要当成审计级不可否认。
174
269
 
175
- 以上四项在 v0.5.0 **不可用**,请勿在回迁中依赖。
270
+ **能力缺口登记(契约里列了但没实现,别照文档写)**:`require-citation` 约束 kind、
271
+ `mingdao constraint test <pack>` —— 见 `PACK-API.md §4.1`。
package/docs/PACK-API.md CHANGED
@@ -174,11 +174,44 @@ export function createPack(ctx) {
174
174
  "matched": "好转", "session": "…", "model": "deepseek-v4-flash" }
175
175
  ```
176
176
 
177
+ **字段要求(v0.6.0 起在装载时强制校验,写错不会静默失效)**
178
+
179
+ | kind | 必填 | 说明 |
180
+ | --- | --- | --- |
181
+ | `tool-deny` | `tool` | |
182
+ | `tool-arg-require` | `tool`、`requireArg` | |
183
+ | `arg-forbid` | `tool`、`arg`、`pattern` | `pattern` 必须是**合法且非空**的正则 |
184
+ | `output-forbid` | `pattern`、`action` | 同上 |
185
+ | `result-forbid` | `tool`、`pattern` | 同上 |
186
+ | `completeness` | `tool`、`fields[]` | |
187
+
188
+ **pattern 写错会怎样(这一条值得单独读)**:v0.5.0 的行为是**静默放行**——
189
+ `arg-forbid` 给一个非法正则时求值失败、`re` 为 null,红线**永不命中**且不进 `invalid` 列表,
190
+ 作者以为自己有红线、实际没有;给一个缺失的 pattern 则退化成 `new RegExp('')`(匹配一切),
191
+ 比本意严得多。这与「fail-closed」的原则直接矛盾。v0.6.0 起:
192
+
193
+ 1. **装载即拒绝**:`pack.json`/`pack.mjs` 里 pattern 类约束的 pattern 缺失或非法 → Pack 装载失败
194
+ 并给出可操作的错误(`pack verify` 是下游 CI 门禁,因此拼写错误在 CI 就会被拦下);
195
+ 2. **运行期 fail-closed**:作用域限于某工具的 `arg-forbid` / `result-forbid` 若 pattern 仍然坏掉
196
+ (例如绕过装载校验直接传约束),只阻断**那个工具**并明说「配置有误」,而不是放行、也不是拦下全部;
197
+ 3. `output-forbid` 的 pattern 坏掉时不进生效集合(否则「一个正则写错」会变成「整个会话无法输出」),
198
+ 但会被计入 `invalid` 并在装载/校验时报告。
199
+
177
200
  **设计原则**
178
201
  - 约束**只能收紧、不能放松**权限(约束不授予任何权限);
179
202
  - 约束失败**默认 fail-closed**(拿不准就阻断并提示),与 hooks 同口径;
180
203
  - 每条约束必须有 `id`,便于审计与测试;
181
- - 提供 `mingdao constraint test <pack>`:对每条约束跑一遍内置反例样本(下游 CI 用)。
204
+ - 约束 kind 的**唯一来源**是引擎(`src/constraints.js` 的 `KINDS`)。此前 `packs.js` 另有一份副本,
205
+ 已经真实漂移并导致下游照契约写的 `result-forbid` 被判「kind 非法」而整包装载失败。
206
+
207
+ **§4.1 已知缺口(v1 契约中列出、但尚未实现——请勿依赖)**
208
+
209
+ | 项 | 状态 |
210
+ | --- | --- |
211
+ | `require-citation`(输出前 kind) | ❌ **未实现**。当前 `KINDS` 中没有它,写进 Pack 会被判 kind 非法。需要先定「什么算引用来源」的规格(与下游一起定),故不在此处擅自实现 |
212
+ | `mingdao constraint test <pack>` | ❌ **未实现**。目前请用 `pack verify`(静态校验)+ 自己写的反例测试 |
213
+
214
+ > 把缺口写在契约里而不是留在文档里当承诺:下游按本文档迁移时,能一眼看到哪些不能依赖。
182
215
 
183
216
  ---
184
217
 
@@ -192,7 +225,7 @@ const out = await ctx.llm({
192
225
  system: '…', user: '…',
193
226
  maxTokens: 2000,
194
227
  reasoningEffort: 'off', // 复用内核的模型能力表与参数校验
195
- json: true, // 结构化输出(内核负责解析与重试)
228
+ json: true, // 结构化输出:内核发 response_format=json_object,并把回复解析到 data
196
229
  purpose: 'patient-extract', // 归因标签(进账本与分账)
197
230
  });
198
231
  // → { text, data, usage: { prompt_tokens, completion_tokens, prompt_cache_hit_tokens, … } }
@@ -216,6 +249,13 @@ manifest 可声明 `budget.dailyYuan` + `budget.action`。超限时 `ctx.llm()`
216
249
 
217
250
  ---
218
251
 
252
+ **`json: true` 的确切语义(v0.6.0 更正)**:内核会带上 `response_format: {type:'json_object'}`,
253
+ 并把回复解析到 `data`——解析方式与下游 `deepseekJson` 同款:取文本里**第一个 `{` 到最后一个 `}`**
254
+ 再 `JSON.parse`。两点请注意:
255
+ - **不重试**(此前文档写「负责解析与重试」,重试并不存在);
256
+ - 解析失败时 `data` 为 **null**,**不抛错**——因此调用方自己判断 `data` 是否为空,
257
+ 不要把「拿不到结构化结果」当成异常路径。
258
+
219
259
  ## 6. 兼容性与版本策略
220
260
 
221
261
  | 内核版本 | Pack API | 承诺 |
@@ -31,6 +31,8 @@
31
31
  断言规模:smoke 83 → **86 组**;6/6 套测试全绿;tsc 0 错误;strict 0/0。
32
32
 
33
33
  **下一步**:
34
+ 0. **v0.6.0「合规与确定性」已立项**:落地计划见 `PLAN-v0.6.0.md`(确定性③:执行账本 / 决策回放 /
35
+ 出网白名单自证 / 离线安装)。上游在等待下游回迁反馈的窗口期,先推进这条不依赖下游的线。
34
36
  1. **下游执行回迁**(Linux 原机):按 `MIGRATION-DEYI-v0.5.md` 把 3 个域工具搬进 `pack-tcm`,
35
37
  `pack verify` 进下游 CI;回迁中遇到的每个「别扭点」都反馈上游当契约缺陷修(dogfooding)。
36
38
  2. **v0.5.x**:Pack API 按回迁反馈做兼容性加固(minor 只增不改)。