mingdao-harness 0.6.2 → 0.6.4
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/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 +300 -32
- package/docs/CONFIG.md +32 -6
- package/docs/DEVELOPER.md +1 -1
- package/docs/PACK-API.md +26 -3
- package/docs/PROVIDERS.md +22 -0
- package/docs/RELEASE-CHECKLIST.md +13 -2
- package/package.json +1 -1
- package/src/agent.js +133 -15
- package/src/atomic-write.js +247 -71
- package/src/audit.js +23 -18
- package/src/cachestats.js +57 -47
- package/src/cli.js +25 -4
- package/src/commands/diagnose.js +3 -0
- package/src/commands/key.js +23 -1
- package/src/commands/ledger.js +41 -3
- package/src/commands/net.js +5 -1
- package/src/commands/pack.js +23 -6
- package/src/commands/repl.js +9 -1
- package/src/commands/skill.js +30 -1
- package/src/commands/sync.js +7 -0
- package/src/commands/workspace.js +3 -3
- package/src/compact.js +15 -1
- package/src/config.js +71 -6
- package/src/constraints.js +55 -6
- package/src/credentials.js +36 -5
- package/src/hooks.js +37 -2
- package/src/ledger.js +3 -3
- package/src/log-writer.js +22 -17
- package/src/mcp.js +19 -1
- package/src/memory.js +8 -8
- package/src/packs.js +57 -8
- package/src/permissions.js +35 -9
- package/src/presets.js +46 -1
- package/src/providers/index.js +133 -7
- package/src/providers/openai-compatible.js +24 -3
- package/src/redact.js +38 -2
- package/src/replay.js +15 -3
- package/src/safe-fetch.js +8 -2
- package/src/schedule.js +22 -4
- package/src/session-index.js +2 -2
- package/src/session.js +2 -2
- package/src/skill-lib.js +43 -14
- package/src/skill-registry.js +31 -2
- package/src/sync-server.js +47 -28
- package/src/sync.js +17 -3
- package/src/task-state.js +10 -1
- package/src/tasks.js +22 -10
- package/src/tools/bash.js +96 -18
- package/src/tools/fetch.js +24 -0
- package/src/web/app.js +7 -7
- package/src/web/attachments.js +7 -1
- package/src/web/index.html +2 -2
- package/src/web/routes/api.js +27 -2
- package/src/web/routes/domains/sessions.js +3 -3
- package/src/web/routes/domains/sync.js +2 -1
- package/src/web/routes/domains/workspace.js +45 -15
- package/src/web/server.js +22 -5
- package/src/workspace.js +31 -18
|
@@ -520,6 +520,231 @@ Windows 对这种情况回报的正是 **ENOENT**,与"本来就没有检查点
|
|
|
520
520
|
> 补上「冷却过后**不做任何重置**也必须自动恢复」这条断言后,该突变才被拦下。
|
|
521
521
|
> **断言要落在"这条修复独有的行为"上**,否则测的是别的东西。
|
|
522
522
|
|
|
523
|
+
## 3.26 已修复(第二十七批:文件锁的**阻塞面**——`Atomics.wait` 冻结事件循环)
|
|
524
|
+
|
|
525
|
+
自评报告 P2-7 拆成两半:**正确性**(死区、误抢活锁)在 §3.10 已修;本轮修的是**阻塞面**。
|
|
526
|
+
|
|
527
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
528
|
+
| --- | --- | --- | --- |
|
|
529
|
+
| P2-7(阻塞面) | `src/atomic-write.js#withFileLockSync` | **实测复现并量化**:同步锁用 `Atomics.wait` 睡眠,等待期间**整个事件循环停摆**。持锁方存活 2.6 秒时,一个 100ms 的定时器在锁返回前**根本没触发**。对常驻 WebUI 而言,一次文件锁争用就冻结所有并发会话/权限确认/SSE 流 | ① 新增**异步版 `withFileLock`**(等待时 `await` 让出事件循环,语义与同步版完全一致:同一套 `tryAcquire` / 陈旧回收 / 可重入 / 超时与文案,**抽成共用逻辑以免两条路径漂移**);② 把**正好在 WebUI 请求路径上**的 `workspace.js` 全部 7 处锁迁到异步版(11 个 src 调用点 + 测试调用点补 `await`);③ 同步版保留给纯同步调用链,理由见下 |
|
|
530
|
+
|
|
531
|
+
### 迁移时发现的**必须先修的隐患**:可重入判据是进程级 Set
|
|
532
|
+
原实现用一个**进程级** `Set` 判"我是否已持有该锁"(可重入)。同步临界区不会 yield,所以一直没出事;
|
|
533
|
+
但**异步临界区会 yield**——此时另一个任务拿同一把锁会被**误判成可重入**而并发进入临界区,
|
|
534
|
+
读-改-写互相覆盖(丢失更新)。已改用 `AsyncLocalStorage` 把可重入限定在**同一条调用链**内:
|
|
535
|
+
并发任务各持各的集合,嵌套调用复用同一份。这是"能安全异步化"的前提,单独写了断言钉住。
|
|
536
|
+
|
|
537
|
+
> 顺带记一个**比预期严重**的失败形态:把异步锁的等待改回 `Atomics.wait` 后,测试不是"变慢",
|
|
538
|
+
> 而是**直接死锁**——A 阻塞事件循环等 B 持有的锁,而 B 永远没机会运行,最后锁超时。
|
|
539
|
+
> "在 async 函数里做阻塞等待"不只是性能问题,是能卡死整个进程的错误。
|
|
540
|
+
|
|
541
|
+
### 为什么只迁这一组,而不是 24 个调用点
|
|
542
|
+
`tasks`(2 处)/ `schedule`(12 处)/ `cachestats`(1 处)/ `sync`(1 处)**仍用同步版**,原因如实登记:
|
|
543
|
+
- 它们要么在**纯同步的读-改-写链**里(`patchTask` 被 `tasks.js` 内部多处同步调用,异步化会传染十几处,回归风险大于收益);
|
|
544
|
+
- 要么在**调度守护进程内部**(`schedule`)——那里的阻塞只会推迟定时任务,不会冻结用户请求;
|
|
545
|
+
- `cachestats` 的锁只在**轮转时**(文件 >4MB)取,且追加本身不加锁。
|
|
546
|
+
|
|
547
|
+
**因此本轮的结论是"请求路径不再冻结",不是"阻塞面已彻底消除"。** 剩余 16 处仍会在
|
|
548
|
+
极端争用下阻塞其所在进程——已量化、已登记,未假装解决。
|
|
549
|
+
|
|
550
|
+
### 测试(含对照组,否则容易恒绿)
|
|
551
|
+
1. **异步版不阻塞**:真跨进程持有者持锁 1.2s,等待期间 100ms 定时器**必须**按时触发;
|
|
552
|
+
2. **对照组**:同一场景下同步版返回时定时器**必须还没触发过**(证明断言有能力区分,而不是"根本没人持锁");
|
|
553
|
+
3. **真实路径**:`addWorkspace` 在争用时 50ms 心跳至少跳 3 次(退回同步锁时实测 **0 次 / 923ms**);
|
|
554
|
+
4. **可重入语义**:三个并发 async 任务各自 read-modify-write,必须都落上(丢失更新会暴露旧判据);同链嵌套不自死锁;
|
|
555
|
+
5. **陈旧回收**:持有者已死 → 立刻回收(不等 `staleMs`)。
|
|
556
|
+
|
|
557
|
+
### 新增常驻守卫:async 函数的调用点**必须 await**
|
|
558
|
+
这类迁移最容易漏的就是调用点——**漏了不报错**,只会让 `.ok` / `.error` 恒为 `undefined`,
|
|
559
|
+
于是断言**恒真**。本次就是靠这个扫描器抓出 src 里漏掉的 **2 处**
|
|
560
|
+
(`web/routes/domains/sessions.js` 的会话改名/删除映射——我先前手工 grep 时只看了 `touchWorkspace`,
|
|
561
|
+
scanner 是穷举的,手工 grep 不是)。守卫同时扫 `src/` 与 `test/`,并为两种正当写法放行:
|
|
562
|
+
作为回调传给已 await 的异步助手、以及在 `Promise.all` / `.then()` 内收口。
|
|
563
|
+
|
|
564
|
+
## 3.27 已修复(第二十八批:临界区里不该做的事 + 锁超时上限)
|
|
565
|
+
|
|
566
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
567
|
+
| --- | --- | --- | --- |
|
|
568
|
+
| P2-7(临界区构成) | `src/tasks.js#killTask` | **发现并核实**:`killTask` 把 `killTaskInner` **整个**包在锁里,而它内部会调 `pidOwnedBy()`(非 Linux 回退到**同步** `execFileSync('ps')`)与 `killTree()`(Windows 上走**同步** `spawnSync('taskkill')`)——等于把外部进程调用塞进临界区,持锁时间从毫秒级变成百毫秒级,**每个等锁的人都被拖住**。实测纯状态的临界区极短(小文件读-改-写 **0.13ms**、最重的 cache-stats 轮转 **6ms**),也就是说长持锁完全是"自己把慢操作放进去"造成的 | 把进程操作**全部移到锁外**,临界区只剩状态读-改-写。**语义不变**:状态写回仍在锁内(与 worker 的终态写互斥),P3 T20 的「killed 优先」保护仍在 `patchTask` 里 |
|
|
569
|
+
| P2-7(超时上限) | `src/atomic-write.js` | 默认 `timeoutMs=20000` 意味着「持有者活着但卡死」时,等锁方会被冻 **20 秒** | 依实测收紧为 **5000 / 4000**(仍满足 `timeout > stale`,不留死区):实测最坏临界区 6ms,5 秒是它的约 **800 倍**,正常争用绝不会误判超时,而最坏冻结直接降到四分之一。两个值都可用 `MINGDAO_LOCK_TIMEOUT_MS` / `MINGDAO_LOCK_STALE_MS` 覆盖;超时错误改为**可操作**(说清锁文件在哪、怎么看持有者 pid、什么时候可以删) |
|
|
570
|
+
|
|
571
|
+
### 更正 §3.26 里我自己的一个说法
|
|
572
|
+
§3.26 结尾我写「`tasks` 是剩余项里唯一还在用户请求路径上的」——**这句话不准确**,本轮核实后更正:
|
|
573
|
+
WebUI 的 `web/` 层**只 import `listTasks`**(读路径、不加锁),**从不直接调用** `patchTask` / `killTask`。
|
|
574
|
+
`tasks` 的锁只会在一个分支上被请求路径碰到:`listTasks` 默认 `reap=true` → 回收器发现**死掉的任务**时才
|
|
575
|
+
调 `patchTask`。也就是说这是"罕见分支 + 毫秒级临界区",比我上次说的轻。
|
|
576
|
+
**记录这个更正本身**:宁可当场纠正自己的判断,也不要让一个听起来更严重的说法留在文档里。
|
|
577
|
+
|
|
578
|
+
### 把残留变成清单(与"静默吞写白名单"同款做法)
|
|
579
|
+
剩余 **15 处**同步锁调用点已列成受审阅清单(`cachestats` 1 / `schedule` 12 / `sync` 1 / `tasks` 1),
|
|
580
|
+
每处写明为什么可以留;测试断言「清单与代码逐项一致」——**新增一处同步锁即失败**,
|
|
581
|
+
并提示"WebUI 请求路径请改用异步版"。
|
|
582
|
+
|
|
583
|
+
### 顺带修掉一处**会无声碎掉的源码断言**
|
|
584
|
+
既有测试 92d 用正则 `timeoutMs = (\d+), staleMs = (\d+) }` 去 **grep 源码文本**。
|
|
585
|
+
本轮把默认值换成命名常量后,这条断言直接匹配不到(`m92` 为 null)——**源码文本一改就碎**。
|
|
586
|
+
已改为读导出的 `lockDefaults()`:断言的是**行为契约**(`timeout > stale` 且上限有界),不是字符串长相。
|
|
587
|
+
|
|
588
|
+
## 3.28 已修复(第二十九批:合规静默失效——红线在,但没被求值 / 没被记录 / 没被看见)
|
|
589
|
+
|
|
590
|
+
> 这一批的共同形态:**不报错、不崩溃,只是某个"主打能力"实际空转**。
|
|
591
|
+
> 三份报告里这类条目最容易被"看起来实现了"骗过去,因此本批每条都补了**能被变异验证抓住**的断言。
|
|
592
|
+
|
|
593
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
594
|
+
| --- | --- | --- | --- |
|
|
595
|
+
| P1-1(约束事件无 `id`) | `src/constraints.js#event()` | **核实成立**:`event()` 返回 `{pack, constraint, kind, stage, …}`,而**两个**消费方都读 `id`(`agent.js` 的账本 `tool.call.constraint.id`、`replay.js` 的 `constraintId`)→ 每次被红线拦下,账本与回放里「是哪条红线拦的」**恒为 null**。不报错、只输出 null,任何断言都不会失败——这正是审计报告说的"100 组断言也抓不到"的那类字段级契约缺陷 | `event()` 增加 `id`(`constraint` 保留以兼容历史账本);补断言:事件带 `id` + 账本→回放的 `constraintId` 非空 |
|
|
596
|
+
| P1-2(`confirm` 从不求值) | `src/constraints.js` / `src/agent.js` / `src/replay.js` | **核实成立**:`confirm` 在 `KINDS` 里、能通过装载校验、会被计入 `size`(于是 `active=true`),却落进 `output` 桶并被 `if (c.kind !== 'output-forbid') continue` 跳过——作者以为"这条工具要人工确认",实际零效果 | **补齐实现**(而不是删掉 kind:`KINDS` 的 `confirm` 已在 `CHANGELOG-PACK` / `STRATEGY-0.5` / `PLAN-v0.5.0` / `PACK-API` 四处成文):`confirm` 进入 pre 桶后由 agent 走 `io.confirm`;答否**或拿不到交互通道**(`-p`/调度/子 Agent)一律按阻断(与权限引擎同口径)。回放无交互通道,故单列 `confirm-required` 一类如实报告,**不静默读成"通过"** |
|
|
597
|
+
| P1-2 连带(顺序陷阱) | 同上 | **自查发现**:`confirm` 若与阻断类**同轮**求值,把 `confirm` 写在 `tool-deny` 之前就会先返回 `needsConfirm`、后面的 deny 再也走不到——等于给红线开后门 | `checkPreTool` 改为**两遍**:先全部阻断类,再 confirm。补断言「confirm 声明在 tool-deny 之前时必须由 deny 给出结论」,并用**真·顺序变异**验证(第一版变异是等价变异,抓不到——已更正) |
|
|
598
|
+
| P1-12(CLI 回放恒假阴性) | `src/commands/ledger.js` | **核实成立**:CLI 的命令分发(`cli.js`)发生在 `mountPacks` **之前**,`getActivePackContext()` 恒为 null → `constraints=[]` → `compiled.active=false` → 回放恒输出「当前没有任何生效的领域约束」。把它当 CI 门禁时**恒通过** | replay 分支按启动路径同口径 `await mountPacks(...)`(幂等,失败降级为**可见告警**而不是静默空约束)。回归断言用**用户级 Pack**(默认受信任)端到端跑 CLI,断言 `constraintId` 与 `nowBlocked` 都对 |
|
|
599
|
+
| M-12(`--json` 恒失效) | `src/commands/ledger.js` | **核实成立**:`flag('--json')` 是**取值型**解析(`rest[i+1]`),布尔开关写在末尾取不到值 → 返回 null → `ledger replay <id> --json` 恒走人读分支,`\| jq` 直接失败 | 布尔开关与取值型参数分开解析(`boolFlag()`)。断言:末尾 `--json` 的输出**必须能被 `JSON.parse`**,且同一条账本的人读分支仍出现红线 |
|
|
600
|
+
| M-21(失败路径退 0) | `src/commands/diagnose.js`、`ledger.js`、`key.js`、`net.js` | **部分成立**(逐条复核):`diagnose` 打印 `[错误]` 后**退 0**;`ledger` 的非法 runId / 找不到账本 / 未知子命令打印提示后退 0;`key`、`net` 的未知子命令打印用法后退 0。**报告点名的 `update.js` 不成立**:它的 `return false` 是**设计行为**(P3-1 子命令劫持防护——`mingdao update the docs` 应被当作提问),不是"静默当默认并退 0" | 四处失败路径统一 `process.exitCode = 1`;`ledger` 把「未知子命令」判定**前置**(放在 runId 解析之前,否则同样的输入会因分支顺序而时 1 时 0),并与用法提示共用一份 `USAGE` 串。断言覆盖 5 条失败路径 |
|
|
601
|
+
|
|
602
|
+
### 两条值得记下的教训(都来自本批的 CI 红)
|
|
603
|
+
|
|
604
|
+
**一、"载荷没送到"的判据不能依赖事件到达时机。** BUG-042 的 fail-closed 断言连续两轮在
|
|
605
|
+
ubuntu Node 18/20 上红,而 macOS 本地恒绿。根因是**载荷尺寸 vs 平台 socket 缓冲**:
|
|
606
|
+
Node 在 POSIX 上用 socketpair 做 stdio,Linux 的 AF_UNIX 发送缓冲默认约 **208KB**
|
|
607
|
+
(`net.core.wmem_default`),200KB 的 hook 载荷能**整个塞进内核缓冲**——父进程的写入全部完成、
|
|
608
|
+
`stdin 'finish'` 照常触发,而 macOS 缓冲小得多所以先撞 EPIPE。
|
|
609
|
+
**判据本身(`'finish'` 是否触发)没错,是测试载荷不够大**:改成 **4MB**(大于任何平台的发送缓冲)
|
|
610
|
+
后,"写不完"这个事实才稳定暴露。留下的通用结论:跨平台测试里凡是"通过管道写大数据、指望对端不读就失败"
|
|
611
|
+
的用例,载荷必须做到**大于内核缓冲上限**,否则在缓冲大的平台上会静默变成另一条路径。
|
|
612
|
+
|
|
613
|
+
**二、变异也可能"等价"。** confirm 顺序陷阱的第一版变异(删掉第一遍循环里的 `if (c.kind === 'confirm') continue;`)
|
|
614
|
+
**不是有效变异**——因为第一遍循环体本来就不处理 confirm,删掉这行只是让它"穿过"循环,
|
|
615
|
+
行为完全不变,于是"仍然全绿"被误读成"断言抓不到"。改成**真的让 confirm 在第一遍返回**后,
|
|
616
|
+
断言立刻按预期失败。**结论:变异验证也要验证变异本身**,看到"没抓住"先怀疑变异是否等价。
|
|
617
|
+
|
|
618
|
+
## 3.29 已修复(第三十批:静默数据损失——写下去了,但那一行 / 那份配置没了)
|
|
619
|
+
|
|
620
|
+
> 这一批的共同形态:**没有报错,数据就是少了一部分**。四个位置都是"读-改-写"或"读不出来当没有",
|
|
621
|
+
> 且都发生在并发或多进程场景下——单进程手测永远看不到,只有把时序摆出来才现形。
|
|
622
|
+
|
|
623
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
624
|
+
| --- | --- | --- | --- |
|
|
625
|
+
| H-7(配置损坏被静默覆盖) | `src/config.js#loadConfig`、`cli.js`、`ensureMinimalConfig` | **核实成立,且比报告多一条**:`catch → null` 把 ENOENT 与"解析失败/权限不足/不是对象"抹平成同一件事,所有调用点都把 null 当**首次运行**。报告只提到"改坏一个字符",实测还有一支同源问题:**带 BOM 的 config.json**(内容完全合法,Excel/记事本另存为的常见产物)也会被判成损坏 | 新增 `readConfigStrict()`(区分不存在/读不出来,解析前剥 BOM)+ `quarantineCorruptConfig()`(**改名**为 `config.json.corrupt-<时间戳>` 而不是覆盖,并明确告警"从备份并回 customModels/mcpServers/sync/net 等字段")。CLI 向导前与桌面首启路径都先隔离。`loadConfig()` 的既有语义(读不出来 → null)保持不变,避免影响既有调用点 |
|
|
626
|
+
| H-4(`/compact` 用追加) | `src/commands/repl.js` | **核实成立**:手动 `/compact` 把"压缩点标记 + 压缩后消息"**追加**到已含旧历史的文件后面,与会话文件里已有的历史重复;而自动压缩走的是 `rewriteSession`(原子重写)——同一件事两套写盘口径 | `/compact` 改用 `rewriteSession(session.file, messages)`,与自动压缩同口径;补断言(会话层"重写即替换" + 源码级钉住 `/compact` 分支不得再用 `appendMessages`) |
|
|
627
|
+
| M-1(压缩后不清读缓存) | `src/agent.js`、`src/tools/fs-tools.js`、`repl.js` | **核实成立**:读取去重按 `mtime+size` 判"同一文件读过",而压缩会把早期消息(含文件正文)替换成摘要 → 模型再读同一文件只拿到「内容与上次读取一致」的占位串,**正文已不在上下文里**,它可能据此继续改文件 | 压缩成功路径(自动)与 `/compact`、`/clear`(手动)都调 `agentReadCache.clear()`;agent 对外暴露 `clearReadCache()`。行为断言:占位串 → 清缓存 → 正文回来;另加源码级钉子钉住自动压缩路径的调用顺序 |
|
|
628
|
+
| BUG-009(追加在锁外) | `src/cachestats.js` | **核实成立**:轮转的 `read-modify-write` 在锁内,而**追加在锁外** —— A 追加 → B 追加 → A 在锁内读到含 A 的快照并原子替换,B 那一行落在读与写之间就被覆盖。丢的是费用明细,而费用明细是日费用护栏的依据 | 追加与轮转收进**同一把锁**(锁内追加 → 立刻判断是否超限 → 超限则瘦身重写)。频率是"每回合一条",不在请求热路径上;同步锁清单同步复核并写明理由 |
|
|
629
|
+
| M-20(轮转窗口吃行) | `src/audit.js`、`src/log-writer.js` | **核实成立,但两处取舍不同**:`audit.jsonl` 丢的是**合规证据**("少一行"等于证据链有洞),故与 cache-stats 同法——追加与轮转同锁;`web-server.log` 的写入器跑在 **WebUI chat 请求路径**上(`srvlog(...)`),加同步锁会引入 `Atomics.wait` 阻塞(正是 §3.26–3.27 花力气消掉的东西) | 审计:同锁。日志:改为**改名式轮转**(把整文件 `rename` 成 `<file>.1` → 立刻按 logrotate 的 create 语义建空文件 → 上一轮 `.1` 先删),没有"读-改-写"窗口因此**不需要锁**;Windows 上 rename 失败即"这次不轮转、文件继续长",**不丢数据**属于可接受降级 |
|
|
630
|
+
|
|
631
|
+
### 两条自我纠错(都是变异验证逼出来的)
|
|
632
|
+
|
|
633
|
+
**一、"整次调用耗时"证明不了"追加在锁内"。** 第一版并发断言测的是 `recordCacheStats()` 的耗时:
|
|
634
|
+
锁被别的进程持有时它确实会等待,于是断言通过。但把 `append` 挪到 `withFileLockSync` **之前**再跑,
|
|
635
|
+
耗时**照样**包含等锁时间——变异验证直接指出"该断言抓不到这个变异"。
|
|
636
|
+
改成观测**写入时机**:让另一个进程持锁 1.5 秒,从第三个进程发起追加,期间读文件确认那一行
|
|
637
|
+
**还没落盘**,锁释放后才出现。这才是"追加在锁内"的可观测证据。
|
|
638
|
+
|
|
639
|
+
**二、"有没有 `.corrupt-*` 备份"是假绿。** 断言 `readdir(...).some(f => f.startsWith('config.json.corrupt-'))`
|
|
640
|
+
在本组测试里恒真——因为前面的用例已经留下过一个备份,把 `ensureMinimalConfig` 的隔离调用删掉也照样通过。
|
|
641
|
+
改成比较**增量**后才真正抓住变异,并顺带暴露一个真缺陷:同一秒内两次隔离时,
|
|
642
|
+
`renameSync` 会**覆盖**上一份备份(POSIX rename 是覆盖语义),于是"备份的意义:一份都不许丢"被破坏。
|
|
643
|
+
已改为重名顺延编号(`…-1`、`…-2`)。
|
|
644
|
+
|
|
645
|
+
## 3.30 已修复(第三十一批:Web 安全——围栏逃逸 / 元数据 SSRF / 跨站盲打 / Provider 名穿越)
|
|
646
|
+
|
|
647
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
648
|
+
| --- | --- | --- | --- |
|
|
649
|
+
| P0-3(符号链接逃逸) | `src/web/routes/domains/workspace.js` | **核实成立(实测复现)**:`withinAllowed` 只做 `path.resolve` + 字符串前缀比较,而后续 `statSync/readdirSync` **跟随符号链接** → `ln -s / <家目录>/escape` 之后 `?dir=…/escape` 通过检查、stat 落到 `/`,于是任意目录枚举;再用一条 `POST /api/workspaces {dir:…/escape}` 登记,Agent 的 bash/write/edit 就整体跑到围栏之外(`os.tmpdir()` 还被无条件算作允许根,门槛更低) | 围栏改为按 **realpath** 判定,且**目标与允许根两边都归一化**(只归一化一边会把合法目录误判越界——macOS 上 `/var/folders` → `/private/var/folders` 就是实例);目标不存在时回退到"最近的存在祖先"的 realpath;登记(add)与改目录(set)同样先归一化;直接访问符号链接路径同样 403 |
|
|
650
|
+
| P0-3 附(报告的另一半) | 同上 | **核实为不可利用**:报告称"`readdir` 不过滤符号链接条目"是逃逸的一半。实测 `Dirent.isDirectory()` 走 **lstat** 语义,对符号链接本就返回 `false`,因此**去掉** `!e.isSymbolicLink()` 也列不出链接条目——它只是双保险。已在代码注释里如实写明,不把它当防线 | 保留过滤(纵深防御),但断言改成**能真正失败**的那条:「把符号链接路径直接喂进来必须 403」 |
|
|
651
|
+
| P1-8(回环绑定时整段跳过 SSRF) | `src/web/server.js#validateRemoteUrl` | **核实成立**:`serverBoundLocal` 为真时**整段**校验被旁路,于是云元数据(`169.254.169.254`)、CGNAT、DNS 重绑定检测一起失效;而同一进程里 `skill-lib`/`safe-fetch` 的注释明确写着"WebUI 默认拦截(连回环都拦)"——两处策略相反 | 新增**元数据端点无条件拒绝**(`isMetadataHost`,单一来源在 `tools/fetch.js`):回环绑定、`web.allowPrivateEndpoints`、`allowPrivate: true` 都**不放行**。同时把"回环放行"与"对外监听拒绝"的差异写成**显式决定**并保留:本机模型服务(Ollama/Dify)必须可达,否则是拿安全换掉可用性。**刻意不采纳"CGNAT 段无条件拒绝"**:`100.64.0.0/10` 正是 Tailscale 的地址段,自托管场景常见,无条件拒绝会直接打断真实用法——它仍属"私网档",由 `allowPrivateEndpoints` 决定 |
|
|
652
|
+
| H-5(回环 + 无令牌 = 无认证) | `src/web/routes/api.js`、`src/web/server.js` | **核实成立,但采纳的是"关掉浏览器攻击面 + 把决定说出来",而不是"回环也强制令牌"**:桌面版**本来就**随机生成令牌,只有 CLI `mingdao web` 走本机信任;改成默认强制令牌会让"浏览器打开 127.0.0.1 直接用"的日常用法每次都要回终端取令牌(SPA 把令牌存在 sessionStorage,关掉浏览器即失效),代价大于收益 | ① 跨站浏览器请求一律 403(见下条),"任意网页"这一半**彻底关闭**;② 回环未配令牌时启动横幅**明确提示**"本机信任模式:本机其它进程可直接访问 /api/*",并给出启用令牌的两种方式。**残留如实登记**:同机其它进程/其它用户仍可直连 API(多用户机器、共享 CI runner 需自行启用令牌) |
|
|
653
|
+
| H-6(GET 有副作用,可被盲打) | `src/web/routes/api.js` | **核实成立**:CSRF 保护只覆盖非 GET,而 `GET /api/draft` 是"读取即删除"、`GET /api/sync` 会外呼、`GET /api/skill-library?refresh=1` 会联网——`<img>`/`fetch(no-cors)` 盲打即可触发(读不到响应,但副作用已发生) | 引入 **`Sec-Fetch-Site` 判定**:该头由浏览器强制附加且页面脚本无法伪造,`cross-site`/`same-site` 一律 403(含 `img`/`no-cors` 盲打),`same-origin`/`none`(地址栏、书签)放行;并**豁免**"从外站导航到壳页面"(页内子资源仍是 same-origin,拿不到数据)。另外把 **Origin 校验扩展到所有方法**(同源 GET 不带 Origin,因此不误伤)。回归断言直接钉住危害:跨站 GET 不得读走草稿 |
|
|
654
|
+
| R4(自定义 Provider 名穿越) | `src/providers/index.js` | **核实成立**:`pc.name` 来自 `customModels.<名>.provider`,原实现直接 `path.join(home,'providers', name + '.mjs')` 再 `import` → `provider: '../evil'` 可 import 目录之外的 `.mjs`(读+执行任意本地模块,与 v0.4.6 修掉的环境变量外泄是孪生通道) | 两层防护:**名字白名单** `^[A-Za-z0-9][A-Za-z0-9._-]{0,64}$` + **路径包含性检查**(`path.relative` 落在 `providers/` 内)。名字不合法时退化为"没有自定义模块"(走普通 OpenAI 兼容端点并报缺 baseUrl),不抛异常、不改变正常 Provider 的行为;回归断言用"把恶意模块放在 providers/ 之外"端到端验证它**没有**被执行 |
|
|
655
|
+
|
|
656
|
+
### 三条值得记下
|
|
657
|
+
|
|
658
|
+
**一、变异验证又一次抓出"我以为在测、其实测不到"。** 本批 8 个变异里有两个的结果需要如实修正:
|
|
659
|
+
- 「把符号链接条目过滤去掉」**抓不到**——因为 `Dirent.isDirectory()` 对链接本就是 false,这是一个**等价变异**(同 §3.28 的第二条教训)。于是把断言换成能真正失败的那条。
|
|
660
|
+
- 「`link-out` 指向 `os.homedir()`」**抓不到**——因为 `os.homedir()` **本身就是允许根**,指它根本不算逃逸。改成指向 `/usr` 后才暴露出真实差异。
|
|
661
|
+
|
|
662
|
+
**二、并行跑变异会骗人。** 批五的冒烟用例会起 Web 服务并绑定**固定端口**,9 个副本并行时互相抢端口,
|
|
663
|
+
上一版并行结果里"变异 2/6/7 被抓住"其实混进了端口冲突导致的假失败;改成串行重跑才看清真实情况
|
|
664
|
+
(其中 m6 的补丁还把代码改出了语法错误,那是"被抓住",但不是被断言抓住)。
|
|
665
|
+
**结论:变异验证必须在与断言同等的条件下跑;有共享资源(端口/文件/锁)时一律串行。**
|
|
666
|
+
|
|
667
|
+
**三、报告的"安全建议"要按真实用法复核,而不是照抄。** 两处刻意没有照做,理由都写在上表里:
|
|
668
|
+
CGNAT 段无条件拒绝会打断 Tailscale 场景;回环强制令牌会让日常用法每次回终端取令牌。
|
|
669
|
+
把"为什么不照做"写清楚,比默默照做或默默不做都更负责。
|
|
670
|
+
|
|
671
|
+
## 3.31 已修复(第三十二批:供应链——CI 门禁不得执行被审代码 / 遮蔽必须可见 / 本地路径不进安装器)
|
|
672
|
+
|
|
673
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
674
|
+
| --- | --- | --- | --- |
|
|
675
|
+
| H-9 / P0-1(`pack verify` 会执行 Pack 代码) | `src/commands/pack.js`、`src/packs.js` | **核实成立,且是"文档与实现说反话"的典型**:帮助文本写「只做静态校验(不 import Pack 代码)」,实现却无条件 `loadPack()` → **import pack.mjs**。下游 CI 按文档拿它当门禁审第三方 Pack,等于在 CI 上以完整 Node 权限执行被审仓库的任意代码,且**不经 pack trust 信任门** | 新增 `loadPackStatic()`(manifest 字段 / 兼容窗口 / 声明文件齐全 / pack.mjs **存在性**,全程不 import),`pack verify` **默认静态**、要执行必须显式 `--runtime`(并先警告"将以完整 Node 权限执行")。`loadPack()` 复用同一份静态校验,避免两处漂移。**如实写明边界**:约束与提示词段由代码产出,静态阶段无法校验 |
|
|
676
|
+
| M-5(未信任 Pack 顶掉同名内置/用户级) | `src/packs.js#listPacks` | **核实成立**:`seen.set(name, …)` 按 name 后写覆盖,tier 顺序 builtin < user < project → clone 一个仓库并在其中放同名 Pack,就能让**内置 Pack 的 constraints/promptSections 静默失效**(项目版本因为带 gate 不挂载,被顶掉的内置版本也回不来 = 两边都没生效) | 未信任的 tier **不参与遮蔽**(它本来就不会被挂载,无权挤掉别人);同名冲突写进 `warning` 并由 `mountPacks` 变成启动告警;已信任的高优先级遮蔽低优先级时记 `shadowed` 并同样告警("哪个版本真的生效"必须可见) |
|
|
677
|
+
| M-8(项目级预设静默遮蔽 + 注入面) | `src/presets.js`、`cli.js`/`repl.js`/`web` 共用 `loadPreset` | **核实成立**:`--preset <名>` 会被仓库里 `.mingdao/presets/` 的同名预设静默替换,而预设可以定义 `systemPrompt` 与 `tools` 白名单——clone 一个仓库再 cd 进去用自己熟悉的名字调预设,拿到的是对方的提示词 | 不再改变遮蔽语义(那是文档化行为),但**遮蔽必须说出来**:`listPresets` 记录被遮蔽来源,`loadPreset` 在命中项目级且确有遮蔽时打一条含**两份路径**的告警(每 (工作目录,名) 一次) |
|
|
678
|
+
| BUG-056(`installFromGit` 收 `file://` 与本地路径) | `src/skill-lib.js` | **核实成立**:只挡了 `-` 开头的选项注入,`file:///etc` 或直接给本地路径都能让 git 去 clone 本地目录——安装器的语义是"从远端取回内容",本地路径等于把本机目录当技能读走 | 加**形态白名单**:只接受 `https://` / `ssh://` / `git://` / `git@host:path`;`file://` 与本地路径明确拒绝并说明原因 |
|
|
679
|
+
| M-6(技能库索引的 sha256 是自证) | `src/commands/skill.js` | **核实成立但不宜"修"**:文件与哈希同源(都在索引里),投毒者可同时下发恶意正文与匹配哈希,校验必然通过。真正的修法是签名索引/TUF——那要下游生态一起定规格,不是本批能擅自定的 | 安装后**如实说明这条校验的边界**:"sha256 与索引同源,只防传输损坏/镜像篡改,不防索引本身被投毒"。把"它能做什么"和"它不能做什么"同时讲出来,比让用户以为"已校验=可信"更负责 |
|
|
680
|
+
|
|
681
|
+
## 3.32 已修复(第三十三批:并发与长驻——进程不能死 / 任务卡不死也不能双跑 / pid 复用)
|
|
682
|
+
|
|
683
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
684
|
+
| --- | --- | --- | --- |
|
|
685
|
+
| P0-4 A(未处理的 rejection 直接杀进程) | `src/sync-server.js:586` | **核实成立**:`withWriteLock(...)` 是 async 且**未 await、未 .catch**。临界区里任何 I/O 异常(ENOSPC/EACCES)都会让返回的 promise reject,而它没有挂在请求处理链上 → Node 默认 `--unhandled-rejections=throw` → **整个同步服务退出**(所有在线设备同时掉线)。请求层那句 `handle().catch(()=>{})` 接不住它 | `await withWriteLock(...)` 并包 try/catch 留痕(lastSeen 只是"最近在线"标记,写失败不该升级成进程崩溃)。补一条**扫描式守卫**:sync-server.js 里任何"调用 withWriteLock 但不 await/return"的行都会被测试揪出来 |
|
|
686
|
+
| P0-4 B(锁外快照写回) | `doShareAccept`、`doChangePassword` | **核实成立**:accept 在锁**外**读 shares/accepted,锁内把陈旧快照整份写回 → revoke 之后的一次并发 accept 会把已删除的 shareId 写回去(分享复活);改密 `delete devices[username]` 同样在锁外,可被并发 pair 写回 → **已吊销的旧 token 复活** | accept 改为**锁内重读**再写,且重读后发现分享已被撤销就返回 notFound;改密整体纳入写锁(与设备表写互斥),调用点补 await |
|
|
687
|
+
| P1-5(任务永久卡 running / 崩溃恢复双跑) | `src/cli.js` | **核实成立**:`markRunning` 把 `runnerPid` 写成**本 daemon 自己**,而恢复分支的判据是 `if (procAlive(j.runnerPid)) continue;` → `procAlive(自己)` 恒真 → **永久跳过**(面板一直转圈,只能 daemon stop + 删 pidfile 自愈)。反向问题:宿主被 SIGKILL 但 detached worker 还在跑时,恢复分支会把它当孤儿**重排 → 同一任务跑两遍** | ① `runnerIsSelf` 例外:宿主就是自己时不"等它"(那是死等);② worker 仍活着时**绝不重排**(`taskWorkerAlive`,与 tasks.js 共用同一判据),等它收尾后由下一轮的定案分支处理;③ `runSleeper(...).catch(() => {})` 改为**留痕日志**——静默吞掉的异常正是"永久卡住"且无从诊断的原因 |
|
|
688
|
+
| M-11(pid 复用让锁僵死) | `src/atomic-write.js` | **核实成立**:陈旧回收只看"持有者 pid 是否存活",pid 被复用给一个活进程后 `procAlive` 恒真 → 锁**永不回收**,写方等到超时 | 锁内容记下入口脚本文件名作为身份凭据,回收前用既有的 `pidOwnedBy()` 复核"活着的这个 pid 还是不是原来那个进程";确证不是 → 回收;能读到命令行但匹配不上才回收,**读不到(Windows)则维持原判据**(不引入新的不确定行为)。**踩过的坑写在下面第一条** |
|
|
689
|
+
| M-13(避峰长等待不看租约) | `src/schedule.js` | **核实成立**:`await wait(defer-now+2000)` 可睡数小时,期间 daemon 被接管/停止,旧协程仍持 job 视图与新 daemon 交错 | 切片等待 `waitGuarded()`(每片 ≤60s,每片醒来复查 `shouldStop()`,失去租约立刻 aborted) |
|
|
690
|
+
|
|
691
|
+
### 四条自我纠错(全部由断言/变异/CI 当场抓出)
|
|
692
|
+
|
|
693
|
+
**零、源码级正则断言必须容忍 CRLF。** Windows 腿的 `actions/checkout` 会把 LF 转成 CRLF,
|
|
694
|
+
于是 `/try \{\n\s+await withWriteLock/` 这类**用 `\n` 写死换行**的正则只在 Linux/macOS 通过,
|
|
695
|
+
Windows 腿红。本批的三条守卫(sync-server 写锁、BUG-029 循环头、安装器 finally)都是这个毛病,
|
|
696
|
+
已统一改成 `\r?\n`。**教训:源码级断言是在"文本"上断言,就必须按各平台的文本形态写。**
|
|
697
|
+
|
|
698
|
+
### 另外三条自我纠错(由断言/变异当场抓出)
|
|
699
|
+
|
|
700
|
+
**一、「身份凭据」必须与观察窗口的形态一致。** M-11 第一版把 `process.argv[1]`(Node 解析后的**绝对**路径)写进锁,
|
|
701
|
+
而 `ps` 显示的是**输入时的形态**(`node test/smoke.js`)——两者匹配不上,于是**同进程内的并发任务也被判成 pid 复用,
|
|
702
|
+
活锁被抢走**:既有的并发串行断言(3 次自增)当场只剩 1。改成 `path.basename(...)`(两种形态下都出现)。
|
|
703
|
+
教训:验证"这是不是同一个进程"时,凭据必须取自**双方都能看到的那份表示**。
|
|
704
|
+
|
|
705
|
+
**二、扫描式守卫要钉整行。** P1-5 的守卫第一版只匹配 `taskWorkerAlive(t)) continue;`——把条件短接成
|
|
706
|
+
`if (false && … && taskWorkerAlive(t)) continue;` 的变异**仍然通过**(变异验证当场发现)。改成匹配整行后变异被抓住。
|
|
707
|
+
|
|
708
|
+
**三、调试时的临时还原会吃掉补丁。** 排查 M-11 时用 `cp 备份 → 打调试补丁 → cp 还原` 来回切换,
|
|
709
|
+
其中一次还原把刚打好的 basename 修复覆盖回了旧版,导致后续几次实验的结论自相矛盾(同一脚本相对/绝对两种启动方式
|
|
710
|
+
表现不同)。最后用 `git diff` 核对文件真实状态才发现。**结论:调试补丁一律用 git 暂存/还原,不用手工 cp 备份。**
|
|
711
|
+
|
|
712
|
+
## 3.33 已修复(第三十四批:计费与可用性——重复计费 / 半截回答 / 压缩永久失效 / 归属错模型)
|
|
713
|
+
|
|
714
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
715
|
+
| --- | --- | --- | --- |
|
|
716
|
+
| BUG-028(对非幂等请求重试 → 重复计费) | `src/providers/index.js` | **核实成立**:chat 是**非幂等** POST,上游一旦开始生成就按量计费;而重试判据只看"错误是不是瞬态"——流式空闲超时(已经开始输出)也会被重试,于是**用户看到一份回答、账本却记两次** | 新增"收到过任何一帧就不再重试"(`sawFrame`):帧到达说明请求已被受理并开始计费。**代价如实说明**:已开始输出后的空闲超时不再静默重试,失败以错误回到用户手里,而不是悄悄多花一份钱。反向用例一并保留:什么都没收到时仍必须重试 |
|
|
717
|
+
| BUG-029(总量护栏在循环头未复查) | 同上 | **核实成立**:`totalExpired` 只在 catch 的 transient 判定里被读,于是"总量计时器恰好在退避期间触发"这条路径会照常发起新 attempt(重试序列可能超出声明的总时长) | 重试循环**头部**复查 `totalExpired` 并直接抛出 |
|
|
718
|
+
| BUG-030(退避等待不可中断) | 同上 | **核实成立**:`await sleep(backoff)` 最长 30s 期间 Ctrl+C 无效 | `sleep(ms, signal)` 接信号,中断即抛 |
|
|
719
|
+
| BUG-036(`compactTrigger` 只夹下限) | `src/compact.js` | **核实成立**:只 `Math.max(…, TARGET_RATIO)`,配成 >1(手滑多打一个 0)会让 `total <= budget*trigger` 恒真 → **自动压缩永久失效** → 长会话涨到窗口上限才由服务端报错 | 上限夹到 1 并**明确告警**(一次性),不再静默失效 |
|
|
720
|
+
| BUG-024(降级回合的账本用错模型) | `src/agent.js` | **核实成立**:计价用 `modelName`(用户配置),而成本护栏降级后请求实际发给 `activeModel`——两者单价差数倍,账本与日费用护栏系统性偏移,"降级省钱"反而看不出来 | 计价与 `cost` 事件的 `model` 都改用 `activeModel`(`run.start` 已记过用户请求的模型,两者不再混为一谈) |
|
|
721
|
+
| M-16(提前关流被当正常完成) | `src/providers/openai-compatible.js`、`agent.js` | **核实成立**:网关既不发 `[DONE]` 也不给 `finish_reason` 就关流时,`finish` 为 null 却被记成正常完成——半个回答当完整交付,界面上与真完成无法区分 | 流解析返回 `truncated`(有正文却既无 `[DONE]` 也无 `finish_reason`);agent 收到后**界面提示 + 账本标记**,回合结果另加 `upstreamTruncated` 字段(刻意不叫 `truncated`——那个既有字段的含义是"步数兜底总结也失败了") |
|
|
722
|
+
| M-17(网关改写 content-type → 正文全丢) | 同上 | **核实成立**:只看 `text/event-stream`,代理标成 `application/json` 时 `res.json()` 必失败 → 整段正文丢失且无提示 | 读成文本后**按内容嗅探**:出现以 `data:` 开头的行就走流式解析;否则按 JSON 解析,两者都不是时给出带 content-type 与正文前 120 字的错误 |
|
|
723
|
+
|
|
724
|
+
### 两条自我纠错(都是变异验证抓出来的)
|
|
725
|
+
|
|
726
|
+
**一、"同形调用"会让守卫假绿。** BUG-024 的守卫第一版写成 `/estimateCost\(activeModel, usage\.prompt_tokens/`——
|
|
727
|
+
而同文件里 `inFlightCost()` 有一处同形调用,于是**把这行改回 modelName 的变异照样通过**。
|
|
728
|
+
收紧为连同该行特有的参数(`cacheSplit(usage), costDate`)一起匹配后才抓住。
|
|
729
|
+
|
|
730
|
+
**二、"掐断连接"不是瞬态错误,测不出重试语义。** BUG-028 的第一版用例用"中途销毁连接"来验证"收到帧后不重试",
|
|
731
|
+
但那个错误根本没被 `isTransient` 归为瞬态 → 两种实现都不重试 → 断言恒真(变异验证直接指出"抓不到")。
|
|
732
|
+
改用**可控的流式空闲超时**(`timeout.streamIdleMs: 400`):它既有帧(说明已开始计费)、又确实属于可重试的瞬态,
|
|
733
|
+
才真正落在两条实现的分歧点上。
|
|
734
|
+
|
|
735
|
+
## 3.34 已修复(第三十五批:上游能力与文档——临时目录 / 检查点路径穿越 / 令牌进 argv / 文档漂移)
|
|
736
|
+
|
|
737
|
+
| 项 | 位置 | 核实结论 | 处理 |
|
|
738
|
+
| --- | --- | --- | --- |
|
|
739
|
+
| BUG-010(安装器临时目录泄漏) | `src/skill-lib.js` | **核实成立**:`installFromUrl` 只在"校验失败"这一条早退路径上显式 `rmSync`,`installFromGit` 同理——一旦中途抛异常(`readSkillMeta` 返回 null 时读 `meta.name` 直接 TypeError,或 `ensureHome`/写盘失败)就**留下整个临时目录**,反复失败即持续吃磁盘 | 两条安装器的清理都收进 `finally`;并补 `meta` 为 null 的显式判定(返回可读错误而不是抛 TypeError) |
|
|
740
|
+
| BUG-067(检查点文件名可穿越) | `src/task-state.js` | **核实成立**:`taskStateFile(sessionName)` 直接拼 `${name}.json`,`../../evil` 会把检查点读写到目录之外(三个调用点都只 catch 异常,等于**静默**把错误当"没有检查点") | 加**包含性检查**(`path.relative` 落在 `taskstates/` 内),越界即抛;三个调用点已有的 try/catch 会如实返回 `{ok:false,error}`。用 `path.relative` 而非"过滤 `../`":后者挡不住绝对路径与平台差异 |
|
|
741
|
+
| BUG-015(`--auth-token` 进 argv) | `src/commands/skill.js` | **核实成立**:令牌写在命令行里会留在 argv 与 shell 历史(`ps aux` 可见)——本仓发布链路早已把"token 不进 argv"定为纪律,这是漏网的一处 | 新增 `--auth-token=-` **从 stdin 读**;直接用字面量时给出明确告警并指向 `MINGDAO_WEB_TOKEN` / `config.web.token`(不改默认行为,只消除沉默) |
|
|
742
|
+
| P3(文档与实现漂移) | `docs/PACK-API.md`、`docs/CONFIG.md`、`docs/DEVELOPER.md` | **确实存在**(本批修的多处行为在文档里还是旧说法) | 逐处跟上:`pack verify` 默认静态 / `--runtime` 的语义与理由、目录围栏的 realpath 语义(含"返回规范化路径"这一契约变化)、本机信任模式与令牌的三种来源(含 `--auth-token=-`)、跨站请求一律 403、云元数据端点无条件拒绝、`compactTrigger` 的 0–1 范围与越界行为。并把文档一致性写进断言(文档回退会被测试挡住) |
|
|
743
|
+
|
|
744
|
+
> 关于 §3.34 的验证层级:BUG-010 与 BUG-067 是**行为级**(真的制造失败路径、真的看磁盘上有没有残留);
|
|
745
|
+
> BUG-015 是**行为 + 源码级**(空 stdin 真的退 1;告警文案由源码守卫钉住);
|
|
746
|
+
> 文档一致性是**文本级**。**如实登记,不把文本级说成行为级。**
|
|
747
|
+
|
|
523
748
|
## 4. 其余登记项(**第三方结论,我未逐条复核**)
|
|
524
749
|
|
|
525
750
|
### 4.1 自评报告(`MingDao-harness-v0.6.1-技术评估报告.md`)
|
|
@@ -535,11 +760,11 @@ Windows 对这种情况回报的正是 **ENOENT**,与"本来就没有检查点
|
|
|
535
760
|
| ~~P2-4~~ | ~~Web 会话忙锁~~ | ✅ **已修**(见 §3.11) |
|
|
536
761
|
| ~~P2-5~~ | ~~终端渲染~~ | ✅ **已修**(见 §3.1,`io.print` 统一过 `sanitizeKeepingSgr`) |
|
|
537
762
|
| ~~P2-6~~ | ~~`sleeperAlive`~~ | ✅ **已修**(见 §3.8) |
|
|
538
|
-
| P2-7 |
|
|
763
|
+
| ~~P2-7~~ | ~~文件锁~~ | ✅ **已修**(陈旧判据见 §3.10;**阻塞面**见 §3.26 异步锁 + §3.27 临界区收窄/超时上限。残留 15 处同步锁已列受审阅清单) |
|
|
539
764
|
| ~~P2-8~~ | ~~出网闸门~~ | ✅ **已修**(见 §3.14) |
|
|
540
765
|
| ~~P2-9~~ | ~~项目记忆~~ | ✅ **已修**(见 §3.2) |
|
|
541
|
-
| P2-10 |
|
|
542
|
-
| P2-11 |
|
|
766
|
+
| ~~P2-10~~ | ~~`killTask`~~ | ✅ **已修**(见 §3.9:升级 SIGKILL + 立即置终态) |
|
|
767
|
+
| ~~P2-11~~ | ~~调度 `runOnce`~~ | ✅ **已修**(见 §3.9:轮询期间检查租约) |
|
|
543
768
|
| P3-1 | 避峰备注 | 写「北京时间」却打印 UTC(错 8 小时) |
|
|
544
769
|
| P3-2 | `kind:'every'` | `nextRunAt` 缺失时每 1 秒空转 |
|
|
545
770
|
| P3-3 | git 工具 | 默认加 `--stat` 与模型显式 `--no-stat` 冲突 |
|
|
@@ -550,14 +775,14 @@ Windows 对这种情况回报的正是 **ENOENT**,与"本来就没有检查点
|
|
|
550
775
|
|
|
551
776
|
| 级别 | 位置 | 摘要 |
|
|
552
777
|
| --- | --- | --- |
|
|
553
|
-
| P2-1 |
|
|
554
|
-
| P2-2 |
|
|
555
|
-
| P2-3 |
|
|
778
|
+
| ~~P2-1~~ | ~~`src/memory.js:96`~~ | ✅ **已修**(`kept.join('\\n') + '\\n'`,见 `src/memory.js:100` 的 v0.6.2 注释) |
|
|
779
|
+
| ~~P2-2~~ | ~~`src/audit.js:36-45`~~ | ✅ **已修**(§3.17:截断判据改为**文件大小**,跨进程有效) |
|
|
780
|
+
| ~~P2-3~~ | ~~`src/audit.js:41`~~ | ✅ **已修**(§3.17:改 `atomicWriteFileSync`;v0.6.3 批四又把追加与轮转收进同一把锁,见 §3.29) |
|
|
556
781
|
| ~~P2-4~~ | ~~`src/commands/pack.js:31/166`~~ | ✅ **已修**(见 §3.12,并修掉连带发现的文档/实现不一致) |
|
|
557
782
|
| P2-5 | `src/mcp-presets.js:43-48` | sqlite 预设必填参数含 `{dir}` 未替换,静默落到 cwd |
|
|
558
783
|
| ~~P2-6~~ | ~~`src/commands/sync.js:81-88`~~ | ✅ **已修**(见 §3.1) |
|
|
559
|
-
| P2-7 |
|
|
560
|
-
| P2-8 |
|
|
784
|
+
| ~~P2-7~~ | ~~`src/skill-registry.js:56`~~ | ✅ **已修**(§3.12:抽 `safe-fetch.js` 单一来源,逐跳复检) |
|
|
785
|
+
| ~~P2-8~~ | ~~`src/skill-registry.js:144-150`~~ | ✅ **已修**(§3.25:缺哈希即**拒绝安装**;v0.6.3 批六补了「sha256 只防传输损坏、不防索引投毒」的说明,见 §3.31) |
|
|
561
786
|
| ~~P2-9~~ | ~~`src/tools/index.js:332-374`~~ | ✅ **已修**(见 §3.8) |
|
|
562
787
|
| P2-10 | `src/routing.js:17`、`config.js:160` | 路由默认仍是改名前的旧模型名 |
|
|
563
788
|
| ~~P2-11~~ | ~~`src/agent.js:104` vs `:34`~~ | ✅ **已修**(见 §3.12) |
|
|
@@ -575,45 +800,79 @@ Windows 对这种情况回报的正是 **ENOENT**,与"本来就没有检查点
|
|
|
575
800
|
| --- | --- | --- | --- |
|
|
576
801
|
| B-CMD-3 | 凭据泄露 | `sync passwd` 新密码走命令行 | ✅ 已修(§3.1) |
|
|
577
802
|
| ~~B-CMD-1/2~~ | 资源泄漏 | readline 未 close | ✅ **已核实为非缺陷**(§3.20 附:全仓仅 3 处 createInterface,均已有 close;EOF 下 question 正常回调) |
|
|
578
|
-
| B-ATW-1/2 | 环境兼容 | SharedArrayBuffer / `Atomics.wait` 阻塞 |
|
|
803
|
+
| B-ATW-1/2 | 环境兼容 | SharedArrayBuffer / `Atomics.wait` 阻塞 | ✅ **已处理**(§3.10 正确性 + §3.26 异步锁 + §3.27 临界区收窄与超时上限;残留 15 处已列清单,理由见 §3.27) |
|
|
579
804
|
| B-CT-1 | 性能 | 「WeakMap 缓存永远 miss,每步全量 BPE」 | ✅ **已核实并按其真实影响处理**(§3.15):**"永远 miss"不成立**——同一计数器下 200 次调用 0.0ms;真问题是计数器 identity 不稳定,已按模型名缓存 |
|
|
580
805
|
| ~~B-CON-1~~ | 安全/注入 | ReDoS | ✅ **已核实并修复**(§3.16:约束 pattern 的嵌套量词,实测 29 字符 4.8 秒) |
|
|
581
806
|
| ~~B-HK-1~~ | 安全/注入 | `shell:true`(`hooks.js`) | ✅ **已核实为非缺陷**(§3.16:hooks 来自用户自己的 config,Pack 无法贡献) |
|
|
582
|
-
| B-SR-1 | 安全/注入 | 路径穿越 |
|
|
807
|
+
| ~~B-SR-1~~ | 安全/注入 | 路径穿越 | ✅ **已核实并修复,且实测复现**(§3.24:registry 索引名 `.`/`..` 拼进安装路径 → `{"name":".."}` 清空整个 MINGDAO_HOME。**注意 §3.23 一度误判为「未发现」,已更正**) |
|
|
583
808
|
| A-PI-1 | 安全 | 「无 Key 强制」 | ✅ v0.6.0 C4 已处理(本地/内网端点免 Key) |
|
|
584
809
|
| ~~A-LG-1~~ | 数据完整性 | 哈希链盲区 | ✅ **已核实并修复**(§3.17:盲区是**尾部截断**,实测「只留前 3 行」旧实现报 ok:true。已加封条侧车) |
|
|
585
|
-
| B-CS-1 | 数据完整性 | 竞态丢数据 |
|
|
586
|
-
| B-WS-1/2 | 数据完整性 | 静默吞写入 |
|
|
587
|
-
| B-TOK-1 | 数据完整性 | 词表永不重试 |
|
|
588
|
-
| B-SL-1 | 资源泄漏 | 临时目录 |
|
|
810
|
+
| B-CS-1 | 数据完整性 | 竞态丢数据 | 🟡 **部分落实(有具体进展,仍不宣称穷尽)**:批四把 `cachestats`/`audit`/`log-writer` 的**追加与轮转收进同一把锁**(原来「锁外追加 + 锁内轮转」存在丢失窗口,§3.29)、批七把 `sync-server` 的分享接受改为**锁内重读**并把改密纳入写锁(§3.32);并发读-改-写断言常驻。**仍不宣称「全仓无丢更新」** |
|
|
811
|
+
| ~~B-WS-1/2~~ | 数据完整性 | 静默吞写入 | ✅ **已穷举并清完**(§3.17–§3.21:账本/任务检查点/费用明细/会话索引/工作空间/守护 pidfile/记忆去重/审计日志 共 8 处)。剩余静默吞写已列**受审阅白名单**并由测试常驻约束 |
|
|
812
|
+
| ~~B-TOK-1~~ | 数据完整性 | 词表永不重试 | ✅ **已核实并修复**(§3.25:一次读失败终生锁死 + 无声降级;改为有界重试 + 降级可见) |
|
|
813
|
+
| ~~B-SL-1~~ | 资源泄漏 | 临时目录 | ✅ **已修**(§3.34 BUG-010:拿到 `defect-register.md` 后定位到 `skill-lib.js` 的两处安装器——原来只在「校验失败」这一条早退上 `rmSync`,中途抛异常即**留下整个临时目录**。**§3.23 的「未发现泄漏」结论由此被推翻**:当时的普查只看了「有没有 rmSync」,没看它是否覆盖所有失败路径) |
|
|
589
814
|
| ~~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(发布链路) |
|
|
815
|
+
| ~~B-CMD-4~~ | 凭据泄露 | skill token | ✅ **已定位并修复**(§3.34 BUG-015:`--auth-token` 写在命令行 → argv/shell 历史可见;现支持 `--auth-token=-` 从 stdin 读,字面量给出告警) |
|
|
816
|
+
| ~~D-REL-1/2~~ | 凭据泄露 | gitee token(发布链路) | ✅ **已核实并修复**(§3.22:实测确认 argv / 回显 / URL 三条泄露路径,改走 ssh stdin + HTTP 头鉴权 + 读后即删) |
|
|
592
817
|
|
|
593
|
-
>
|
|
818
|
+
> **2026-09-15 更新**:那份 `defect-register.md` **后来由项目方提供了**(`BUG-001` … `BUG-105`,含 file:line),
|
|
819
|
+
> 本文件的 §3.28–§3.34 就是按它逐条对照处理的(P1-1/P1-2/P1-12、BUG-009、BUG-010、BUG-015、BUG-024、
|
|
820
|
+
> BUG-028/029/030、BUG-036、BUG-056、BUG-067 等)。下面这段「需要那份 register」的原始说明**保留不改**,
|
|
821
|
+
> 作为当时的记录。
|
|
822
|
+
>
|
|
823
|
+
> **当时的原始说明**:要真正排期这 22 项,需要那份 `defect-register.md`(含 file:line);
|
|
594
824
|
> 上表的"待核实"不是"已确认存在",而是**该报告未给出可复现位置**、我尚未逐条定位。
|
|
595
825
|
> 已核实的 B-CT-1 说明了一点:**这份报告的标题级结论可能夸大**("永远 miss"实测不成立),
|
|
596
826
|
> 引用它的条目时应先复现再排期。
|
|
597
827
|
|
|
598
828
|
## 5. 处理顺序与进度
|
|
599
829
|
|
|
600
|
-
**总口径**:三份第三方报告的可执行条目,**已核实并处理
|
|
830
|
+
**总口径**:三份第三方报告的可执行条目,**已核实并处理 35 批**(§2.1–§3.34)。
|
|
601
831
|
每一批都要求:先复现 → 再定级 → 修复 → 回归断言 → **变异验证断言本身**。
|
|
602
|
-
「先复现」对两个方向同样适用:B-CT-1 标题夸大(实测不复现),B-CON-1 / A-LG-1 实测确实严重。
|
|
832
|
+
「先复现」对两个方向同样适用:B-CT-1 标题夸大(实测不复现),B-CON-1 / A-LG-1 / B-SR-1 实测确实严重。
|
|
833
|
+
|
|
834
|
+
> **版本标注(2026-09-21 补记)**:本表与 §3.28–§3.35 里的 `v0.6.3 批一…批九` 是**开发期标记**
|
|
835
|
+
> (提交信息与部分源码注释至今仍是该标记)。v0.6.3 已于 2026-09-15 发版,这九批改动在其之后合入,
|
|
836
|
+
> 因此**实际随 v0.6.4 发布**;用户文档(`docs/CONFIG.md`、`docs/PACK-API.md`)已按实际发布版本更正。
|
|
603
837
|
|
|
604
838
|
**已闭合的类别**:
|
|
605
839
|
|
|
606
840
|
- 三个过程缺陷(§2.1)、缓存跨代理污染(§2.2)、**项目级 Pack 信任门**(§3)
|
|
607
|
-
- **凭据泄露**:`sync passwd` 命令行传密(§3.1 P2-6)、诊断包结构脱敏(§3.3 P2-13
|
|
841
|
+
- **凭据泄露**:`sync passwd` 命令行传密(§3.1 P2-6)、诊断包结构脱敏(§3.3 P2-13)、
|
|
842
|
+
**发布链路 token 进 argv/URL/回显**(§3.22 D-REL-1/2,实测确认三条路径)
|
|
608
843
|
- **静默失效**:出网闸门丢 Request 语义(§3.1 P2-8)、旧模型名静默失效(§3.4)、
|
|
609
|
-
自启文件未转义(§3.5)、MCP 预设参数(§3.6)、**账本写失败无声 + 尾部截断不可检出**(§3.17
|
|
844
|
+
自启文件未转义(§3.5)、MCP 预设参数(§3.6)、**账本写失败无声 + 尾部截断不可检出**(§3.17)、
|
|
845
|
+
**任务检查点/费用明细/会话索引/工作空间/守护 pidfile/记忆去重/审计日志的"写失败却报成功"**(§3.18–§3.21)、
|
|
846
|
+
**词表读失败终生锁死**(§3.25)
|
|
847
|
+
- **计费与可用性**(v0.6.3 批八):非幂等请求不再重试(避免重复计费)、总量护栏在循环头复查、
|
|
848
|
+
退避可被 Ctrl+C 打断、`compactTrigger` 越界不再让压缩永久失效、降级回合按实际模型计价、
|
|
849
|
+
提前关流可检出、content-type 被改写不再丢正文
|
|
850
|
+
- **上游能力与文档**(v0.6.3 批九):安装器临时目录 finally 清理、检查点路径穿越设防、
|
|
851
|
+
令牌不进 argv(`--auth-token=-` 从 stdin 读)、文档与实现逐处对齐并有断言钉住
|
|
852
|
+
- **供应链**(v0.6.3 批六):`pack verify` 默认静态(CI 不再执行被审代码)、未信任 Pack 不遮蔽同名内置、
|
|
853
|
+
预设遮蔽可见、git 安装器拒 `file://` 与本地路径
|
|
854
|
+
- **长驻与并发**(v0.6.3 批七):同步服务不再因未处理的 rejection 整体退出、锁外快照写回(分享/吊销复活)、
|
|
855
|
+
调度恢复不自锁也不双跑、pid 复用下锁可回收、避峰长等待切片复查租约
|
|
856
|
+
- **Web 攻击面**(v0.6.3 批五):符号链接逃逸围栏(P0-3)、回环绑定跳过 SSRF(P1-8)、
|
|
857
|
+
无令牌 + GET 副作用可被跨站盲打(H-5/H-6)、自定义 Provider 名穿越 import(R4)
|
|
858
|
+
- **静默数据损失**(v0.6.3 批四):并发轮转吃掉别人那一行(`cachestats` BUG-009 / `audit`+`log-writer` M-20)、
|
|
859
|
+
压缩后读取去重缓存说谎(M-1)、`/compact` 追加导致会话文件膨胀(H-4)、
|
|
860
|
+
配置"读不出来"被当成"没有配置"而静默覆盖(H-7,含 BOM 这一支)
|
|
861
|
+
- **合规能力空转**(v0.6.0 主打的账本 / 回放 / 领域约束):
|
|
862
|
+
约束事件缺 `id` → 「是哪条红线拦的」恒为 null(§3.28 P1-1)、`confirm` 从不求值(P1-2)、
|
|
863
|
+
CLI 下 `ledger replay` 恒看不见 Pack 约束 → 门禁静默假阴性(P1-12)、
|
|
864
|
+
`ledger replay --json` 恒输出人读文本(M-12)、五条失败路径退 0(M-21)
|
|
610
865
|
- **终端注入**:控制序列过滤(§3.1 P2-5)
|
|
611
866
|
- **记忆注入**:项目记忆持久化围栏(§3.2 P2-9)
|
|
612
867
|
- **日志/调度/锁**:写入与轮转按大小触发(§3.7)、进程归属校验与整组清理(§3.8)、
|
|
613
|
-
`/proc` 命令行归一(§3.8 附)、锁陈旧判据看持有者 pid(§3.10
|
|
868
|
+
`/proc` 命令行归一(§3.8 附)、锁陈旧判据看持有者 pid(§3.10)、
|
|
869
|
+
**锁的阻塞面:异步锁 + 请求路径迁移 + 临界区收窄 + 超时上限**(§3.26–§3.27)
|
|
614
870
|
- **能力声明不一致**:deny 按段匹配 / git 缩写前缀 / permissions 说实话 / 忙锁键迁移(§3.11)
|
|
615
871
|
- **安全**:SSRF 逐跳复检单一来源(§3.12)、token 计数器 identity(§3.13)、
|
|
616
|
-
约束 pattern 防灾难性回溯(§3.16)、`hooks.js` shell:true 核实为**非缺陷**(§3.16
|
|
872
|
+
约束 pattern 防灾难性回溯(§3.16)、`hooks.js` shell:true 核实为**非缺陷**(§3.16)、
|
|
873
|
+
**路径穿越实测复现并修复**(§3.24 B-SR-1)、**stdout EPIPE 崩溃**(§3.20)
|
|
874
|
+
- **发布纪律固化**:四平台(GitHub + Gitee + GitCode + npm)一致性由
|
|
875
|
+
`scripts/verify-release.mjs` 在发版最后一步自动校验;v0.6.2 已按该流程发布(四渠道校验退出 0)
|
|
617
876
|
|
|
618
877
|
**未闭合(如实登记,不假装完成)**:
|
|
619
878
|
|
|
@@ -622,9 +881,13 @@ Windows 对这种情况回报的正是 **ENOENT**,与"本来就没有检查点
|
|
|
622
881
|
`memory`(去重) / `audit` **已修**;`titles` 复核为**非缺陷**(调用点已判空)、
|
|
623
882
|
`compact` 复核为**正常重试写法**。剩余 18 处已在 §3.21 的白名单里**逐条写明可以吞的理由**,
|
|
624
883
|
并由测试常驻约束——新增即失败。**这一类不再靠人工记忆。**
|
|
625
|
-
2.
|
|
626
|
-
|
|
627
|
-
|
|
884
|
+
2. ~~**`withFileLockSync` 的阻塞面**(自评 P2-7)~~ 🟡 **已处理到"可接受"**:
|
|
885
|
+
§3.26 新增异步版并把 **WebUI 请求路径**上的 `workspace`(7 处)迁过去;
|
|
886
|
+
§3.27 把 `tasks` 的进程操作移出临界区、把同步锁超时上限 20s → 5s(依实测),
|
|
887
|
+
并给剩余 **15 处**同步锁列出受审阅清单(含理由,新增即测试失败)。
|
|
888
|
+
**仍有 15 处在极端争用下会阻塞其所在进程**——但已核实:它们要么不在请求路径上
|
|
889
|
+
(`schedule` 在守护进程内、`sync` 是 CLI 一次性命令、`cachestats` 仅在 >4MB 轮转时),
|
|
890
|
+
要么临界区已缩到毫秒级。**不宣称"阻塞面已彻底消除"**,只说"已知残留均为可接受并受清单约束"。
|
|
628
891
|
3. ~~**EPIPE 未处理**(B-UI-1 / B-CLI-1 / B-REPL-1)~~ ✅ **已修**(§3.20:实测复现并加了两种策略的兜底)。
|
|
629
892
|
4. ~~**readline 未 close**(B-CMD-1/2)~~ ✅ **已普查完毕**:全仓仅 3 处 `createInterface`
|
|
630
893
|
(`ui.js` / `commands/sync.js` / `commands/skill.js`),**都已有 `close()`**;
|
|
@@ -634,13 +897,18 @@ Windows 对这种情况回报的正是 **ENOENT**,与"本来就没有检查点
|
|
|
634
897
|
frontmatter 校验直接进 `rmSync`,`{"name":".."}` 会清空整个 `MINGDAO_HOME`)。
|
|
635
898
|
注意 §3.23 当时误判为"未发现",§3.24 已更正并说明原因。
|
|
636
899
|
6. ~~**词表永不重试**(B-TOK-1)~~ ✅ **已定位并修复**(§3.25:`loadData` 一次失败终生锁死 + 无声降级)。
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
900
|
+
~~**临时目录**(B-SL-1)~~ ✅ **已定位并修复**(§3.34 BUG-010)。**这里更正一次我自己的结论**:
|
|
901
|
+
当时写的是「已普查全仓 3 处 `mkdtempSync`,均有 `rmSync`。**未发现泄漏**」——
|
|
902
|
+
拿到 `defect-register.md` 后发现真正的缺陷不在"有没有 `rmSync`",而在**它没覆盖所有失败路径**
|
|
903
|
+
(只在"校验失败"这一条早退上删,中途抛异常就留下整个目录)。
|
|
904
|
+
**普查得出"没有泄漏"和验证"失败路径也清理"是两件事**——这是本项目反复遇到的同一类教训。
|
|
905
|
+
~~**skill token**(B-CMD-4)~~ ✅ **已定位并修复**(§3.34 BUG-015:它就是 `mingdao web --auth-token`
|
|
906
|
+
写在命令行导致令牌进 argv/`ps`/shell 历史;当时的"全仓搜不到 skill token"是因为按报告的措辞去找名字,
|
|
907
|
+
而不是按**行为**去找)。
|
|
641
908
|
~~gitee token 发布链路(D-REL-1/2)~~ ✅ **已修**(§3.22)。
|
|
642
|
-
7. **§4.3 `audit-report.md` 其余约 15 项**:该报告只给了 22
|
|
643
|
-
|
|
909
|
+
7. **§4.3 `audit-report.md` 其余约 15 项**:该报告只给了 22 个**代号**,
|
|
910
|
+
明细在 `30-defects/defect-register.md` 里——**该文件后来由项目方提供了**(`BUG-001`…`BUG-105`),
|
|
911
|
+
因此 §3.28–§3.34 已能按 file:line 逐条对照处理;本条的原始措辞保留不改,作为当时的记录。
|
|
644
912
|
已核实的 4 条(B-CT-1 / B-CON-1 / B-HK-1 / A-LG-1)说明它**既有夸大也有真货**,
|
|
645
913
|
所以对它既不能整份照单排期、也不能整份忽略——只能逐条复现。
|
|
646
914
|
8. **账本可选签名**(`--sign-key`):仍未实现。封条只提升到「防误删/漏写」,
|
package/docs/CONFIG.md
CHANGED
|
@@ -52,7 +52,8 @@ MingDao 先用 executor 模型(路由关闭时为当前模型)把被裁段
|
|
|
52
52
|
|
|
53
53
|
触发线(滞回缓冲):默认达到预算 **80%** 即提前压缩、压到约 60%——避免在预算线附近反复
|
|
54
54
|
裁剪/压缩导致缓存前缀频繁失效(每次失效 = 该轮 prompt 全额按未命中计费)。可用
|
|
55
|
-
`"compactTrigger": 0.9` 调整触发线(0–1
|
|
55
|
+
`"compactTrigger": 0.9` 调整触发线(0–1 之间的小数;**超出 0–1 会被夹到边界并告警**——
|
|
56
|
+
此前只夹下限,配成 >1 会让自动压缩永不触发)。
|
|
56
57
|
|
|
57
58
|
## 会话检索索引(P3-2)
|
|
58
59
|
|
|
@@ -68,6 +69,17 @@ WebUI 中每个会话记住自己的工作目录:新会话记录创建时的
|
|
|
68
69
|
`process.chdir`)。载入历史会话时全局工作空间自动聚焦到该会话的目录;头部下拉显式切换
|
|
69
70
|
时当前会话跟随。映射存于 `~/.mingdao/session-workspaces.json`,会话改名/删除自动维护。
|
|
70
71
|
|
|
72
|
+
**目录围栏(v0.6.4 起按真实路径判定)**:可登记/可浏览的目录限定在 家目录 / 启动目录 /
|
|
73
|
+
当前工作空间 / `web.browseRoots` 显式授权 之内(Windows 上为 桌面·文档·下载 三个常用目录)。
|
|
74
|
+
判定走 `realpath`,因此:
|
|
75
|
+
|
|
76
|
+
- **符号链接不会成为逃生通道**——指向围栏之外的链接目录会被 403(此前只做字符串前缀比较,
|
|
77
|
+
`ln -s / <家>/escape` 之后 `?dir=…/escape` 就能枚举全盘);
|
|
78
|
+
- 登记与切换返回的是**规范化后的真实路径**(macOS 上 `/var/folders/…` 会显示为
|
|
79
|
+
`/private/var/folders/…`),围栏判定与返回值同口径;
|
|
80
|
+
- 确需家目录之外的位置(外置卷/网络盘):`web.browseRoots` 加进白名单,或
|
|
81
|
+
`web.allowAnyWorkspaceDir: true`(**这会整体放开围栏**,仅在完全可信的本机环境使用)。
|
|
82
|
+
|
|
71
83
|
## 权限规则(工具级 allow/deny)
|
|
72
84
|
|
|
73
85
|
```json
|
|
@@ -82,7 +94,16 @@ WebUI 中每个会话记住自己的工作目录:新会话记录创建时的
|
|
|
82
94
|
|
|
83
95
|
- `allow` / `deny` 为工具名列表,支持「工具名:参数前缀」匹配(如 `bash:git *` 只放行 git 命令);
|
|
84
96
|
- `deny` 优先于 `allow`;匹配不到时回落到 `mode`(`ask` 逐次确认);
|
|
85
|
-
- **需要特殊授权时弹窗交互**:被 `deny` 规则拦截、或 `readonly` 模式下执行写操作时,WebUI/TUI
|
|
97
|
+
- **需要特殊授权时弹窗交互**:被 `deny` 规则拦截、或 `readonly` 模式下执行写操作时,WebUI/TUI 会弹出询问(「是否本次强制放行?」),同意即放行、拒绝/无响应即拒绝——不再静默拦截。提示里会**点名命中了哪条规则**,便于判断这次放行是不是你想要的;
|
|
98
|
+
- **`denyStrict: true`(v0.6.4 起)**:把 `deny` 升级为**不可临时放行**的硬拦截(连弹窗都没有,直接拒绝并在输出里说明命中了哪条规则)。默认 `false`,保持上面那条"可放行"的既有语义。
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{ "permission": { "mode": "auto", "deny": ["bash:rm *", "fetch:*"], "denyStrict": true } }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> 什么时候该开:无人值守 / 强合规场景(定时任务、CI 门禁、下游容器),
|
|
105
|
+
> 那里"一次误按 y 就绕过自己写的禁令"是不可接受的;
|
|
106
|
+
> 交互式日常使用保持默认即可——每次放行都是一次明确的、点名规则的确认。
|
|
86
107
|
|
|
87
108
|
## 项目记忆(自动写入项目目录,默认已自忽略)
|
|
88
109
|
|
|
@@ -202,12 +223,17 @@ mingdao net report --since 7d --json # 支持 7d / 24h / 90m
|
|
|
202
223
|
}
|
|
203
224
|
```
|
|
204
225
|
|
|
205
|
-
- 仅本机使用保持默认 `127.0.0.1
|
|
226
|
+
- 仅本机使用保持默认 `127.0.0.1`(**本机信任模式**:不校验令牌)。启动横幅会明确提示这一点——
|
|
227
|
+
同机的其它进程/其它用户可以直接访问 `/api/*`,多用户机器与共享 CI 上请启用令牌;
|
|
206
228
|
- 绑定 `0.0.0.0`/局域网地址时**强制令牌认证**:未配置则每次启动随机生成并打印
|
|
207
|
-
`http://<地址>:<端口>/?token=…`
|
|
208
|
-
`mingdao web --auth-token
|
|
229
|
+
`http://<地址>:<端口>/?token=…` 访问链接;固定令牌(优先级从高到低):
|
|
230
|
+
环境变量 `MINGDAO_WEB_TOKEN`、`web.token`、`mingdao web --auth-token <令牌>`(或 `--auth-token=-` 从 stdin 读);
|
|
231
|
+
**命令行字面量会进入 argv 与 shell 历史(`ps` 可见)**,故用它时会给出告警;
|
|
209
232
|
- 令牌同时接受 URL `?token=`、请求头 `X-MingDao-Token` 或 `Authorization: Bearer`;
|
|
210
|
-
- 服务端校验 `Host` 头必须等于回环名或绑定地址(防 DNS rebinding),代理场景会 403
|
|
233
|
+
- 服务端校验 `Host` 头必须等于回环名或绑定地址(防 DNS rebinding),代理场景会 403 属预期;
|
|
234
|
+
- **跨站浏览器请求一律 403**(按 `Sec-Fetch-Site` 判定,含 `<img>`/`no-cors` 盲打):
|
|
235
|
+
本机端口不受同源策略保护,这是"任意网页盲打本机 API"的唯一有效拦截点;
|
|
236
|
+
- 云元数据端点(`169.254.169.254` 等)在任何绑定与任何开关下**都拒绝**。
|
|
211
237
|
|
|
212
238
|
## 沙箱环境变量过滤
|
|
213
239
|
|
package/docs/DEVELOPER.md
CHANGED
|
@@ -90,7 +90,7 @@ Preset 定制的是「提示词 + 工具白名单 + 权限」;**Pack 定制的
|
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
mingdao pack new tcm # 脚手架:pack.json + pack.mjs + prompts/domain.md
|
|
93
|
-
mingdao pack verify ./packs/tcm #
|
|
93
|
+
mingdao pack verify ./packs/tcm # 静态契约校验(下游 CI 门禁:非 0 退出即失败;**不执行 Pack 代码**)
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
最小 `pack.mjs`:
|
package/docs/PACK-API.md
CHANGED
|
@@ -219,18 +219,35 @@ export function createPack(ctx) {
|
|
|
219
219
|
|
|
220
220
|
| 时机 | 可用的 kind | 行为 |
|
|
221
221
|
| --- | --- | --- |
|
|
222
|
-
| PreToolUse | `tool-deny`、`tool-arg-require`、`arg-forbid` |
|
|
222
|
+
| PreToolUse | `tool-deny`、`tool-arg-require`、`arg-forbid`、`confirm` | 前三个命中即阻止执行,回填工具错误给模型,写审计;`confirm` 见下方说明 |
|
|
223
223
|
| PostToolUse | `completeness`、`result-forbid` | `completeness` 缺项 → 拒绝该工具结果,要求模型补采;`result-forbid` → 屏蔽结果并提示 |
|
|
224
224
|
| 输出前 | `output-forbid`、`require-citation` | 命中 → 按 `action` 处理:`block`(改为固定合规文案)/ `block-and-rewrite`(再请求一次修正)/ `warn`(放行并标注) |
|
|
225
225
|
|
|
226
|
+
**`confirm`(v0.6.4 起真正求值)**:即使在 `auto` 权限档位下,命中的工具调用也**必须**先得到人工确认才执行
|
|
227
|
+
——它的存在意义正是「权限已经放行,领域还要再问一次」。两种情形一律按 fail-closed 阻止:
|
|
228
|
+
|
|
229
|
+
- 用户答否;
|
|
230
|
+
- 当前环境**无法交互**(`-p` 单次提问、调度任务、子 Agent 等)——拿不到确认即不执行,
|
|
231
|
+
与权限引擎在同类情形下「按拒绝处理」保持同一口径。
|
|
232
|
+
|
|
233
|
+
求值顺序上,`tool-deny` / `tool-arg-require` / `arg-forbid` **永远先于** `confirm`,
|
|
234
|
+
因此把 `confirm` 写在 `tool-deny` 之前不会遮住后者。
|
|
235
|
+
|
|
236
|
+
> `confirm` 在 v0.5.0–v0.6.2 之间是 `KINDS` 里的一个字符串:能通过装载校验、会被计入约束条数
|
|
237
|
+
> (于是 `active=true`),但引擎里没有任何一处求值它——作者以为「这条工具要人工确认」,实际零效果。
|
|
238
|
+
> 这是「红线静默消失」,与本文档的 fail-closed 声明相反;v0.6.4 补齐实现并补了回归断言。
|
|
239
|
+
|
|
226
240
|
约束事件统一结构(进执行账本):
|
|
227
241
|
|
|
228
242
|
```jsonc
|
|
229
|
-
{ "at": 1757…, "pack": "tcm", "constraint": "no-diagnosis-conclusion",
|
|
243
|
+
{ "at": 1757…, "id": "no-diagnosis-conclusion", "pack": "tcm", "constraint": "no-diagnosis-conclusion",
|
|
230
244
|
"kind": "output-forbid", "stage": "pre-output", "action": "block-and-rewrite",
|
|
231
245
|
"matched": "好转", "session": "…", "model": "deepseek-v4-flash" }
|
|
232
246
|
```
|
|
233
247
|
|
|
248
|
+
> `id` 自 v0.6.4 起写入事件。此前事件只有 `constraint` 字段,而账本与回放两处消费方都读 `id`,
|
|
249
|
+
> 于是「是哪条红线拦的」在账本里恒为 `null`——不报错、只输出 null,任何断言都不会失败。
|
|
250
|
+
|
|
234
251
|
**字段要求(v0.6.0 起在装载时强制校验,写错不会静默失效)**
|
|
235
252
|
|
|
236
253
|
| kind | 必填 | 说明 |
|
|
@@ -241,6 +258,7 @@ export function createPack(ctx) {
|
|
|
241
258
|
| `output-forbid` | `pattern`、`action` | 同上 |
|
|
242
259
|
| `result-forbid` | `tool`、`pattern` | 同上 |
|
|
243
260
|
| `completeness` | `tool`、`fields[]` | |
|
|
261
|
+
| `confirm` | `tool` | 无 `tool` 的 `confirm` 在 `toolMatches` 里恒不命中,等于红线不存在,故装载即拒绝 |
|
|
244
262
|
|
|
245
263
|
**pattern 写错会怎样(这一条值得单独读)**:v0.5.0 的行为是**静默放行**——
|
|
246
264
|
`arg-forbid` 给一个非法正则时求值失败、`re` 为 null,红线**永不命中**且不进 `invalid` 列表,
|
|
@@ -323,6 +341,10 @@ manifest 可声明 `budget.dailyYuan` + `budget.action`。超限时 `ctx.llm()`
|
|
|
323
341
|
|
|
324
342
|
**下游义务**:在 `pack.json` 声明 `apiVersion` 与 `engines.mingdao`,并把 `mingdao pack verify` 放进 CI。
|
|
325
343
|
|
|
344
|
+
> v0.6.4 更正:此前帮助文本写的是"只做静态校验",实现却会 `import pack.mjs` —— 于是按文档把
|
|
345
|
+
> `pack verify` 当 CI 门禁,等于**在 CI 上执行被审仓库的任意 Node 代码**(且不经 `pack trust` 信任门)。
|
|
346
|
+
> 现在默认确实是静态的;约束与提示词段的合法性由代码产出,静态阶段无法校验,需要那种覆盖请显式加 `--runtime`。
|
|
347
|
+
|
|
326
348
|
**上游义务**:任何 Pack API 变更必须同步更新本文 + 兼容性矩阵 + `docs/CHANGELOG-PACK.md`。
|
|
327
349
|
|
|
328
350
|
---
|
|
@@ -331,7 +353,8 @@ manifest 可声明 `budget.dailyYuan` + `budget.action`。超限时 `ctx.llm()`
|
|
|
331
353
|
|
|
332
354
|
```bash
|
|
333
355
|
mingdao pack list # 已加载 Pack / 来源 / 版本 / 兼容状态
|
|
334
|
-
mingdao pack verify <dir> #
|
|
356
|
+
mingdao pack verify <dir> # **静态**校验(manifest + 文件齐全 + pack.mjs 存在性)——**不执行 Pack 代码**,下游 CI 门禁用这个
|
|
357
|
+
mingdao pack verify <dir> --runtime # 额外 import pack.mjs 验证运行时契约(会以完整 Node 权限执行 Pack 代码,仅在你审过代码时用)
|
|
335
358
|
mingdao pack new <name> # 脚手架
|
|
336
359
|
mingdao pack info <name> # 贡献面:工具/约束/提示词段/权限/费用统计
|
|
337
360
|
mingdao pack test <name> # 跑内置反例样本(约束 + 工具契约)
|