mingdao-harness 0.6.13 → 0.6.15

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 CHANGED
@@ -19,7 +19,7 @@ does not cover (this file is written so a reviewer can decide that, not to make
19
19
  than it is).
20
20
 
21
21
  - Node.js **>= 18.17** (`package.json` `engines`), no build step, no npm runtime dependencies.
22
- - Current version: **0.6.8** · License: **MIT**.
22
+ - Current version: **0.6.14** · License: **MIT**.
23
23
  - Repo: <https://github.com/MingDaoTCM/MingDao-Harness> · Package: `mingdao-harness` on npm.
24
24
 
25
25
  ## Quickstart (3 steps)
@@ -66,11 +66,12 @@ enforced by the kernel is described as a declaration, not as a control.
66
66
  | **Sandbox** — `config.sandbox` | `"off"` (default), `"readonly"` (whole filesystem read-only, `/tmp` writable, network available), `"safe"` (read-only + no network via `unshare-net`; working dir and `/tmp` writable). Implemented with **bubblewrap** on **Linux only**. | If the platform is not Linux or `bwrap` is missing/incapable, it **downgrades to `off`** and says so in the tool result — it never silently pretends to be sandboxed. It wraps the `bash` tool only; the file tools are governed by the permission modes, not by this. |
67
67
  | **Credential separation** | API keys live in `<home>/credentials.json` (`<home>` defaults to `~/.mingdao`, overridable with `MINGDAO_HOME`), written atomically and forced to mode **600**. `config.json` is key-free, so it is shareable and committable. Resolution order: environment variable → local credential store → legacy `config.json` field. Manage with `mingdao key`. | The legacy `config.json` `"apiKey"` field is still **read** for backward compatibility — do not use it. Keys are stored as plaintext in your home directory (no OS keychain), so any process running as your user can read them. `mingdao key status` and logs only ever print a masked form. |
68
68
  | **Telemetry opt-out** | Only **packaged desktop builds** send anything, and only one event: after an update finishes downloading, a `POST` to `https://harness.mingdao.ai/updok` with `{"kind":"update","os":...,"ver":...}` — no user identifier, no session, no code. | It is **opt-out, not opt-in**: on by default in packaged builds. Disable with `MINGDAO_NO_TELEMETRY=1` or `"telemetry": false` in `config.json`; updates still download and install. The CLI and the WebUI send **no** telemetry at all. |
69
+ | **Update-package signature** | The packaged desktop app verifies an **ed25519 detached signature** over the installer bytes against a public key pinned in the source (`desktop/update-verify.js`). A signature that is present but invalid is always rejected. | The release pipeline does not sign builds yet, so current builds verify as "no signature" and follow the default policy: **install, but state that the signature was not verified**. Set `MINGDAO_REQUIRE_UPDATE_SIGNATURE=1` to **reject** unsigned updates (fail-closed, recommended for high-assurance deployments). A feed controlled by an attacker can strip the signature — that is exactly the downgrade this switch closes off. |
69
70
  | **PID ownership** | Before `killTask` / `stopDaemon` signals a pid, it verifies the pid still belongs to the process we started (Linux reads `/proc/<pid>/cmdline`, macOS falls back to `ps -ww -o command=`). Three-valued: ours / definitely not ours / unknown. | **Windows has neither `/proc` nor `ps`**, and the project deliberately does not take a PowerShell/WMI dependency for this check, so ownership is reported as unknown and the caller falls back to best-effort "process is alive". Deploy on Linux or macOS if you need precise pid-ownership checks. |
70
71
  | **Egress gate** — `config.net` | `{ "allow": ["api.deepseek.com", "*.internal.corp", "10.0.0.0/8"], "mode": "warn", "allowLoopback": true }` — `warn` (default) logs and allows, `block` refuses. Decisions are made at a single HTTP exit and recorded per event (host, port, decision, matched rule); `mingdao net report` exports the record as evidence. Non-`fetch` paths (`node:https` sync, `safe-fetch` for skill/registry/model discovery) are wired into the same decision point. Loopback is exempt; private ranges are not. | **It does not cover child processes.** Your own `curl` inside `bash`, **MCP server processes**, `config.tools` and Pack-spawned subprocesses, and the Electron shell's update check all make their own requests that the gate never sees. It proves *"the kernel did not exfiltrate"*, **not** *"this machine never egressed"*. `net.jsonl` records hosts/ports/decisions, never request bodies or full URLs. If unconfigured, the gate is not installed at all. For process-level enforcement use an egress proxy or firewall. |
71
72
  | **SSRF protection** | One implementation (`src/ssrf-guard.js`): literal private / loopback / link-local / cloud-metadata hosts are refused (IPv4 plus all IPv6 forms, including IPv4-mapped, NAT64 and zone-ids); a hostname is resolved and **every** resolved address is checked; a hostname that fails to resolve is refused. The **validated IP is pinned to the connection layer** (via the `lookup` option of `node:http(s)`), so the check-time and connect-time resolutions cannot differ — this closes DNS rebinding. Redirects are re-checked per hop and cross-origin hops drop credential headers using a reverse allowlist. | The rule is fail-closed by design, which means it can over-block unusual-but-legitimate targets (odd IPv6 forms, `%`-scoped hosts); the fix is to use a literal, routable address or a proper hostname. It protects harness-initiated requests; it cannot constrain a subprocess' own networking (see the row above). |
72
73
  | **Pack trust** | A project-level `.mingdao/packs/**` is **not mounted** until you explicitly run `mingdao pack trust <project-dir>`. Trust is pinned to content: if the Pack's content changes afterwards, trust lapses automatically. `mingdao pack verify <dir>` is **static by default** and does not execute Pack code; only `--runtime` imports it. | `manifest.permissions` (`fs` / `net` / `env`) is a **declaration, not enforcement**. The kernel does not enforce it: `pack.mjs` is imported into the **host process** and runs with full Node privileges — it can read and write any file and reach the network — and Pack-contributed tools are **not** subject to `config.permission`. Packs you list explicitly in `config.packs` bypass the trust gate, because that is your own written authorization. Treat installing a Pack as equivalent to running its code. |
73
- | **Execution ledger** | Per-turn event streams under `<home>/ledger/`, each event chained to the previous one by a truncated SHA-256 digest starting from a fixed genesis value, plus a `run.end` seal sidecar. Details are redacted on write and again on export. Inspect with `mingdao ledger list\|show\|export\|verify` (there is also `replay`, which re-evaluates recorded calls against your *current* rules offline). | Hash chain + seal prove "unchanged and not truncated **since it was written**". There is **no trusted timestamp and no signature** (optional signing is not implemented), so it is **not** audit-grade non-repudiation — `verify` says so in its own output. |
74
+ | **Execution ledger** | Per-turn event streams under `<home>/ledger/`, each event chained to the previous one by a truncated SHA-256 digest starting from a fixed genesis value, plus a `run.end` seal sidecar. The seal carries an **ed25519 source signature** (key at `<home>/ledger-key.json`, mode 600, generated on first seal). Details are redacted on write and again on export. Inspect with `mingdao ledger list\|show\|export\|verify` — `verify` reports chain/seal **and** provenance in four states (valid / unsigned / invalid / unverifiable) — plus `replay`, which re-evaluates recorded calls against your *current* rules offline; `ledger --sign-key` shows or rotates the key. | Hash chain + seal prove "unchanged and not truncated **since it was written**"; the signature proves "written by the holder of that key" — but the key lives on the same machine with the same permissions, so it does **not** stop a local attacker who can already rewrite the ledger. There is **no trusted timestamp**, and replacing the key makes old ledgers report "signed by another key". It is provenance, not audit-grade non-repudiation. |
74
75
  | **WebUI exposure** | Binds `127.0.0.1:3820` by default. When bound to a non-loopback address without a configured token, the server generates a random token for that run and prints a `?token=` URL. Tokens also accepted via `X-MingDao-Token` or `Authorization: Bearer`. Host header is checked against loopback/bind names (anti-DNS-rebinding), cross-site browser requests are rejected, and cloud-metadata endpoints are always refused. | On loopback with no token configured there is **no authentication** (local trust by design). With `host: 0.0.0.0` the Host-header check is relaxed and the **token becomes the only boundary**, so a token is mandatory in that mode. The token is deployment-level: everyone sharing it is the same user, and task/session lists are mutually visible. |
75
76
 
76
77
  ## Command index
@@ -87,7 +88,7 @@ Commands are grouped here for evaluation; `mingdao --help` and `src/help.js` are
87
88
  | `mingdao web [port]` | Start the WebUI (default `http://127.0.0.1:3820`); `--auth-token` (or `--auth-token=-` from stdin). |
88
89
  | `mingdao diagnose` | Build one redacted diagnostic bundle (logs / audit / config) for bug reports. |
89
90
  | `mingdao audit [n]` | Show the tool-call audit log (default last 20 entries). |
90
- | `mingdao ledger list\|show\|export\|verify\|replay` | Execution ledger: inspect, export, verify the hash chain, or replay against current rules. |
91
+ | `mingdao ledger list\|show\|export\|verify\|replay` | Execution ledger: inspect, export, verify the hash chain and source signature, or replay against current rules. `ledger --sign-key [--generate\|--force]` shows or rotates the ed25519 signing key. |
91
92
  | `mingdao net report\|policy` | Egress record export and current `config.net` policy. |
92
93
  | `mingdao key [status\|set\|remove\|import] [provider]` | Manage the local credential store (never writes keys into `config.json`). |
93
94
  | `mingdao pack list\|verify\|info\|new\|trust\|untrust` | Discover, statically validate, scaffold, and trust domain Packs. |
@@ -111,7 +112,7 @@ Commands are grouped here for evaluation; `mingdao --help` and `src/help.js` are
111
112
  The full reference is **Chinese**; these are the entry points worth knowing:
112
113
 
113
114
  - [`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.
115
+ - [`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 provenance, local-endpoint pre-flight/serialization, timeouts and the turn-level watchdog, full field table.
115
116
  - [`docs/PACK-API.md`](docs/PACK-API.md) — the Pack contract (manifest, contributions, constraint kinds, and which of them are **not** implemented).
116
117
  - [`docs/PROVIDERS.md`](docs/PROVIDERS.md) — built-in providers and writing a custom OpenAI-compatible provider.
117
118
  - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — module map and data flow.
package/README.md CHANGED
@@ -299,7 +299,7 @@ src/ CLI / Agent 循环 / 工具 / 权限 / 技能库 / MCP / 云
299
299
  skills/ 14 个内置常驻技能
300
300
  skills-lib/ 22 个可安装技能库预设
301
301
  registry/ 线上技能 registry 索引
302
- presets/ 内置 Agent 预设(如 local-audit)
302
+ presets/ 内置 Agent 预设(只 `local-model`:只调参数,不含人格/工具白名单/权限)
303
303
  packs/ 内置示例 Pack(example-hello)
304
304
  assets/ 内置分词器数据(tokenizer-data.json.gz,约 762 KB,`src/tokenizer.js` 使用)
305
305
  desktop/ Electron 桌面版外壳(含自动更新、原生目录选择器)
package/SECURITY.md CHANGED
@@ -11,7 +11,7 @@ Security fixes are provided for the **latest minor release only**.
11
11
 
12
12
  | Version | Supported |
13
13
  | --- | --- |
14
- | **0.6.x** (current: **0.6.8**) | Yes |
14
+ | **0.6.x** (current: **0.6.14**) | Yes |
15
15
  | 0.5.x and older | No |
16
16
 
17
17
  Upgrade with `npm i -g mingdao-harness@latest` (npm installs), or `mingdao update` / `mingdao rollback`
@@ -48,7 +48,8 @@ Please do not open a public issue for an unfixed vulnerability. Credit is offere
48
48
  - `src/permissions.js` — the permission decision engine: `allow` / `deny` / `denyStrict` matching and
49
49
  the `readonly` / `ask` / `auto` modes.
50
50
  - `desktop/**` — the Electron shell: window navigation and `openExternal` handling, the contextBridge
51
- preload surface, IPC handlers.
51
+ preload surface, IPC handlers, and update-package signature verification (a bypass of the pinned key or
52
+ of the fail-closed policy is in scope).
52
53
 
53
54
  Reports that affect anything else in `src/` are welcome too; the list above is where the trust
54
55
  boundaries live.
@@ -90,14 +91,18 @@ boundaries live.
90
91
  5. **Treat Packs as untrusted code.** Run `mingdao pack verify <dir>` (static; `--runtime` executes the
91
92
  Pack) and read `pack.mjs` before `mingdao pack trust`; never trust a freshly cloned repository you
92
93
  have not reviewed.
93
- 6. **Enable the audit trail and the ledger.** Keep `config.audit` on, review `mingdao audit`
94
- periodically, and run `mingdao ledger verify` on turns you care about. For unattended jobs, prefer
94
+ 6. **Enable the audit trail and the ledger, and keep the signing key.** Keep `config.audit` on,
95
+ review `mingdao audit` periodically, and run `mingdao ledger verify` on turns you care about; a new
96
+ ledger is signed automatically with `<home>/ledger-key.json` (mode 600). Back that file up: replacing
97
+ it turns every previously signed ledger into "signed by another key". For unattended jobs, prefer
95
98
  `readonly` or `denyStrict` over `auto`.
96
99
  7. **Sandbox shell execution where you can.** On Linux with `bubblewrap` installed, set
97
100
  `config.sandbox` to `readonly` or `safe` (no network). Elsewhere the sandbox downgrades to `off` and
98
101
  says so — compensate with OS-level isolation or a container.
99
102
  8. **Run the desktop build only from a source you trust** and verify the checksum published on the
100
- Release page; current desktop builds are unsigned (see below).
103
+ Release page; current desktop builds are unsigned. If you require update provenance, set
104
+ `MINGDAO_REQUIRE_UPDATE_SIGNATURE=1` so an update package without a valid signature is **rejected**
105
+ (the default is to install but explicitly report that the signature was not verified).
101
106
 
102
107
  ## Known boundaries
103
108
 
@@ -114,15 +119,23 @@ price them in.
114
119
  2. **Packs have no isolation.** `pack.mjs` runs in the host process with full Node privileges, and
115
120
  `manifest.permissions` is not enforced by the kernel. Capability brokering or subprocess isolation
116
121
  for Packs requires an architectural change and is not implemented.
117
- 3. **Electron code signing and update-package signature verification are not done.** Current builds
118
- are unsigned (see [`docs/CODE-SIGNING.md`](docs/CODE-SIGNING.md), which describes the steps to sign);
119
- the auto-updater does not verify a signature on the downloaded update package beyond the transport.
122
+ 3. **Electron code signing / notarization is still not done.** Current builds are unsigned (see
123
+ [`docs/CODE-SIGNING.md`](docs/CODE-SIGNING.md), which describes the steps to sign). Update-package
124
+ **source-signature verification is implemented** (`desktop/update-verify.js`: ed25519 over the
125
+ installer bytes, public key pinned in the source; a present-but-invalid signature is always rejected),
126
+ but the release pipeline does not sign builds yet. Consequently the default policy is
127
+ `warn-and-install` (install, and state that the signature was not verified); set
128
+ `MINGDAO_REQUIRE_UPDATE_SIGNATURE=1` to make unsigned updates fail closed.
120
129
  4. **IDE plugin token storage needs hardening.** The VS Code and JetBrains plugins drive the local
121
130
  WebUI over loopback; the broader item (secure token storage, and distinguishing HTTP 401 from
122
131
  "server unreachable") is registered as open work.
123
- 5. **Ledger events are tamper-evident but not signed.** The hash chain plus the `run.end` seal detect
124
- modification and truncation after the fact, but there is no trusted timestamp and optional signing
125
- (`--sign-key`) is not implemented — so the ledger is not audit-grade non-repudiation, and
126
- `mingdao ledger verify` says exactly that in its own output.
132
+ 5. **Ledger provenance has the same limits as the machine it runs on.** The hash chain plus the
133
+ `run.end` seal detect modification and truncation after the fact, and since v0.6.11 the seal also
134
+ carries an **ed25519 source signature** (auto-generated key at `<home>/ledger-key.json`, mode 600;
135
+ `mingdao ledger --sign-key` to inspect/rotate; `verify` reports valid / unsigned / invalid /
136
+ unverifiable, with "unverifiable" also failing). The boundary: the private key and the ledger share
137
+ the same machine and permissions, so this does not defend against a local attacker who can already
138
+ rewrite the ledger; there is no trusted timestamp; and replacing the key makes old ledgers report
139
+ "signed by another key". It is provenance, not audit-grade non-repudiation.
127
140
 
128
141
  We do not claim this project is vulnerability-free, and this document is not a marketing statement.
package/docs/CONFIG.md CHANGED
@@ -111,7 +111,7 @@ WebUI 中每个会话记住自己的工作目录:新会话记录创建时的
111
111
  > 那里"一次误按 y 就绕过自己写的禁令"是不可接受的;
112
112
  > 交互式日常使用保持默认即可——每次放行都是一次明确的、点名规则的确认。
113
113
 
114
- ### 权限优先级与预设(v0.6.14)
114
+ ### 权限优先级与预设(v0.6.14,v0.6.16 收窄)
115
115
 
116
116
  口径:**显式选择 > 预设建议(`recommendedPermission`)> config.json**。
117
117
  (这句说的是"档位意图从哪来"的优先序;落到判定上,`recommendedPermission` **只是建议**——不参与判定、
@@ -120,26 +120,31 @@ WebUI 中每个会话记住自己的工作目录:新会话记录创建时的
120
120
  | 层级 | 从哪来 | 性质 |
121
121
  | --- | --- | --- |
122
122
  | 显式选择 | CLI `mingdao run "…" --permission auto\|ask\|readonly`、REPL `/mode`、WebUI「权限模式」下拉(随本次请求发出) | **覆盖**:本回合的终值,任何预设都压不过它 |
123
- | 预设建议 | 预设 JSON 的 `recommendedPermission`(内置 `local-audit` 即 `readonly`);`GET /api/presets` 会透出该字段 | **只是建议**:不参与权限判定、不静默改档;想让建议生效,就在上面那一层显式选它 |
123
+ | 预设建议 | 预设 JSON 的 `recommendedPermission`(**内置预设不使用它**;字段支持保留给第三方/自写预设);`GET /api/presets` 会透出该字段 | **只是建议**:不参与权限判定、不静默改档;想让建议生效,就在上面那一层显式选它 |
124
124
  | config.json | `permission` 字段(支持 `{mode, allow, deny}` 对象形态) | **兜底**:没有显式选择、预设也没声明 `permission` 时才用它 |
125
125
 
126
126
  三条要点(别再踩):
127
127
 
128
- - **预设的 `tools` 白名单才是只读的硬约束。** 内置 `local-audit` 的白名单里没有 `write`/`edit`,
129
- 写工具根本不会出现在模型可见的工具表里——这比任何权限档都硬(权限档只决定"要不要逐次询问")。
130
- 所以"要一个只读预设"的正确做法是**收白名单**,不是把会话钉死在 `readonly`。
128
+ - **要"只读",请自己组合权限档与工具白名单。** 预设的 `tools` 白名单是只读的**硬约束**
129
+ (白名单里没有 `write`/`edit`,写工具根本不会出现在模型可见的工具表里——这比任何权限档都硬,
130
+ 权限档只决定"要不要逐次询问");把会话钉在 `readonly` 档则是另一层。内置预设**不替你决定**这两件事,
131
+ 正确的做法是在权限档里显式选 `readonly`,并在自己的预设里收 `tools` 白名单。
131
132
  - **`permission` 字段是覆盖。** 预设声明它会改本回合的档位(更宽松的方向被反提权拦掉:
132
- `readonly→auto` 一律拒绝并只发一条 ⚠ banner)。**内置预设不得声明 `permission`**——它唯一的
133
- 实际效果是"沉默覆盖用户的选择":负责人实测的坑就是界面上选了「自动」,勾上内置审计预设后仍按
133
+ `readonly→auto` 一律拒绝并只发一条 ⚠ banner)。**内置预设不携带任何权限字段**——`permission`
134
+ 的实际效果只是"沉默覆盖用户的选择":负责人实测的坑就是界面上选了「自动」,勾上当时的审计预设后仍按
134
135
  `readonly` 跑,于是每调用一次非只读工具都弹「只读模式将拦截 task,是否本次放行?」。
135
136
  第三方/老预设仍可声明(兼容),但同样受反提权约束,且要自己承担"覆盖用户选择"的后果。
136
- - **`recommendedPermission` 只是建议。** 它是预设对"我适合跑在哪个档"的表达,只做展示/透出,
137
- 不写进任何权限判定;权限档以你的显式选择为准,其次才是 config.json 里的默认值。
138
- 内置 `local-audit` 用它表达"建议只读",只读本身则由 tools 白名单落到实处。
139
-
140
- > 另一个常被名字误导的点:内置 `local-audit`(「只读代码审计(本地模型)」)**不启用审计日志/账本**。
141
- > 工具调用审计是 `config.audit` 与 `mingdao audit` / `mingdao ledger` 的事,与这个预设无关——
142
- > 别把它当合规审计开关。
137
+ - **`recommendedPermission` 只是建议,且内置预设不使用它。** 它是预设对"我适合跑在哪个档"的表达,
138
+ 只做展示/透出,不写进任何权限判定;权限档以你的显式选择为准,其次才是 config.json 里的默认值。
139
+ v0.6.16 起**内置预设连这个字段也不用**(避免任何形式的权限覆盖/权限偏好暗示)——字段本身保留,
140
+ 第三方/自写预设可以继续用它。
141
+
142
+ > **内置预设只提供参数类默认值。** 发行版面向普通大众,不为某种任务做定制,因此内置只保留
143
+ > `local-model`(只调 `contextBudget`/`maxOutputTokens`/`maxRounds`,**不加人格、不限制工具、
144
+ > 不涉及权限**)。v0.6.16 已**删除**原来的内置「只读代码审计」预设 `readonly-audit`(它只是负责人
145
+ > 拿 MDH 做的一次较长任务的测试);老名字 `local-audit` 仍可用,但会提示已更名为 `local-model`。
146
+ > 另:审计日志/账本与预设无关——那是 `config.audit` 与 `mingdao audit` / `mingdao ledger` 的事,
147
+ > 别把任何预设当合规审计开关。
143
148
 
144
149
  ## 项目记忆(自动写入项目目录,默认已自忽略)
145
150
 
@@ -442,7 +447,7 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
442
447
  ——桌面壳日志取的就是渲染进程 console,此前那里只有「回合收尾:generating=false」,
443
448
  与内核的 `status=stalled` 对不上。
444
449
 
445
- #### 端点预检(v0.6.13,本地端点自动执行)
450
+ #### 端点预检(v0.6.13,本地端点自动执行;v0.6.13 修复误报)
446
451
 
447
452
  对**本地端点**(baseUrl 是回环/内网,或 `customModels.<名>.local: true`)在开工前做一次轻量预检
448
453
  (结果缓存 10 分钟,只读不写你的配置):
@@ -456,6 +461,35 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
456
461
  3. 再发一次 **参考 prefill**(约 2000 tokens 的提示)测耗时;
457
462
  4. 与本机历史值比对:**同一端点明显变慢**(≥3 倍)会提示"该端点可能仍在处理上一个请求,建议重启引擎"。
458
463
 
464
+ **预检的耗时与"失败不阻断"(v0.6.13 修复)**
465
+
466
+ - **探针只打 `POST {base}/chat/completions`**(与主请求同一个 URL),**不发** `GET /models`;
467
+ `GET /props` 只在引擎自报模型名后才打。所以"只实现了 chat 的端点"**一个 GET 都不会收到**,
468
+ 不会被预检打扰(`test/smoke.js` §141 用桩服务钉住"GET 次数 = 0")。
469
+ - **单次尝试 1.5s、共 3 次、尝试之间退避 300/700ms**,整个探针的时间预算 15s(`timeoutMs` 是**总预算**,
470
+ 不是单次超时);参考 prefill 单独放宽到 20s(本机 35B 实测 6.6~7.8s/2000 tokens)。
471
+ 单次超时**覆盖到首帧**:引擎"回了响应头、卡在 prefill 或正在启动"也算超时,不会把回合发起一直挂着。
472
+ - **预检是尽力而为:失败只降级、不阻断**。探针自身任何异常都被吞掉(只在 `web-server.log` 留一行
473
+ "预检 … 失败(忽略,按默认超时继续)"),本回合照常发起——预检结论只影响 banner、日志与自适应超时。
474
+ - **失败原因分开判、分开说**(`web-server.log` 的 `chat 预检 … reason=…` 和 banner 文案都跟着变):
475
+
476
+ | `reason` | 判据 | 文案(节选) | 你该做什么 |
477
+ | --- | --- | --- | --- |
478
+ | `refused` | `ECONNREFUSED` | `❌ 不可达:连接被拒(端口没人监听)` | 这才是"引擎没起来/端口不对":确认端口与进程 |
479
+ | `timeout` | 1.5s 内没有首帧 | `⏱ 探针超时:1.5s 内没有首帧` | 端口是通的:引擎在 prefill 或**正在启动**;不代表主请求会失败 |
480
+ | `reset` | 连接被对端断开 | `❌ 连接被断开` | 引擎可能刚崩/正在重启 |
481
+ | `http-unimplemented` | HTTP 404/405/501 | `❌ 探针被拒绝:HTTP 404(端点未实现该探针请求)` | 该端点没有实现 `POST {base}/chat/completions`(探针与主请求同一个 URL) |
482
+ | `http-auth` / `http` | 401/403、其它 4xx/5xx | `HTTP 401(鉴权被拒)` 等 | 端口是通的:按状态码排查密钥/路由 |
483
+ | `other` | 其它网络错误 | `❌ 探针失败:其它错误(…)` | 按错误码排查,不要当成"引擎没起来" |
484
+
485
+ - **失败结论只保鲜 60 秒**(成功结论仍缓存 10 分钟)。这条是**真机误报的根因**:v0.6.13 早期版本的
486
+ 进程内备忘没有 TTL,进程启动时(引擎还没起来)留下的"不可达"被之后**每一个新回合**原样重放
487
+ (真机 `web-server.log:413` 07:03:02 的真实探测 → `:416` 07:08:13、`:799` 10:13:58 两次重放,
488
+ 相隔 7 小时 10 分钟),而同一回合其实完全可用(`:803` 首帧 8.7s)。现在超过 60 秒会**重测**。
489
+ - **复核行**:如果预检判过"不可达",而这一回合**真的出过首帧**,回合结束后日志会留一行
490
+ `chat 预检复核 <taskId> 预检曾判不可达(reason=…),但本回合实际可用(首帧 Xs)——预检判据需复核`
491
+ ——看到它说明"预检判据"该被质疑,而不是"引擎没起来"。
492
+
459
493
  #### 预检读「引擎自述上下文」(v0.6.13)
460
494
 
461
495
  本地端点预检时**额外读一次引擎自述**(`GET /props`,只读、不改配置):
@@ -497,6 +531,52 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
497
531
  既有默认, 1800s)、无进展看门狗 = clamp(max(实测首帧×100, 参考prefill×20), 10min, 60min);
498
532
  **你显式配置的 `timeout.*` / `noProgressTimeoutMs` 永远优先**(显式 > 自适应)。
499
533
 
534
+ #### 本地模型:并发与内存(v0.6.15)
535
+
536
+ 真机证据(v0.6.14,8081 的 `mtplx-qwen38-27b-optimized-quality`):长任务中途被引擎拒绝——
537
+ `insufficient memory: the request exceeded available GPU memory (sustained critical memory pressure
538
+ during prefill; aborted before the allocator wall). The engine shed its caches and stays up; this
539
+ request failed.` 同一错误此前在 60091 的 35B 上也出现过 → **不是模型问题,是本机内存压力**。
540
+ 观测到该回合此前有子代理在跑(13 轮 / 17 次工具调用)→ **父回合 + 子代理同时向同一本地引擎
541
+ 发大 prefill**(各自要一份 KV cache)是主要来源。所以内核现在对**同一 `baseUrl`** 的本地端点
542
+ 在途请求做**串行化/限量**:
543
+
544
+ - **默认 1 并发**:父回合、子代理(`task`)、**端点预检**(探针打的是同一个 `/chat/completions`)
545
+ 共用同一条**进程级 Promise 队列**,谁先到谁先跑,后到的排队(不是忙等,释放即移交名额)。
546
+ - **配置**(与 `timeout` 同级,写在 `config.json` 顶层):
547
+
548
+ ```json
549
+ {
550
+ "localConcurrency": 1,
551
+ "localQueueTimeoutMs": 1800000
552
+ }
553
+ ```
554
+
555
+ | 字段 | 默认 | 含义 |
556
+ | --- | --- | --- |
557
+ | `localConcurrency` | `1` | 同一本地 `baseUrl` 的同时在途请求上限。`0` = **显式退出串行化**(不限并发);正整数上限 16;非法/负数回落 1 |
558
+ | `localQueueTimeoutMs` | `1800000`(30 分钟) | 排队等待上限。超过即报**可见错误**(不重试、不死等),避免"看起来在跑、其实永远排不到" |
559
+
560
+ - **云端端点不受影响**:`baseUrl` 不是回环/内网(如 `api.deepseek.com`)时直接放行——不排队、
561
+ 不记日志、不增加任何延迟。`customModels.<名>.local: true` 可把经反代的本地端点纳入串行化。
562
+ - **可见性**:入队时写一行 `本地端点串行化:等待 N 个在途请求(端点,并发上限 1)`
563
+ (WebUI 落 `~/.mingdao/logs/web-server.log`;CLI 写 stderr);界面状态条/进度行显示
564
+ `本地端点已串行化(防内存压力)`,发送前的 `📏 本轮上下文` 预告在**已有在途请求**时会加一条强警告
565
+ `并发会加剧内存压力 … 建议减少子代理数量`。
566
+ - **排队超时的报错**(`code=LOCAL_QUEUE_TIMEOUT`):
567
+ `本地端点排队超时(等待 1800.0s,仍有 1 个在途请求,并发上限 1):为避免内存压力已在本机端点串行化——可调大 config.localQueueTimeoutMs,或减少子代理/并行会话。`
568
+ - **引擎活着但丢了缓存**:上游文案含 `engine shed its caches and stays up` / `allocator wall` 时,
569
+ 错误分型会明确"**该引擎未崩溃**(自己丢弃缓存后继续运行)"、**不要重启引擎**,处置是
570
+ 压上下文(`contextBudget` / `/compact`)、减少并发、等它自行恢复后重试。这与"引擎崩了/连接被拒"
571
+ (`ECONNREFUSED`、连接被断开)是**不同**的两种判定,后者才需要确认端口与进程。
572
+ - **排查步骤**(还遇到内存拒绝时):
573
+ 1. 看 `web-server.log` 有没有 `本地端点串行化:等待 N 个在途请求`——有排队说明闸门在工作,
574
+ 若仍被拒绝,说明单请求本身就超了引擎内存(压 `contextBudget`,或让 `customModels.<名>.contextWindow`
575
+ ≤ 引擎自述的 `n_ctx`,见上一节);
576
+ 2. 确认没有多个会话/多个本地模型同时跑长任务(每个模型各占一份显存与 KV cache);
577
+ 3. 临时把 `localConcurrency` 保持 `1`,并减少子代理数量(读只读子代理在本地模型下已不并行);
578
+ 4. **不要**因为 `engine shed its caches and stays up` 去重启引擎——它没有崩;重启只会重新加载模型。
579
+
500
580
  ### 用本地模型测 harness(测试者步骤清单)
501
581
 
502
582
  > 这一段是**操作清单**,不是原理说明。按顺序做,能在几分钟内区分"端点不行"和"harness 不行"。
@@ -547,7 +627,8 @@ curl -s http://127.0.0.1:8081/v1/models | head -c 400 # 端口按你的部
547
627
 
548
628
  | 现象 | 内核会给什么信号 |
549
629
  | --- | --- |
550
- | 引擎没起来 / 端口不对 | 预检 `❌ 不可达:…`;请求以 `网络请求失败` 报错(几秒内) |
630
+ | 引擎没起来 / 端口不对 | 预检 `❌ 不可达:连接被拒(端口没人监听)`(`reason=refused`);请求以 `网络请求失败:fetch failed` 报错(几秒内)。**注意区分**:`⏱ 探针超时`(端口是通的)/ `❌ 探针被拒绝:HTTP 404`(端点没实现该请求)都不是"引擎没起来"——见上面「端点预检」的失败原因表 |
631
+ | 引擎正在启动(端口刚 bind、模型在加载) | 预检会退避重试(300/700ms);仍没等到首帧则记 `reason=timeout`——**它不代表本回合会失败**,主请求照常发起;若本回合真的出过首帧,回合结束后日志会写「预检曾判不可达 … 但本回合实际可用(首帧 Xs)——预检判据需复核」 |
551
632
  | 连上但不发首帧 | 到 `timeout.firstTokenMs` 报「首 token 等待超限(Ns,本地模型长上下文 prefill 可能很慢)——可调大 config.timeout.firstTokenMs…」 |
552
633
  | 有帧但长时间没新数据 | 到 `timeout.streamIdleMs`(默认 120s)报「流式响应空闲超限(Ns 无新数据)」 |
553
634
  | 每轮都能返回、但什么都没做成 | 到 `noProgressTimeoutMs`(默认 60min,快端点自适应到 10min)报「连续 N 分钟没有任何工具调用、也没有新内容,已中止」,并附**中止时正在等待哪个工具/等了多久**;界面另有告警 banner(与 `capped` 跑满步数、`aborted` 你点的停止**区分开**) |
@@ -618,6 +699,11 @@ mingdao ledger verify <runId> --key <文件> # 用指定公钥验签(本
618
699
  顺手一次就会把签名密钥清掉、让全部历史账本变成"另一把密钥签发"。
619
700
  - **默认开启**:新账本在首次收尾时若本机还没有密钥会**自动生成**并签名(可选签名在合规上等于
620
701
  没有签名)。封条里带 `sig{alg,keyId,value}`,`run.end` 链内带 `sigKey`(公钥指纹)。
702
+ - **`run.start.permissionSource`(v0.6.17 §3.64)**:记录本回合生效权限的**来源**——`explicit`
703
+ (用户/调用方显式选择,如 WebUI 权限档)/ `preset`(预设声明生效)/ `config`(`config.json`
704
+ 的 `permission`)/ `cli`(后台/调度任务由 CLI 参数指定)/ `default`(以上都没有,落到内置缺省
705
+ `ask`;WebUI 的 `config` 分支在缺字段时也归 `config`)。`permission` 仍是生效值、`LEDGER_VERSION`
706
+ 不变;旧账本没有该字段照样 `verify` 通过(`show` 只是少一行可选的「权限来源」)。
621
707
  - **`ledger verify` 的三种结论**(前两者退出码 0,第三者非 0):
622
708
 
623
709
  | 结论 | 含义 | 退出码 |
@@ -636,3 +722,9 @@ mingdao ledger verify <runId> --key <文件> # 用指定公钥验签(本
636
722
  (按模型分账、每日费用柱状图、缓存命中率、Batch 子项),文件落当前目录
637
723
  `mingdao-cost-report-<月>.md`。数据源为 cache-stats.jsonl(含缓存折扣与 Batch 半价的真实口径)。
638
724
 
725
+
726
+ ## 开发工具:变异验证的 fail-safe
727
+
728
+ `node test/mutate/run.mjs` 启动时会先自检残留标记(能自愈的自愈,冲突/在飞则拒绝),
729
+ 并用 `test/mutate/.generated/mutate.lock` 做互斥;详见 `test/mutate/FAILSAFE.md`。
730
+ 手工自检:`node test/mutate/check.mjs`(`--allow-dirty` 可放行脏工作树)。
package/docs/DEVELOPER.md CHANGED
@@ -25,13 +25,16 @@
25
25
 
26
26
  1. `<项目>/.mingdao/presets/<名>.json` — 项目级(随项目走)
27
27
  2. `~/.mingdao/presets/<名>.json` — 用户级(本机全局)
28
- 3. `presets/`(随 npm 包分发)— 内置参考,现内置两个**语义单一**的示例:
28
+ 3. `presets/`(随 npm 包分发)— 内置参考。**内置预设的定位:只提供参数类默认值——不携带人格、
29
+ 不限制工具、不涉及权限**(发行版面向普通大众,不为某种任务做定制)。因此内置**只有一个**:
29
30
  - `local-model` —— **只放"让本地模型跑得动"的参数**(`contextBudget`/`maxOutputTokens`/`maxRounds`)。
30
- 本地模型只是替换云模型 API,**功能一致**:所以它不带人格、不限制工具、不建议权限档。
31
- - `readonly-audit` —— 审计人格 + 只读工具白名单(`tools`)+ `recommendedPermission: "readonly"`(只是**建议**)。
31
+ 本地模型只是替换云模型 API,**功能一致**:所以它不带人格、不限制工具、不涉及权限。
32
32
  - 老名字 `local-audit` 保留为**别名**(指向 `local-model`):按老名字调用仍可用,且会打印一次
33
33
  「已更名」提示(CLI/内核日志)并在 WebUI 用 banner 说明——改名兼容,但不静默。
34
34
  若你自己在项目级/用户级写了 `local-audit.json`,**以你的文件为准**(别名只在没找到同名预设时兜底)。
35
+ - v0.6.16 起内置**不再有**「只读代码审计」预设(`readonly-audit` 已删除:代码审计只是负责人拿
36
+ MDH 做的一次较长任务的测试,发行版不做任务定制)。要只读效果请**自己组合**权限档(`readonly`)
37
+ 与工具白名单(`tools`)——见 §1.2。
35
38
 
36
39
  ### 1.2 格式
37
40
 
@@ -60,10 +63,11 @@
60
63
 
61
64
  | 字段 | 语义 | 谁该用 |
62
65
  | --- | --- | --- |
63
- | `permission` | **覆盖**本回合的权限档(仍受反提权规则约束:只能更保守,不能更宽松) | 第三方/老预设;**内置预设不得声明** |
64
- | `recommendedPermission` | **建议**:只透出给界面/诊断,**不参与判定、不覆盖用户选择** | 内置预设想表达"建议只读"时用它 |
66
+ | `permission` | **覆盖**本回合的权限档(仍受反提权规则约束:只能更保守,不能更宽松) | 第三方/老预设;**内置预设不涉及权限,不使用它** |
67
+ | `recommendedPermission` | **建议**:只透出给界面/诊断,**不参与判定、不覆盖用户选择** | 第三方/自写预设想表达"建议档"时可用;**内置预设不使用它**(避免任何权限偏好暗示) |
65
68
 
66
69
  只读的**硬约束**是 `tools` 白名单(不含 `write`/`edit`,模型连写工具都看不到),不是权限档。
70
+ 内置预设不替你决定权限档,也不限制工具:要只读就**自己组合** `permission.mode: "readonly"` 与 `tools` 白名单。
67
71
 
68
72
  #### 参数怎么给"本地模型"用(v0.6.15)
69
73
 
@@ -79,8 +83,10 @@
79
83
  且大上下文会顶满引擎内存预算)。给 32k–65k 通常就够单个模块级任务。
80
84
  - 模型窗口/最大输出等**能力声明**请写在 `config.customModels.<模型名>`
81
85
  (`contextWindow`/`maxOutputTokens`/`local`),不要再写一份进预设——两处会漂移。
82
- - 想让"本地模型 + 只读审计"同时生效:两个预设**不能叠加**(一次只能选一个)。
83
- 要么用 `readonly-audit` 并自己把 `contextBudget` 调小,要么复制一份按需合并。
86
+ - 想同时要"本地模型的保守参数 + 只读":预设**不能叠加**(一次只能选一个内置预设),而且 v0.6.16 起
87
+ 内置不再提供只读审计预设。自己的做法是**复制一份 `local-model` 再按需合并**:在 `tools` 白名单里
88
+ 只留只读工具,权限档在你那一层显式选 `readonly`(预设里**不要**写 `permission`/`recommendedPermission`
89
+ ——那会分别造成"沉默覆盖"与"权限偏好暗示")。
84
90
 
85
91
  ### 1.3 使用
86
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mingdao-harness",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "local-model",
3
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),本预设只覆盖本回合参数。",
4
+ "description": "给**本地部署模型**用的保守参数预设。先说清两件事:① 本地模型与云模型**功能一致**——本预设不加人格、不限制工具、不建议权限档,只把「上下文预算 / 单次最大输出 / 轮次」调到本地推理跑得动的量级;② 它**不是**某个任务的专用预设(发行版面向普通大众,内置预设只提供参数类默认值,不携带人格、不限制工具、不涉及权限)。为什么只调这几个数:本地引擎的瓶颈在 prefill 与显存/内存预算(本机实测 35B Q4_K_M:约 4.9ms/token,2 万 tokens 上下文的 prefill ≈ 1.6 分钟,13 万窗口 ≈ 10 分钟以上,还会把引擎的内存预算顶满)——预算给小一点,任务跑得完、也更容易看清是哪一步慢。模型窗口/最大输出等**能力声明**请写在 config.customModels.<模型名>(contextWindow/maxOutputTokens),本预设只覆盖本回合参数。需要只读效果时,请自行组合**权限档**(readonly)与**工具白名单**。",
5
5
  "contextBudget": 65536,
6
6
  "maxOutputTokens": 4096,
7
7
  "maxRounds": 4