mingdao-harness 0.6.10 → 0.6.12
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.en.md +1 -0
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CODE-SIGNING.md +103 -1
- package/docs/CONFIG.md +173 -2
- package/docs/DEVELOPER.md +34 -1
- package/package.json +1 -1
- package/presets/local-model.json +8 -0
- package/presets/readonly-audit.json +10 -0
- package/src/agent.js +496 -111
- package/src/commands/ledger.js +93 -9
- package/src/cost-guard.js +78 -0
- package/src/help.js +7 -0
- package/src/ledger.js +406 -11
- package/src/model-discovery.js +503 -2
- package/src/presets.js +86 -8
- package/src/tools/index.js +130 -14
- package/src/tools-flow.js +131 -0
- package/src/web/app.js +153 -18
- package/src/web/index.html +4 -3
- package/src/web/routes/domains/misc.js +61 -9
- package/src/web/server.js +277 -22
- package/presets/local-audit.json +0 -10
package/README.en.md
CHANGED
|
@@ -111,6 +111,7 @@ Commands are grouped here for evaluation; `mingdao --help` and `src/help.js` are
|
|
|
111
111
|
The full reference is **Chinese**; these are the entry points worth knowing:
|
|
112
112
|
|
|
113
113
|
- [`docs/CONFIG.md`](docs/CONFIG.md) — every `config.json` field: permission rules, sandbox, `config.net`, hooks, MCP, sync, cost guard.
|
|
114
|
+
- [`docs/CONFIG.en.md`](docs/CONFIG.en.md) — English configuration reference (verified against the code): permissions, sandbox & env filtering, egress gate and its limits, cost guard, WebUI exposure, audit/ledger, full field table.
|
|
114
115
|
- [`docs/PACK-API.md`](docs/PACK-API.md) — the Pack contract (manifest, contributions, constraint kinds, and which of them are **not** implemented).
|
|
115
116
|
- [`docs/PROVIDERS.md`](docs/PROVIDERS.md) — built-in providers and writing a custom OpenAI-compatible provider.
|
|
116
117
|
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — module map and data flow.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MingDao-Harness 架构设计
|
|
2
2
|
|
|
3
|
-
> 版本口径:v0.6.8。本文档覆盖 `src/` 全部 **
|
|
3
|
+
> 版本口径:v0.6.8。本文档覆盖 `src/` 全部 **89** 个模块(`find src -name '*.js' | wc -l` = 89),
|
|
4
4
|
> 按层给出每个模块的职责与它守住的那条不变量;判定以文件头注释与实际实现为准,不做推断性描述。
|
|
5
5
|
>
|
|
6
6
|
> 阅读顺序建议:§2 模块索引(查「某件事归谁管」)→ §3 数据流(看一次回合怎么走)→ §4 已知边界(看它**保证不了**什么)。
|
|
@@ -76,9 +76,10 @@ MingDao 是一个「模型循环 + 工具 + 权限」内核,纯 Node.js ≥ 18
|
|
|
76
76
|
- `cost-guard.js` —— 费用护栏:按北京时间自然日累计真实费用,`warn` 线提醒、`limit` 线按 `action` 处理(`warn` / `block` / `downgrade` 到便宜模型)—— 长回合把在途费用并入今日已用,否则统计文件不变会烧穿日限。
|
|
77
77
|
- `batch.js` —— Batch API 半价通道:单轮批量任务(无工具、无流式)走 OpenAI 兼容批处理协议(`/files` → `/batches` → 轮询 → 下载结果,DeepSeek 风格结果端点回退 OpenAI)—— 端点不可用(404/405)必须明确报错告知网关不支持,**绝不静默假装成功**;计费按闲时全未命中 × 0.5,结果记入 `cachestats.js`(`batch: true`)。
|
|
78
78
|
|
|
79
|
-
### 2.3 工具与围栏(
|
|
79
|
+
### 2.3 工具与围栏(13)
|
|
80
80
|
|
|
81
81
|
- `tools/index.js` —— 工具注册表:模型可见的工具 Schema(OpenAI function-calling 格式)与执行分发器,含第三方 `registerTool` 与 `config.tools` 挂载 —— Schema 与 dispatch 分支必须成对增加。
|
|
82
|
+
- `tools-flow.js` —— 工具编排两端的判据(v0.6.11,审计 P1-1 拆分第三刀,纯函数):`visibleToolsFor()`(只读档 × 预设白名单 × 本会话已用工具快照 × MCP 只读标注 → 本轮**发给模型哪些工具**)、`serializeToolResult()` / `toolResultMessage()`(字符串原样 / 对象紧凑 JSON × 复用前缀 × 领域约束拒绝 × 按窗口截断 → 工具结果**怎么回填**)以及 `READONLY_TIER_SET`(`agent.js` 原样再导出,导入面不变)—— 打印/审计/账本/`messages.push` 等副作用仍留在 `agent.js`,判据只此一份,结构守卫见 `test/smoke.js` §136。
|
|
82
83
|
- `tools/fs-tools.js` —— 文件系统工具 read / write / edit / ls / glob / grep:统一返回 `{ok, output?|error?}` —— 工作空间围栏 `withinRoot()` 对悬空软链递归复检、深度超 16 层 fail-closed;写/编辑自动备份供 undo。
|
|
83
84
|
- `tools/bash.js` —— bash 工具:子进程执行、超时强杀、输出截断;沙箱三档 `off` / `readonly`(全盘只读 + /tmp tmpfs)/ `safe`(只读文件系统 + `--unshare-net` 断网 + 工作目录可写)—— 仅 Linux + bubblewrap 可用,缺失时降级为 `off` 并在结果中注明,不静默假装沙箱。
|
|
84
85
|
- `tools/fetch.js` —— HTTP 只读抓取工具:GET 任意 http(s) URL,返回文本(512KB 上限,正文截 20K)—— 只保留工具契约(参数校验、呈现口径、错误措辞),实际抓取整条走 `safeFetchText`。
|
package/docs/CODE-SIGNING.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
**当前构建为未签名**(官网已注明绕过方法);按本指南拿到证书后,把对应密钥配成
|
|
5
5
|
环境变量即可,无需改代码。
|
|
6
6
|
|
|
7
|
+
> 自动更新包的**来源签名**是**另一件事**(防"更新源被投毒",不是防 SmartScreen),见 §五:
|
|
8
|
+
> 代码已就位且判据 fail-closed,但**发布链路尚未接入**——默认档为 `warn-and-install`
|
|
9
|
+
> (照旧安装,但 UI/日志明确写出"本次更新未验证来源签名")。
|
|
10
|
+
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
## 一、Windows 代码签名(二选一)
|
|
@@ -85,8 +89,106 @@ macOS 作业的 `--mac dmg` 在检测到 `CSC_LINK` + Apple 凭证时自动完
|
|
|
85
89
|
概念:域名 OV 是 SSL/TLS 证书(绑定域名、EKU=Server Authentication、给 HTTPS 用);
|
|
86
90
|
代码签名 OV 绑定组织身份(EKU=Code Signing 1.3.6.1.5.5.7.3.3、给软件签名用),
|
|
87
91
|
常以 USB Key 交付,互不可替代。下单务必确认 EKU 为 Code Signing。
|
|
88
|
-
-
|
|
92
|
+
- **不想签**:现状即可用(官网已附绕过说明),仅多一步确认。
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 五、更新包的**来源签名**(ed25519,与代码签名是两件事)
|
|
97
|
+
|
|
98
|
+
> **先分清两件事**:上面一~四节是**代码签名**(证书 + Gatekeeper / SmartScreen,回答"这个 App 的
|
|
99
|
+
> 制作者是谁、系统要不要拦");本节是**更新包的来源签名**(回答"这个自动更新包是不是官网发的")。
|
|
100
|
+
> 两者互不替代:代码签名不能阻止应用从被投毒的更新源装一个**同样签了名**的包,来源签名也不负责
|
|
101
|
+
> 消除 SmartScreen 提示。
|
|
102
|
+
>
|
|
103
|
+
> **本轮状态(诚实标注)**:代码已就位并 fail-closed,但**发布链路尚未接入签名**——
|
|
104
|
+
> `desktop/gen-update-yml.mjs` 与服务器发布脚本都没有签名步骤,feed 里**没有** `.sig` 文件。
|
|
105
|
+
> 因此客户端当前实际走的是**默认档 `warn-and-install`**:照旧安装,但在日志与 UI 里明确写出
|
|
106
|
+
> 「**本次更新未验证来源签名**」。要让"没签名就拒绝"生效,发行方需要:① 按下面步骤接入签名;
|
|
107
|
+
> ② 在客户端设 `MINGDAO_REQUIRE_UPDATE_SIGNATURE=1`(或改成默认值)。
|
|
108
|
+
|
|
109
|
+
### 5.1 为什么不是"再算一次 sha512"
|
|
110
|
+
|
|
111
|
+
`electron-updater` 自带的 sha512 校验,比的是「下载到的字节」与「**同一份 feed 提供的** `latest*.yml`
|
|
112
|
+
里的 sha512」——清单与包**同源**。控制 feed 的一方(DNS / 主机 / CDN / 证书任一被攻破)可以同时换掉包
|
|
113
|
+
与清单里的 sha512,校验照样通过,随后 `quitAndInstall()` 执行攻击者的安装包。
|
|
114
|
+
|
|
115
|
+
这不是推测,是本仓探针(未入库:`/tmp/probe-update-feed.mjs`,本机 HTTP 投毒 feed + 桩 `electron-updater`,
|
|
116
|
+
跑的是**真实的** `desktop/main.js`)实测过的:投毒 feed + 攻击者自己算的 sha512 → 下载 → 校验 MATCH →
|
|
117
|
+
安装被调用;只改字节不改清单(传输损坏)→ sha512 mismatch → 不安装。**即 sha512 只防传输损坏,不防换源。**
|
|
118
|
+
|
|
119
|
+
来源签名补的正是后者:签名用**只有发行方持有**的私钥生成,客户端只内置**公钥**。攻击者控制了 feed
|
|
120
|
+
也造不出签名(他自己签的包会被内置公钥判无效)。
|
|
121
|
+
|
|
122
|
+
### 5.2 密钥托管(私钥留在离线机 / CI secret)
|
|
123
|
+
|
|
124
|
+
| 项 | 约定 |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| 算法 | ed25519(`node:crypto`,零依赖;与 `src/ledger.js` 的账本来源签名同一套约定) |
|
|
127
|
+
| 私钥位置 | **离线机**或 CI secret(如 `MINGDAO_UPDATE_SIGN_KEY` 指向的文件)。**绝不入库、绝不打印、绝不进聊天/工单/截图**(`--keygen` 只打印公钥) |
|
|
128
|
+
| 私钥文件权限 | `600`(`--keygen` 自动设置;Windows 无 POSIX 权限位,用目录权限隔离) |
|
|
129
|
+
| 公钥位置 | 贴进 `desktop/update-verify.js` 的 `UPDATE_PUBLIC_KEY_PEM` 常量(**内置常量**,运行期不从网络取——从网络取公钥等于没 pin) |
|
|
130
|
+
| 密钥轮换 | 换密钥 = 所有旧签名失效。`--keygen` **拒绝覆盖**已存在的密钥文件(必须先手工移走),避免"悄悄换了信任根" |
|
|
131
|
+
| 本轮内置的那把公钥 | `keyId=d1e7bef80ca5d508`。它对应的私钥在本轮**未保留**(生成后即弃,从未写盘/打印)——所以"验签通过"这条路径在当前发布链路下不会被走到;它保证的是:**任何别人签的包一律验不过** |
|
|
132
|
+
|
|
133
|
+
### 5.3 发布步骤(发行方启用签名时)
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# ① 生成密钥对(在离线机 / 受控环境;私钥写仓库之外,脚本会拒绝写进仓库)
|
|
137
|
+
node scripts/update-sign.mjs --keygen --out ~/.mingdao/update-signing
|
|
138
|
+
# → 把打印出来的**公钥** PEM 贴进 desktop/update-verify.js 的 UPDATE_PUBLIC_KEY_PEM
|
|
139
|
+
|
|
140
|
+
# ② 每次发版:对每个平台的安装包签名(产出 <包>.sig)
|
|
141
|
+
node scripts/update-sign.mjs --sign desktop/dist/mingdao-setup-<ver>-x64.exe
|
|
142
|
+
node scripts/update-sign.mjs --sign desktop/dist/mingdao-<ver>-x86_64.AppImage
|
|
143
|
+
# → 打印的 sha512 必须与 latest*.yml 里的 sha512 **逐字一致**(不一致说明签错了文件)
|
|
144
|
+
|
|
145
|
+
# ③ 发布前自检(与客户端**同一实现**,避免"发布侧说通过、客户端说无效")
|
|
146
|
+
node scripts/update-sign.mjs --verify desktop/dist/mingdao-setup-<ver>-x64.exe --pub update-signing-pub.pem
|
|
147
|
+
|
|
148
|
+
# ④ 上传:安装包 + 它的 .sig **一起**放进 feed 目录(官网 /updates/,与 latest*.yml 同级)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
判据与文案在 `desktop/update-verify.js`(纯函数,`test/smoke.js` §135 逐条钉边界):
|
|
152
|
+
|
|
153
|
+
| 情形 | 决策 | 行为 |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| 有签名 + 验签通过 | `install` | 提示"来源签名校验通过(ed25519 · keyId=…)",正常安装 |
|
|
156
|
+
| **有签名 + 验签失败 / 拿不到证据** | `reject` | **一律拒绝**:UI"更新包来源不可信,已拒绝"+ 引导官网手动下载;并**关闭 `autoInstallOnAppQuit`**(否则退出应用时照样装) |
|
|
157
|
+
| 无签名 + 默认 | `warn-and-install` | 照旧安装,但日志与 UI 都写明"**本次更新未验证来源签名**"、并说明当前发布链路没有签名步骤 |
|
|
158
|
+
| 无签名 + `MINGDAO_REQUIRE_UPDATE_SIGNATURE=1` | `reject` | fail-closed:发行方/高安全用户的开关 |
|
|
159
|
+
|
|
160
|
+
**为什么不默认 reject**:当前发布链路还没有签名,默认 reject 等于一夜之间掐死所有存量用户的自动更新。
|
|
161
|
+
威胁模型里"feed 被投毒"是低概率高影响,"更新永远装不上"是必然发生——所以默认放行但**如实告知**,
|
|
162
|
+
把 fail-closed 开关交给发行方。发布链路接入签名后,把 `decideUpdatePolicy` 的默认档改成 `reject`
|
|
163
|
+
即可(一行,判据与文案不用动)。
|
|
164
|
+
|
|
165
|
+
### 5.4 失败处置
|
|
166
|
+
|
|
167
|
+
| 现象 | 原因 | 处置 |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| 用户看到"**更新包来源不可信,已拒绝**" | 签名无效(包被改 / 由别的密钥签发)或 `REQUIRE=1` 而没有签名 | 让用户到官网手动下载;发行方核对 `latest*.yml` 里的 sha512 与 `--sign` 打印的 sha512 是否一致、feed 里是否放上了对应的 `.sig` |
|
|
170
|
+
| 用户看到"更新已就绪(**未验证来源签名**)" | 默认档 + feed 没有 `.sig` | 属预期(见 5.1 的本轮状态);要消除它就得接入签名并设 `REQUIRE=1` |
|
|
171
|
+
| 明明签了名,客户端还是说"未验证来源签名" | `.sig` 没上传 / 文件名不是 `<安装包名>.sig` / feed 目录与 `UPDATE_FEED_BASE` 不一致 | 核对 feed 目录里 `<包名>.sig` 是否存在;改了 `electron-builder.yml` 的 `publish.url` 时,`desktop/update-verify.js` 的 `UPDATE_FEED_BASE` 必须一起改 |
|
|
172
|
+
| 换了密钥后**所有**客户端都拒绝 | 正常且期望:内置公钥还是旧的那把 | 用 `--keygen` 生成新密钥 → 替换 `UPDATE_PUBLIC_KEY_PEM` → 发一次带签名的新版本(旧版本客户端无法验新密钥,这是非对称 pin 的固有代价) |
|
|
173
|
+
| 私钥文件/口令疑似泄露 | —— | **立即轮换**(`--keygen` 新密钥 + 替换公钥常量),并公告旧版本客户端的验签行为;仓库里若出现过私钥,视为已泄露(`git log` 里删不掉) |
|
|
174
|
+
|
|
175
|
+
### 5.5 边界(不在本机制范围内)
|
|
176
|
+
|
|
177
|
+
- **签名缺失时的降级无法区分**:控制 feed 的攻击者可以**删掉** `.sig`,把这次更新降级成"无签名"。
|
|
178
|
+
默认档下这仍然会走到 `warn-and-install`。要真正关闭它,必须让发布链路**每次都签**并且客户端
|
|
179
|
+
`REQUIRE=1`。这是"默认不掐死存量更新"这一取舍的必然后果,已写进源码注释与登记文档。
|
|
180
|
+
- **签名载体**:客户端按 ① `latest*.yml` 内嵌 `signature:` 字段 → ② 本机旁车 `<包>.sig` →
|
|
181
|
+
③ feed 目录 `<包名>.sig`(HTTPS 取公开数据)的顺序取材。**当前实测过的是旁车 `.sig`**;
|
|
182
|
+
内嵌字段要依赖 `electron-updater` 把未知 yml 字段透传到 `update-downloaded` 的 `info`
|
|
183
|
+
(本仓未对真实库验证),故发布流程以旁车为准。
|
|
184
|
+
- `MINGDAO_UPDATE_PUBKEY_PEM` + `MINGDAO_UPDATE_ALLOW_PUBKEY_OVERRIDE=1` 是**显式双开关**的开发/测试
|
|
185
|
+
覆盖(两个都要给才生效)。**发布构建不得设置**;只设前者不会替换内置公钥。
|
|
186
|
+
- 不含 macOS/Windows 的**代码签名**与公证(见第一~四节),也不含"官网下载页手动安装包"的来源校验
|
|
187
|
+
(用户手动下载时走的是浏览器 TLS + 系统 SmartScreen/Gatekeeper)。
|
|
188
|
+
- 不含密钥吊销 / 多密钥并存验签(换密钥即全量失效,见上表)。
|
|
189
|
+
- 私钥与构建机同权时,本层不防"能改构建产物的人"——它防的是**发布源被换**。
|
|
89
190
|
|
|
90
191
|
---
|
|
91
192
|
|
|
92
193
|
*配置文件位置:`desktop/electron-builder.yml`(azureSignOptions 注释)、`.github/workflows/desktop.yml`(env 段)。*
|
|
194
|
+
*来源签名实现:`desktop/update-verify.js`(判据/文案单源)、`desktop/main.js`(门禁接线)、`scripts/update-sign.mjs`(发布侧工具)、`test/smoke.js` §135、`test/mutate/batch21-update-sign.mjs`。*
|
package/docs/CONFIG.md
CHANGED
|
@@ -111,6 +111,36 @@ WebUI 中每个会话记住自己的工作目录:新会话记录创建时的
|
|
|
111
111
|
> 那里"一次误按 y 就绕过自己写的禁令"是不可接受的;
|
|
112
112
|
> 交互式日常使用保持默认即可——每次放行都是一次明确的、点名规则的确认。
|
|
113
113
|
|
|
114
|
+
### 权限优先级与预设(v0.6.14)
|
|
115
|
+
|
|
116
|
+
口径:**显式选择 > 预设建议(`recommendedPermission`)> config.json**。
|
|
117
|
+
(这句说的是"档位意图从哪来"的优先序;落到判定上,`recommendedPermission` **只是建议**——不参与判定、
|
|
118
|
+
不自动改档。真正会改档的只有两处:你的显式选择,以及老/第三方预设声明的 `permission`(受反提权约束)。)
|
|
119
|
+
|
|
120
|
+
| 层级 | 从哪来 | 性质 |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| 显式选择 | CLI `mingdao run "…" --permission auto\|ask\|readonly`、REPL `/mode`、WebUI「权限模式」下拉(随本次请求发出) | **覆盖**:本回合的终值,任何预设都压不过它 |
|
|
123
|
+
| 预设建议 | 预设 JSON 的 `recommendedPermission`(内置 `local-audit` 即 `readonly`);`GET /api/presets` 会透出该字段 | **只是建议**:不参与权限判定、不静默改档;想让建议生效,就在上面那一层显式选它 |
|
|
124
|
+
| config.json | `permission` 字段(支持 `{mode, allow, deny}` 对象形态) | **兜底**:没有显式选择、预设也没声明 `permission` 时才用它 |
|
|
125
|
+
|
|
126
|
+
三条要点(别再踩):
|
|
127
|
+
|
|
128
|
+
- **预设的 `tools` 白名单才是只读的硬约束。** 内置 `local-audit` 的白名单里没有 `write`/`edit`,
|
|
129
|
+
写工具根本不会出现在模型可见的工具表里——这比任何权限档都硬(权限档只决定"要不要逐次询问")。
|
|
130
|
+
所以"要一个只读预设"的正确做法是**收白名单**,不是把会话钉死在 `readonly`。
|
|
131
|
+
- **`permission` 字段是覆盖。** 预设声明它会改本回合的档位(更宽松的方向被反提权拦掉:
|
|
132
|
+
`readonly→auto` 一律拒绝并只发一条 ⚠ banner)。**内置预设不得声明 `permission`**——它唯一的
|
|
133
|
+
实际效果是"沉默覆盖用户的选择":负责人实测的坑就是界面上选了「自动」,勾上内置审计预设后仍按
|
|
134
|
+
`readonly` 跑,于是每调用一次非只读工具都弹「只读模式将拦截 task,是否本次放行?」。
|
|
135
|
+
第三方/老预设仍可声明(兼容),但同样受反提权约束,且要自己承担"覆盖用户选择"的后果。
|
|
136
|
+
- **`recommendedPermission` 只是建议。** 它是预设对"我适合跑在哪个档"的表达,只做展示/透出,
|
|
137
|
+
不写进任何权限判定;权限档以你的显式选择为准,其次才是 config.json 里的默认值。
|
|
138
|
+
内置 `local-audit` 用它表达"建议只读",只读本身则由 tools 白名单落到实处。
|
|
139
|
+
|
|
140
|
+
> 另一个常被名字误导的点:内置 `local-audit`(「只读代码审计(本地模型)」)**不启用审计日志/账本**。
|
|
141
|
+
> 工具调用审计是 `config.audit` 与 `mingdao audit` / `mingdao ledger` 的事,与这个预设无关——
|
|
142
|
+
> 别把它当合规审计开关。
|
|
143
|
+
|
|
114
144
|
## 项目记忆(自动写入项目目录,默认已自忽略)
|
|
115
145
|
|
|
116
146
|
会话收尾时会从对话里提炼几条长期有效的结论,写进**当前项目**目录:
|
|
@@ -189,7 +219,10 @@ mingdao net report --since 7d --json # 支持 7d / 24h / 90m
|
|
|
189
219
|
}
|
|
190
220
|
```
|
|
191
221
|
|
|
192
|
-
- `PreToolUse
|
|
222
|
+
- `PreToolUse`:工具执行前调用;**只有** stdout 回 `{"decision":"block","reason":"…"}` 才阻止执行
|
|
223
|
+
(非 JSON 的非空输出按 fail-closed 也阻止;输出为空、或 JSON 里 `decision` 不是 `block`(如 `approve`)则放行)。
|
|
224
|
+
> 勘误(v0.6.11):此处此前写作"命令输出非空即阻止执行",与实现不符——照它写合规钩子会以为
|
|
225
|
+
> "随便打印点什么就能拦住",实际必须回那个 JSON 结构。
|
|
193
226
|
- `PostToolUse`:执行后调用(审计/日志);
|
|
194
227
|
- 协议:stdin 收 JSON(工具名/参数),stdout 回 JSON;`matcher` 支持 `|` 分隔与 `*` 通配;
|
|
195
228
|
- ⚠ **hooks 命令以 `shell: true` 执行——配置即代码执行**:命令会在每次工具调用时运行,请只填自己完全信任的命令(例如不直接填 `curl <不可信地址>`)。
|
|
@@ -233,7 +266,10 @@ mingdao net report --since 7d --json # 支持 7d / 24h / 90m
|
|
|
233
266
|
同机的其它进程/其它用户可以直接访问 `/api/*`,多用户机器与共享 CI 上请启用令牌;
|
|
234
267
|
- 绑定 `0.0.0.0`/局域网地址时**强制令牌认证**:未配置则每次启动随机生成并打印
|
|
235
268
|
`http://<地址>:<端口>/?token=…` 访问链接;固定令牌(优先级从高到低):
|
|
236
|
-
|
|
269
|
+
`mingdao web --auth-token <令牌>`(或 `--auth-token=-` 从 stdin 读)、环境变量 `MINGDAO_WEB_TOKEN`、`web.token`
|
|
270
|
+
——即**命令行 > 环境变量 > 配置**(`src/commands/skill.js` 的 `--auth-token` 分支 > `MINGDAO_WEB_TOKEN` > `config.web.token`)。
|
|
271
|
+
> 勘误(v0.6.11):此处此前把顺序写反成"环境变量、配置、命令行"。文档审计(F-M2 同批)核对了代码,
|
|
272
|
+
> 实际是"命令行优先"——照旧文档配置的人会以为环境变量最高,从而留下一个自己没意识到的固定令牌。
|
|
237
273
|
**命令行字面量会进入 argv 与 shell 历史(`ps` 可见)**,故用它时会给出告警;
|
|
238
274
|
- 令牌同时接受 URL `?token=`、请求头 `X-MingDao-Token` 或 `Authorization: Bearer`;
|
|
239
275
|
- 服务端校验 `Host` 头必须等于回环名或绑定地址(防 DNS rebinding),代理场景会 403 属预期。
|
|
@@ -370,6 +406,106 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
|
|
|
370
406
|
{ "timeout": { "firstTokenMs": 600000, "streamIdleMs": 120000, "totalMs": 1800000 } }
|
|
371
407
|
```
|
|
372
408
|
|
|
409
|
+
#### 单请求超时之外的"回合级"护栏(v0.6.13)
|
|
410
|
+
|
|
411
|
+
上面三个超时**都是单请求级**:一次请求最多耗到 `totalMs`。而一个回合最坏是
|
|
412
|
+
`maxRounds × stepLimit` 次请求(默认 3 × 24 = 72),`task` 子代理各自还有一整套预算且可递归派生——
|
|
413
|
+
真机实测出现过「本地 35B 的审计回合跑了 4 小时 20 分、界面 0 步、`web-server.log` 只有一行」的现场
|
|
414
|
+
(`audit.jsonl` 里那 4 小时里有 610 次工具调用,绝大多数是**逐字重复**的失败调用)。
|
|
415
|
+
所以补了一条回合级判据:
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{ "noProgressTimeoutMs": 3600000 }
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
- **判据**:连续 N 分钟**既没有"成功且不重复的工具结果"、也没有新的正文/推理增量** → 主动中止整个
|
|
422
|
+
回合,并如实报告「连续 N 分钟没有任何工具调用、也没有新内容,已中止」+ 已发生的模型轮次/工具调用数。
|
|
423
|
+
重复调用(name+args 完全相同)与失败调用**不算进展**——真机现场正是它们把时间烧光的。
|
|
424
|
+
- **默认 60 分钟**的依据:现有单请求总量上限是本地 1800s;回合级阈值取它的 **2 倍**,
|
|
425
|
+
保证"一次慢 prefill"永远由更具体的请求级超时(首帧/空闲/总量各有文案)先说话,看门狗不抢答;
|
|
426
|
+
而真干活的回合每隔几秒就有工具结果或正文增量,60 分钟零增量在任何正常回合都不可能。
|
|
427
|
+
- 该值**不能**用 0/负数/Infinity 关掉(一律回落默认,fail-safe);子代理会**继承父回合的截止时刻**
|
|
428
|
+
(24 个并行子代理不会各跑各的几小时)。
|
|
429
|
+
|
|
430
|
+
#### 端点预检(v0.6.13,本地端点自动执行)
|
|
431
|
+
|
|
432
|
+
对**本地端点**(baseUrl 是回环/内网,或 `customModels.<名>.local: true`)在开工前做一次轻量预检
|
|
433
|
+
(结果缓存 10 分钟,只读不写你的配置):
|
|
434
|
+
|
|
435
|
+
1. 取 `/v1/models`,把**引擎实际加载的模型名**与配置项名对照(不一致会写出来,避免"配的是 A、跑的是 B");
|
|
436
|
+
2. 发 **3 次**极小请求(1 条 user + 1 个工具声明 + `max_tokens: 256`),看 `tool_calls` 与 `finish_reason`,
|
|
437
|
+
给出三态之一:**稳定支持**(3/3)/ **不稳定**(N/3,会提示"agent 任务可能步数为 0")/ **不支持**(0/3);
|
|
438
|
+
被 `max_tokens` 截断的样本(`finish_reason: length`,思考型模型常见)判为**不可判定**,不当作"不支持";
|
|
439
|
+
3. 再发一次 **参考 prefill**(约 2000 tokens 的提示)测耗时;
|
|
440
|
+
4. 与本机历史值比对:**同一端点明显变慢**(≥3 倍)会提示"该端点可能仍在处理上一个请求,建议重启引擎"。
|
|
441
|
+
|
|
442
|
+
预检结论会出现在:回合开始时的界面 banner、`web-server.log`(`预检 <模型> …` 行)、
|
|
443
|
+
以及发送前的规模预告里。并且**按实测 TTFT 自适应**超时:首帧上限 = clamp(max(实测首帧×20, 参考prefill×5),
|
|
444
|
+
既有默认, 1800s)、无进展看门狗 = clamp(max(实测首帧×100, 参考prefill×20), 10min, 60min);
|
|
445
|
+
**你显式配置的 `timeout.*` / `noProgressTimeoutMs` 永远优先**(显式 > 自适应)。
|
|
446
|
+
|
|
447
|
+
### 用本地模型测 harness(测试者步骤清单)
|
|
448
|
+
|
|
449
|
+
> 这一段是**操作清单**,不是原理说明。按顺序做,能在几分钟内区分"端点不行"和"harness 不行"。
|
|
450
|
+
|
|
451
|
+
**第 0 步:确认引擎在跑、名字对不对**
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
curl -s http://127.0.0.1:8081/v1/models | head -c 400 # 端口按你的部署改
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- 返回里应当看到**配置项名**对应的模型。若引擎回的是完整文件路径/别的名字(本机实测:
|
|
458
|
+
配置 `MLocalModel3.6.2`,引擎回 `/Users/…/mLocalModel3.6.2.gguf`),预检会把两者都写出来——
|
|
459
|
+
先确认"配的就是跑的那个",再谈性能。
|
|
460
|
+
|
|
461
|
+
**第 1 步:先跑预检(一条命令看到 TTFT 与工具支持)**
|
|
462
|
+
|
|
463
|
+
预检在**每轮对话开始前自动执行**(仅本地端点),界面上会出现两行:
|
|
464
|
+
|
|
465
|
+
```
|
|
466
|
+
🔎 本地端点预检:✅ 稳定支持工具调用(3/3) · 首帧 1.22s · 参考 prefill 2000 tokens 9.71s · 引擎加载 …gguf
|
|
467
|
+
自适应:首帧上限 600s、无进展看门狗 60 分钟(依据:预检实测 首帧 1220ms + 参考 prefill 9708ms)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
同一条结论会落进 `~/.mingdao/logs/web-server.log`(`预检 <模型> <状态>(<toolState> N/M) ttft=… prefill2000=…`)。
|
|
471
|
+
看到这些就说明通路与能力都测过了;**不要**跳过这步直接放大任务。
|
|
472
|
+
|
|
473
|
+
**第 2 步:小任务验证通路**
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
帮我看看这个仓库的 README,列出三层小标题
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
验收标准:几十秒内出现正文;`web-server.log` 里有 `chat 首帧 <taskId> <秒>` 与每 30s 一行的
|
|
480
|
+
`chat 进度 <taskId> 已跑 …阶段=… 模型轮次=… 工具调用=…`;界面状态条上有「工具调用 N 次」(0 也要显示为 0)。
|
|
481
|
+
|
|
482
|
+
**第 3 步:按模块切分,再放大**
|
|
483
|
+
|
|
484
|
+
35B 级模型 + 131k 窗口做**全量仓库审计**时:上下文越大,prefill 越久(本机实测 35B Q4_K_M
|
|
485
|
+
约 4.9ms/token:2 万 tokens ≈ 1.6 分钟;13 万窗口 ≈ 10 分钟以上,且会顶满引擎内存预算)。
|
|
486
|
+
所以:
|
|
487
|
+
|
|
488
|
+
- 先用预设 `local-model`(`contextBudget` 65536)或自己在预设里调小 `contextBudget`;
|
|
489
|
+
- 一次只做一个模块/一个目录,别一次喂整仓;
|
|
490
|
+
- 真要长跑,把 `timeout.firstTokenMs` 调大(例如 900000)——但要知道**这只会让失败更晚被发现**,
|
|
491
|
+
不会让 prefill 变快。
|
|
492
|
+
|
|
493
|
+
**第 4 步:认识四类"卡住"分别长什么样**
|
|
494
|
+
|
|
495
|
+
| 现象 | 内核会给什么信号 |
|
|
496
|
+
| --- | --- |
|
|
497
|
+
| 引擎没起来 / 端口不对 | 预检 `❌ 不可达:…`;请求以 `网络请求失败` 报错(几秒内) |
|
|
498
|
+
| 连上但不发首帧 | 到 `timeout.firstTokenMs` 报「首 token 等待超限(Ns,本地模型长上下文 prefill 可能很慢)——可调大 config.timeout.firstTokenMs…」 |
|
|
499
|
+
| 有帧但长时间没新数据 | 到 `timeout.streamIdleMs`(默认 120s)报「流式响应空闲超限(Ns 无新数据)」 |
|
|
500
|
+
| 每轮都能返回、但什么都没做成 | 到 `noProgressTimeoutMs`(默认 60min,快端点自适应到 10min)报「连续 N 分钟没有任何工具调用、也没有新内容,已中止」 |
|
|
501
|
+
| 上游 4xx/5xx(模板/内存/鉴权…) | 错误里带**上游原文**并附一条可操作判断(例如"该端点的模板不接受当前 messages 形态") |
|
|
502
|
+
|
|
503
|
+
**第 5 步:模型不支持工具调用时会怎样**
|
|
504
|
+
|
|
505
|
+
若预检判定 **不支持/不稳定**,或整回合 0 次工具调用:回合结束时界面会明确写出
|
|
506
|
+
「⚠ 本回合 0 次工具调用(共 N 轮模型请求 / M 步):该端点看起来不返回 tool_calls」——
|
|
507
|
+
不会出现"看起来在工作、其实什么都没做"。此时换支持 function calling 的端点,或只把它用于纯问答。
|
|
508
|
+
|
|
373
509
|
### 文件访问边界(v0.4.1 P0 路径穿越防护)
|
|
374
510
|
|
|
375
511
|
read/write/edit/ls/glob/grep/undo 默认限定在**工作目录**内——auto 权限模式下,模型(或被提示注入
|
|
@@ -404,6 +540,41 @@ export async function createProvider(cfg) {
|
|
|
404
540
|
- 调度器为**单守护进程**:`mingdao schedule daemon [status|stop]` 查看/停止;有任务时任意
|
|
405
541
|
schedule 命令自动拉起,无任务自动退出(每任务一个 sleeper 进程的旧方案仅作兜底)。
|
|
406
542
|
|
|
543
|
+
## 账本来源签名(v0.6.11:`mingdao ledger --sign-key`)
|
|
544
|
+
|
|
545
|
+
哈希链 + 封条证明的是「这份账本自写入后没被改动、尾部没被截断」,但它们的全部输入都来自
|
|
546
|
+
账本自身、算法(sha256)是公开的——**改内容 + 逐行重算 prev + 改写封条**之后照样逐项吻合
|
|
547
|
+
(实测复现,见 `docs/internal/AUDIT-v0.6.1-第三方报告登记.md` §3.45)。来源签名补上的是另一半:
|
|
548
|
+
「这条账本是不是**持有本机那把密钥**的一方写的」。
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
mingdao ledger --sign-key # 查看密钥状态(只显示公钥与指纹,绝不回显私钥)
|
|
552
|
+
mingdao ledger --sign-key --generate # 生成密钥 → <home>/ledger-key.json(权限 600)
|
|
553
|
+
mingdao ledger --sign-key --generate --force # 覆盖已有密钥(会让旧账本变成"另一把密钥签发",先备份)
|
|
554
|
+
mingdao ledger verify <runId> # 链/封条 + 来源签名(默认用本机密钥验签)
|
|
555
|
+
mingdao ledger verify <runId> --key <文件> # 用指定公钥验签(本模块密钥文件,或 PEM/SPKI 公钥)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
- **算法 ed25519**(`node:crypto` 原生,零新增依赖):非对称,公钥可以交给第三方复核,
|
|
559
|
+
对方能验证「由那把密钥签发且未被改动」,但**不能**补签一份新账本。HMAC 做不到这一点——
|
|
560
|
+
它的验证密钥就是签名密钥,把审查权交出去等于把伪造能力一起交出去。
|
|
561
|
+
- **密钥文件** `<home>/ledger-key.json`:权限 600、原子写、**绝不进 `config.json`、凭证库
|
|
562
|
+
(`credentials.json`)与仓库**。不并进凭证库是因为 `key remove` / `key import` 会全量重写它,
|
|
563
|
+
顺手一次就会把签名密钥清掉、让全部历史账本变成"另一把密钥签发"。
|
|
564
|
+
- **默认开启**:新账本在首次收尾时若本机还没有密钥会**自动生成**并签名(可选签名在合规上等于
|
|
565
|
+
没有签名)。封条里带 `sig{alg,keyId,value}`,`run.end` 链内带 `sigKey`(公钥指纹)。
|
|
566
|
+
- **`ledger verify` 的三种结论**(前两者退出码 0,第三者非 0):
|
|
567
|
+
|
|
568
|
+
| 结论 | 含义 | 退出码 |
|
|
569
|
+
| --- | --- | --- |
|
|
570
|
+
| 链完整 + 签名有效 | 由持有该密钥的一方写入,且内容/封条自签名后未被改动 | 0 |
|
|
571
|
+
| 链完整但无签名 | 写于启用签名之前的老账本(或签名证据被整体抹除,两者本项无法区分)——只证明哈希链完整 | 0 |
|
|
572
|
+
| 链完整但签名无效 | 内容被改写后重算过链,或换了一把密钥签发(含"本条账本来自另一台机器") | 非 0 |
|
|
573
|
+
|
|
574
|
+
另有第四态「签名无法校验」(本机/`--key` 没有对应公钥)同样非 0——**不能确认来源**不等于通过。
|
|
575
|
+
- **诚实边界**:私钥与账本同机同权限,能改账本的对手通常也能读私钥,本层不防同权限的本机对手;
|
|
576
|
+
不含可信时间戳,不等同于审计级不可否认;私钥丢失后历史账本只能报"由另一把密钥签发"。
|
|
577
|
+
|
|
407
578
|
## 月度费用报告
|
|
408
579
|
|
|
409
580
|
`mingdao cost` 控制台速览;`mingdao cost report [YYYY-MM|all]` 导出 Markdown 报告
|
package/docs/DEVELOPER.md
CHANGED
|
@@ -25,7 +25,13 @@
|
|
|
25
25
|
|
|
26
26
|
1. `<项目>/.mingdao/presets/<名>.json` — 项目级(随项目走)
|
|
27
27
|
2. `~/.mingdao/presets/<名>.json` — 用户级(本机全局)
|
|
28
|
-
3. `presets/`(随 npm 包分发)—
|
|
28
|
+
3. `presets/`(随 npm 包分发)— 内置参考,现内置两个**语义单一**的示例:
|
|
29
|
+
- `local-model` —— **只放"让本地模型跑得动"的参数**(`contextBudget`/`maxOutputTokens`/`maxRounds`)。
|
|
30
|
+
本地模型只是替换云模型 API,**功能一致**:所以它不带人格、不限制工具、不建议权限档。
|
|
31
|
+
- `readonly-audit` —— 审计人格 + 只读工具白名单(`tools`)+ `recommendedPermission: "readonly"`(只是**建议**)。
|
|
32
|
+
- 老名字 `local-audit` 保留为**别名**(指向 `local-model`):按老名字调用仍可用,且会打印一次
|
|
33
|
+
「已更名」提示(CLI/内核日志)并在 WebUI 用 banner 说明——改名兼容,但不静默。
|
|
34
|
+
若你自己在项目级/用户级写了 `local-audit.json`,**以你的文件为准**(别名只在没找到同名预设时兜底)。
|
|
29
35
|
|
|
30
36
|
### 1.2 格式
|
|
31
37
|
|
|
@@ -37,6 +43,7 @@
|
|
|
37
43
|
"systemPrompt": "你是代码审查员。只读审查,按严重度分级输出,每条带文件:行号证据。",
|
|
38
44
|
"tools": ["read", "ls", "glob", "grep", "skill", "git", "fetch", "todo"],
|
|
39
45
|
"permission": "auto",
|
|
46
|
+
"recommendedPermission": "readonly",
|
|
40
47
|
"model": "deepseek-v4-flash",
|
|
41
48
|
"temperature": 0.3,
|
|
42
49
|
"maxOutputTokens": 4096,
|
|
@@ -49,6 +56,32 @@
|
|
|
49
56
|
未知字段会**校验报错**(防拼写错误静默失效)。
|
|
50
57
|
`model` 是**建议**:CLI 在未显式 `-m` 时采纳;WebUI 以用户当前选择的模型为准(预设不覆盖)。
|
|
51
58
|
|
|
59
|
+
`permission` 与 `recommendedPermission` **不是一回事**(v0.6.14 起):
|
|
60
|
+
|
|
61
|
+
| 字段 | 语义 | 谁该用 |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `permission` | **覆盖**本回合的权限档(仍受反提权规则约束:只能更保守,不能更宽松) | 第三方/老预设;**内置预设不得声明** |
|
|
64
|
+
| `recommendedPermission` | **建议**:只透出给界面/诊断,**不参与判定、不覆盖用户选择** | 内置预设想表达"建议只读"时用它 |
|
|
65
|
+
|
|
66
|
+
只读的**硬约束**是 `tools` 白名单(不含 `write`/`edit`,模型连写工具都看不到),不是权限档。
|
|
67
|
+
|
|
68
|
+
#### 参数怎么给"本地模型"用(v0.6.15)
|
|
69
|
+
|
|
70
|
+
给本地部署模型写预设时,**只调参数、别加人格**——本地模型与云模型功能一致,它需要的是
|
|
71
|
+
"预算合适",不是"换个角色":
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{ "name": "my-local", "label": "我的本地模型", "contextBudget": 65536, "maxOutputTokens": 4096, "maxRounds": 4 }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `contextBudget` 是**最要紧的一个**:本地引擎的 prefill 时间与内存随上下文线性上涨
|
|
78
|
+
(本机 35B Q4_K_M 实测约 4.9ms/token:2 万 tokens ≈ 1.6 分钟、13 万窗口 ≈ 10 分钟以上,
|
|
79
|
+
且大上下文会顶满引擎内存预算)。给 32k–65k 通常就够单个模块级任务。
|
|
80
|
+
- 模型窗口/最大输出等**能力声明**请写在 `config.customModels.<模型名>`
|
|
81
|
+
(`contextWindow`/`maxOutputTokens`/`local`),不要再写一份进预设——两处会漂移。
|
|
82
|
+
- 想让"本地模型 + 只读审计"同时生效:两个预设**不能叠加**(一次只能选一个)。
|
|
83
|
+
要么用 `readonly-audit` 并自己把 `contextBudget` 调小,要么复制一份按需合并。
|
|
84
|
+
|
|
52
85
|
### 1.3 使用
|
|
53
86
|
|
|
54
87
|
- CLI:`mingdao --preset code-reviewer "审查 src/ 目录"`;交互模式 `mingdao --preset code-reviewer`。
|
package/package.json
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "local-model",
|
|
3
|
+
"label": "本地模型(保守参数)",
|
|
4
|
+
"description": "给**本地部署模型**用的保守参数预设。先说清两件事:① 本地模型与云模型**功能一致**——本预设不加人格、不限制工具、不建议权限档,只把「上下文预算 / 单次最大输出 / 轮次」调到本地推理跑得动的量级;② 它**不是**代码审计预设(审计人格与只读工具白名单在另一个预设 readonly-audit 里)。为什么只调这几个数:本地引擎的瓶颈在 prefill 与显存/内存预算(本机实测 35B Q4_K_M:约 4.9ms/token,2 万 tokens 上下文的 prefill ≈ 1.6 分钟,13 万窗口 ≈ 10 分钟以上,还会把引擎的内存预算顶满)——预算给小一点,任务跑得完、也更容易看清是哪一步慢。模型窗口/最大输出等**能力声明**请写在 config.customModels.<模型名>(contextWindow/maxOutputTokens),本预设只覆盖本回合参数。",
|
|
5
|
+
"contextBudget": 65536,
|
|
6
|
+
"maxOutputTokens": 4096,
|
|
7
|
+
"maxRounds": 4
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "readonly-audit",
|
|
3
|
+
"label": "只读代码审计",
|
|
4
|
+
"description": "只读代码审计预设:审计人格 + 只读工具白名单 + 保守参数。三点先说清:① 本预设**不启用审计日志/账本**——那是 config.audit 与 `mingdao ledger`,与本预设无关,别把它当合规审计开关;② 本预设**不覆盖你的权限档**——只读效果由 tools 白名单保证(里面没有 write/edit,模型根本看不到写工具),权限档始终以你的显式选择为准(recommendedPermission=readonly 只是建议,不参与判定);③ 它**不是**本地模型专用预设——要调本地模型的上下文/输出预算用 local-model(本预设与它相互独立)。",
|
|
5
|
+
"systemPrompt": "你是一名代码审计员。任务是对给定代码库做只读审计:先 ls/glob 摸清结构,再 read/grep 逐文件审查,可用 git 查看历史与 diff、fetch 抓取相关文档。对于大仓库可分模块派发只读子代理并行审查(task 工具传 readOnly:true,子代理同样只读:read/ls/glob/grep/skill)。输出审计报告:发现的缺陷按严重度分级,每条给出文件:行号证据;不做任何修改。若上下文紧张,先审查最关键的部分并明示未覆盖区域。",
|
|
6
|
+
"tools": ["read", "ls", "glob", "grep", "skill", "git", "fetch", "todo", "task"],
|
|
7
|
+
"recommendedPermission": "readonly",
|
|
8
|
+
"maxRounds": 4,
|
|
9
|
+
"maxOutputTokens": 4096
|
|
10
|
+
}
|