dsh-dashcam 1.12.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **English** | [中文](README.zh-CN.md)
4
4
 
5
- ![CI](https://github.com/odysseykernelt/dsh-dashcam/actions/workflows/ci.yml/badge.svg) ![version](https://img.shields.io/badge/version-1.12.0-blue) ![node](https://img.shields.io/badge/node-%3E%3D%2020-green) ![license](https://img.shields.io/badge/license-MIT-lightgrey) ![status](https://img.shields.io/badge/status-WIP-orange)
5
+ ![CI](https://github.com/odysseykernelt/dsh-dashcam/actions/workflows/ci.yml/badge.svg) ![version](https://img.shields.io/badge/version-2.0.0-blue) ![node](https://img.shields.io/badge/node-%3E%3D%2020-green) ![license](https://img.shields.io/badge/license-MIT-lightgrey) ![status](https://img.shields.io/badge/status-WIP-orange)
6
6
 
7
7
  **dsh-dashcam is a dashcam for your AI plugins — it records what they actually do, it does not decide what they may do.**
8
8
 
@@ -35,7 +35,7 @@ If you later want judgment on top of recording, add `config.requests` (what the
35
35
 
36
36
  **If you are not comfortable with a plugin watching plugin activity, do not install it.** This is the primary advice, not a disclaimer: the ledger is a high-value target (it knows exactly which files were touched and when), the host adapter runs inside your agent's main process, and "it only records" is precisely why it sees everything. Verify the clone, keep the ledger out of backups you do not trust, set an HMAC key (`ledgerMacKey`) if tamper-resistance matters — or skip the install; the dashcam works hardest at earning that skepticism.
37
37
 
38
- > **TL;DR — four steps on the DSH desktop host**: clone this repo, then add the dependency `"dsh-dashcam-host": "link:<clone-path>/dsh-dashcam-host"` to the profile's `package.json`, append `dsh-dashcam-host` to `dsh.profile.bundles`, run one `pnpm install`, restart DeepSeek Harness — done. Details in [Fast install](#fast-install-dsh-desktop-host). (An npm distribution of the host adapter is on the roadmap; for now, install from a clone.)
38
+ > **TL;DR — one command, complete plugin**: `dsh plugin --profile web add dsh-dashcam`, restart DeepSeek Harness — done. The same works with `--profile desktop`. By default it only observes and never blocks a call. Details in [Install](#install-dsh-host).
39
39
 
40
40
  Underneath the dashcam sits the machinery built up over previous versions: install-time gates (structure / safety / signature), capability portraits, offline autopsy, ingress reconciliation. They still run — but in dashcam mode their output is evidence and labels, not verdicts.
41
41
 
@@ -160,6 +160,7 @@ src/
160
160
  governance/ three-tier rollback, blacklist, monthly metrics, circuit breaker
161
161
  host/ LocalHost: install → register → invoke → uninstall
162
162
  bin/ CLI
163
+ dsh-dashcam-host/ host adapter: plugin entry (judgment chain), browser panel, build script
163
164
  docs/ specs, rollout cadence, compliance
164
165
  examples/ positive package, negative package, demo policy
165
166
  test/ contracts, runtime, bypasses, gates, signing, portraits, autopsy, governance, audit, admission
@@ -168,7 +169,7 @@ test/ contracts, runtime, bypasses, gates, signing, portraits, autopsy,
168
169
  ## Host integration
169
170
 
170
171
  ```js
171
- const { activate } = require('dsh-dashcam');
172
+ const { activate } = require('dsh-dashcam/core');
172
173
 
173
174
  const plugin = activate({
174
175
  manifest, // parsed manifest
@@ -185,14 +186,50 @@ plugin.deactivate();
185
186
 
186
187
  `activate` installs native module hooks at the guard entrance (covering in-process `fs/net/child_process/env` calls) and registers the pre-execute judgment function. If the host only wants judgments without patching, pass `patchBuiltins: false`.
187
188
 
188
- ## Fast install (DSH desktop host)
189
+ ## Install (DSH host)
189
190
 
190
- DSH loads plugins per profile: `~/.dsh/profiles/desktop/package.json` `dependencies` decide what gets installed and `dsh.profile.bundles` decides what gets enabled; the bundle's own `cordis.patch.yml` wires the plugin into the host config tree. Hooking the judgment chain in takes four steps:
191
+ One command installs the complete plugin — judgment chain, ledger, and the Agent Dashcam browser panel:
191
192
 
192
- 1. **Mount**: clone this repo, then add the dependency `"dsh-dashcam-host": "link:<clone-path>/dsh-dashcam-host"` to the profile's `package.json` (pointing at the `dsh-dashcam-host` directory inside the clone), and append `dsh-dashcam-host` to `dsh.profile.bundles`.
193
- 2. **Install**: run `pnpm install` once in the profile directory (using the node/pnpm bundled with the host runtime).
194
- 3. **Restart** DeepSeek Harness; success is confirmed when the log shows `[dsh-dashcam] 判定链已挂载` ("judgment chain mounted") (logs live in `%APPDATA%\@deepseek-ai\dsh-desktop\logs`).
195
- 4. **Uninstall**: remove the dependency and bundle, restore `cordis.patch.yml`, run `pnpm install` again, and restart.
193
+ ```bash
194
+ dsh plugin --profile web add dsh-dashcam # or: dsh plugin --profile desktop add dsh-dashcam
195
+ ```
196
+
197
+ Restart DeepSeek Harness after installing. **By default the dashcam only observes and never blocks**: every mapped tool call is recorded to the ledger, nothing is denied — judgment on top of recording is opt-in (`config.requests` / `config.grants` + `enforce: true`).
198
+
199
+ **Supported profiles**: `desktop` / `web`. The judgment chain and ledger run in the host main process on both; the browser panel targets the web client (`dsh.client.platform: web` in the shipped package).
200
+
201
+ **What you should see.** On startup the host log (`%APPDATA%\@deepseek-ai\dsh-desktop\logs`) shows:
202
+
203
+ ```
204
+ [dsh-dashcam] 判定链已挂载(event=tools/pre-execute,mode=unprofiled,enforceUnmapped=false,commandlineArchive=hash,patchBuiltins=false,ledger=C:\Users\you\.dsh\dsh-dashcam\ledger.jsonl,requests=0,grants=0)
205
+ ```
206
+
207
+ Every observed tool call then lands in `~/.dsh/dsh-dashcam/ledger.jsonl` (fields elided):
208
+
209
+ ```jsonl
210
+ {"v":4,"plugin":"dsh-dashcam-host","pluginVersion":"1.14.0","capability":"fs","action":"fs.read","scope":{"path":"C:/repo/src/app.ts"},"decision":"allow","ruleId":"unprofiled","profiled":false,"manifestHash":null,"seq":1,"ts":"2026-11-09T14:09:14.822Z","prevHash":"GENESIS","hash":"b504c225…"}
211
+ ```
212
+
213
+ The chain can be re-verified offline at any time — no host process required:
214
+
215
+ ```bash
216
+ npx dsh-dashcam verify-ledger ~/.dsh/dsh-dashcam/ledger.jsonl
217
+ ```
218
+
219
+ ```json
220
+ {
221
+ "ok": true,
222
+ "records": 2,
223
+ "lastSeq": 2,
224
+ "head": "6003e9f193a63198cfbc2039f296029d669d98337cb52c213da5e21238f5300b",
225
+ "macVerified": false,
226
+ "errors": []
227
+ }
228
+ ```
229
+
230
+ **Uninstall** is one command too: `dsh plugin --profile web remove dsh-dashcam` — the judgment chain is detached and no hooks remain.
231
+
232
+ **Upgrading from a link install (pre-2.0)**: remove the `"dsh-dashcam-host": "link:…"` dependency from the profile's `package.json` and the `dsh-dashcam-host` entry in `dsh.profile.bundles`, then install with the one-liner above. The default ledger path is unchanged (`~/.dsh/dsh-dashcam/ledger.jsonl`); existing records and their hash chain are preserved as-is — nothing to migrate by hand.
196
233
 
197
234
  The adapter package defaults to `patchBuiltins: false`: the host is an Electron main process and the Bridge is a process-level patch; installing it directly would take over the app's own fs/net, so by default only the pre-execute judgment function is registered. Set `patchBuiltins: true` explicitly in the cordis config when real interception is needed (semantics above in "Host integration").
198
235
 
@@ -271,7 +308,7 @@ const report = bridge.coverage();
271
308
  The Bridge can only achieve "everything I thought of is wrapped". To genuinely achieve "not declared, therefore impossible", invert the model: do not give the plugin the global API, give it a restricted object instead.
272
309
 
273
310
  ```js
274
- const { createCapabilityHandles } = require('dsh-dashcam');
311
+ const { createCapabilityHandles } = require('dsh-dashcam/core');
275
312
 
276
313
  const handles = createCapabilityHandles({ guard });
277
314
  await handles.fs.promises.readFile('cache.json'); // passes the guard chain
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | **中文**
4
4
 
5
- ![CI](https://github.com/odysseykernelt/dsh-dashcam/actions/workflows/ci.yml/badge.svg) ![version](https://img.shields.io/badge/version-1.12.0-blue) ![node](https://img.shields.io/badge/node-%3E%3D%2020-green) ![license](https://img.shields.io/badge/license-MIT-lightgrey) ![status](https://img.shields.io/badge/status-WIP-orange)
5
+ ![CI](https://github.com/odysseykernelt/dsh-dashcam/actions/workflows/ci.yml/badge.svg) ![version](https://img.shields.io/badge/version-2.0.0-blue) ![node](https://img.shields.io/badge/node-%3E%3D%2020-green) ![license](https://img.shields.io/badge/license-MIT-lightgrey) ![status](https://img.shields.io/badge/status-WIP-orange)
6
6
 
7
7
  **dsh-dashcam 是给 AI 插件装的行车记录仪——它记录插件实际做了什么,不替你决定插件能做什么。**
8
8
 
@@ -35,7 +35,7 @@
35
35
 
36
36
  **不放心就别装。** 这是主要建议,不是免责声明:账本本身就是高价值目标(它精确知道你碰过哪些文件、什么时候碰的),宿主适配包跑在 agent 主进程里,而「它只记录」恰恰意味着它什么都看得见。校验 clone 来源,别把账本放进你不信任的备份,在意防篡改就配 HMAC 密钥(`ledgerMacKey`)——或者干脆不装;行车记录仪最不怕的就是这种怀疑。
37
37
 
38
- > **极速上手(DSH 桌面宿主,四步)**:clone 本仓库,在 profile 的 `package.json` 加依赖 `"dsh-dashcam-host": "link:<clone路径>/dsh-dashcam-host"`,把 `dsh-dashcam-host` 追加进 `dsh.profile.bundles`,跑一次 `pnpm install`,重启 DeepSeek Harness 即生效。详见[极速安装](#极速安装dsh-桌面宿主)。(宿主适配包的 npm 分发在路线图上,目前请以 clone 方式安装。)
38
+ > **极速上手(一条命令,装出完整插件)**:`dsh plugin --profile web add dsh-dashcam`,重启 DeepSeek Harness 即生效(`--profile desktop` 同样可用)。默认只观察、不拦截任何调用。详见[安装](#安装dsh-宿主)。
39
39
 
40
40
  行车记录仪下面,是历届版本积攒的全部机器:安装门禁(结构/安全/签名)、能力画像、离线诊断、入站对账。它们照常运行——但在行车记录仪模式下,它们产出的是证据和标签,不是判决。
41
41
 
@@ -173,6 +173,7 @@ src/
173
173
  governance/ 三级回滚、黑名单、月度度量、熔断
174
174
  host/ LocalHost:装 → 注册 → 调用 → 卸载
175
175
  bin/ CLI
176
+ dsh-dashcam-host/ 宿主适配:插件入口(判定链)、浏览器面板、构建脚本
176
177
  docs/ 规范、上线节奏、合规
177
178
  examples/ 正例包、反例包、演示策略
178
179
  test/ 契约、运行时、绕过、门禁、签名、画像、诊断、治理、审计、准入
@@ -181,7 +182,7 @@ test/ 契约、运行时、绕过、门禁、签名、画像、诊断、
181
182
  ## 宿主集成
182
183
 
183
184
  ```js
184
- const { activate } = require('dsh-dashcam');
185
+ const { activate } = require('dsh-dashcam/core');
185
186
 
186
187
  const plugin = activate({
187
188
  manifest, // 已解析的 manifest
@@ -199,18 +200,50 @@ plugin.deactivate();
199
200
  `activate` 会在守卫入口安装原生模块 Hook(看护进程内 `fs/net/child_process/env` 调用)并注册 pre-execute 判定函数。
200
201
  如果宿主只想要判定、不想打补丁,传 `patchBuiltins: false`。
201
202
 
202
- ## 极速安装(DSH 桌面宿主)
203
+ ## 安装(DSH 宿主)
203
204
 
204
- DSH 按 profile 装载插件:`~/.dsh/profiles/desktop/package.json` 的 `dependencies` 决定装什么、
205
- `dsh.profile.bundles` 决定启用谁;bundle 自带的 `cordis.patch.yml` 负责把插件插进宿主配置树。
206
- 按这个机制把判定链挂进去只需四步:
205
+ 一条命令装出完整插件——判定链、账本、Agent Dashcam 浏览器面板:
207
206
 
208
- 1. **挂载**:clone 本仓库,在 profile 的 `package.json` 加依赖 `"dsh-dashcam-host": "link:<clone路径>/dsh-dashcam-host"`(指向 clone 内的 `dsh-dashcam-host` 目录),
209
- 并把 `dsh-dashcam-host` 追加进 `dsh.profile.bundles`。
210
- 2. **安装**:在 profile 目录跑一次 `pnpm install`(用宿主自带运行时的 node/pnpm)。
211
- 3. **重启** DeepSeek Harness;日志出现 `[dsh-dashcam] 判定链已挂载` 即为成功
212
- (日志在 `%APPDATA%\@deepseek-ai\dsh-desktop\logs`)。
213
- 4. **卸载**:摘掉依赖与 bundle、还原 `cordis.patch.yml`,再跑一次 `pnpm install`,重启生效。
207
+ ```bash
208
+ dsh plugin --profile web add dsh-dashcam # 或:dsh plugin --profile desktop add dsh-dashcam
209
+ ```
210
+
211
+ 装完重启 DeepSeek Harness。**默认只观察、不拦截**:被映射到的工具调用逐条落账,不拒绝任何调用——想要「记录之上再判定」需显式开启(`config.requests` / `config.grants` + `enforce: true`)。
212
+
213
+ **支持的 profile**:`desktop` / `web`。判定链与账本跑在宿主主进程,两种 profile 下都生效;浏览器面板面向 web 客户端(发布包里 `dsh.client.platform: web`)。
214
+
215
+ **装上后应该看到什么。** 启动时宿主日志(`%APPDATA%\@deepseek-ai\dsh-desktop\logs`)出现:
216
+
217
+ ```
218
+ [dsh-dashcam] 判定链已挂载(event=tools/pre-execute,mode=unprofiled,enforceUnmapped=false,commandlineArchive=hash,patchBuiltins=false,ledger=C:\Users\you\.dsh\dsh-dashcam\ledger.jsonl,requests=0,grants=0)
219
+ ```
220
+
221
+ 之后每次被观测的工具调用落进 `~/.dsh/dsh-dashcam/ledger.jsonl`(字段有省略):
222
+
223
+ ```jsonl
224
+ {"v":4,"plugin":"dsh-dashcam-host","pluginVersion":"1.14.0","capability":"fs","action":"fs.read","scope":{"path":"C:/repo/src/app.ts"},"decision":"allow","ruleId":"unprofiled","profiled":false,"manifestHash":null,"seq":1,"ts":"2026-11-09T14:09:14.822Z","prevHash":"GENESIS","hash":"b504c225…"}
225
+ ```
226
+
227
+ 整条链随时可离线复验——不需要宿主进程:
228
+
229
+ ```bash
230
+ npx dsh-dashcam verify-ledger ~/.dsh/dsh-dashcam/ledger.jsonl
231
+ ```
232
+
233
+ ```json
234
+ {
235
+ "ok": true,
236
+ "records": 2,
237
+ "lastSeq": 2,
238
+ "head": "6003e9f193a63198cfbc2039f296029d669d98337cb52c213da5e21238f5300b",
239
+ "macVerified": false,
240
+ "errors": []
241
+ }
242
+ ```
243
+
244
+ **卸载**同样一条命令:`dsh plugin --profile web remove dsh-dashcam`——判定链摘除,无残留 Hook。
245
+
246
+ **从 link 安装升级(2.0 之前)**:把 profile 的 `package.json` 里 `"dsh-dashcam-host": "link:…"` 依赖和 `dsh.profile.bundles` 里的 `dsh-dashcam-host` 条目删掉,再用上面一条命令安装。默认账本路径不变(`~/.dsh/dsh-dashcam/ledger.jsonl`),历史记录与哈希链原样保留——无需手动迁移。
214
247
 
215
248
  适配包默认 `patchBuiltins: false`:宿主是 Electron 主进程,Bridge 是进程级补丁,直接装上会接管
216
249
  应用自身的 fs/net,所以默认只注册 pre-execute 判定函数。需要真实拦截时在 cordis 配置里显式
@@ -296,7 +329,7 @@ const report = bridge.coverage();
296
329
  Bridge 只能做到「我想到的都包了」。想真正做到「没声明就做不到」,得反过来:不给插件全局 API,只给它一个受限对象。
297
330
 
298
331
  ```js
299
- const { createCapabilityHandles } = require('dsh-dashcam');
332
+ const { createCapabilityHandles } = require('dsh-dashcam/core');
300
333
 
301
334
  const handles = createCapabilityHandles({ guard });
302
335
  await handles.fs.promises.readFile('cache.json'); // 过守卫链
@@ -0,0 +1,5 @@
1
+ # dsh-dashcam 的挂载声明:把判定链接进 DSH 配置树。
2
+ # id 必须与包名一致;name 用模块名,解析到 package.json 的 main(dsh-dashcam-host/lib/index.js)。
3
+ - insert:
4
+ - id: dsh-dashcam
5
+ name: 'dsh-dashcam'
package/docs/SPEC.md CHANGED
@@ -268,7 +268,7 @@ CLI 的 `metrics` 现在是「先校验、再统计」:校验不过(含 `mac
268
268
  Bridge 是补丁式的,覆盖永远是 Node API 的真子集。能力句柄走另一条路:**不给插件全局,只给它一个小世界。**
269
269
 
270
270
  ```js
271
- const { createCapabilityHandles } = require('dsh-dashcam');
271
+ const { createCapabilityHandles } = require('dsh-dashcam/core');
272
272
  const handles = createCapabilityHandles({ guard }); // { fs }
273
273
  await handles.fs.promises.readFile('cache.json'); // 每次调用都过守卫链
274
274
  ```
@@ -0,0 +1,177 @@
1
+ # dsh-dashcam-host
2
+
3
+ **English** | [中文](README.zh-CN.md)
4
+
5
+ ![version](https://img.shields.io/badge/version-1.14.0-blue) ![node](https://img.shields.io/badge/node-%3E%3D%2020-green) ![license](https://img.shields.io/badge/license-MIT-lightgrey)
6
+
7
+ Wires the judgment chain of [dsh-dashcam](../README.md) into DeepSeek Harness (the cordos host). This directory merged into the main repository as of 1.10.12 (git subtree, history fully preserved).
8
+
9
+ - Entry point: the DSH core's `tools/pre-execute` waterfall (`(exec, next)` signature; passing through must `return next()`).
10
+ - Defaults to **advisory**: mapped calls each go through `guard.check` for bookkeeping (including deny details) but are allowed through; with `enforce: true`, a failed judgment blocks the call.
11
+ - Scope boundary: the judgment chain is an **entry policy gate** (it judges tool arguments); the real side effects after tool execution are covered by `patchBuiltins: true` (the Bridge, off by default), which guards in-process calls.
12
+
13
+ ## Config keys (cordis plugin config)
14
+
15
+ | Key | Default | Description |
16
+ | --- | --- | --- |
17
+ | `enforce` | `false` | With `true`, a failed judgment blocks the call; authorization see "Two-Key" |
18
+ | `enforceUnmapped` | `false` | F6: enforce by default only covers the mapping table; with this key on, unmapped tools are rejected **on every call** fail-closed (1.10.13 recheck P1: interception is not affected by telemetry dedupe). computer-use / workflow / subagent / MCP are all outside the mapping table and rely on this key as the fallback |
19
+ | `unmappedAllowlist` | `[]` | L6 (1.10.13): the exemption list for enforceUnmapped, supporting exact names and prefix wildcards (`mcp__demo__*` / `memory_*`). **Must be configured before enabling enforceUnmapped** — otherwise the host/MCP tools `compress` / `workflow` / `subagent` / `send_meme` / `present` / `memory_*` / `mcp__*` will all be blocked. A non-array value (e.g. accidentally a comma-separated string) **fails fast and refuses the mount** (R3, 1.10.14) instead of silently treating it as an empty array and blocking everything |
20
+ | `commandlineArchive` | `'hash'` | F4: `'hash'` (default) stores only the SHA-256 + length of the whole command, no plaintext; `'full'` stores masked text (Bearer / `sk-` / `ghp_` / `password=` etc. replaced with `***`, best-effort). Effective per-mount (L2); multiple mounts don't cross-talk |
21
+ | `ledgerPath` | `~/.dsh/dsh-dashcam/ledger.jsonl` | Ledger file path; test profiles must be isolated (e.g. `ledger-dashcam-test.jsonl`) |
22
+ | `ledgerMacKey` / `ledgerMacKeyFile` / `ledgerMacKeyEnv` | empty | F1/1.10.11 D4: ledger HMAC key (>= 32 bytes). **Read the next section before enabling** |
23
+ | `patchBuiltins` | `false` | In-process Bridge patch; enabling it in the Electron main process takes over the app's own fs/net — be careful |
24
+ | `ingress` | `{}` (enabled by default) | 1.12.0: metadata audit of LLM requests / inbound messages. See the section below |
25
+ | `panel` | `{}` (enabled by default) | 1.13.0: browser panel (Agent Dashcam). `{ enabled: false }` disables the `/api/dsh-dashcam/*` routes. See the section below |
26
+ | `requests` / `grants` | `[]` | Two-Key: capability declarations (object arrays; strings fail fast at mount time) |
27
+
28
+ ### ingress audit (1.12.0)
29
+
30
+ Summarizes "the messages sent to the model" and "the inputs that actually entered the session" into the ledger, to answer "did this message come in, or was it fabricated?".
31
+ Metadata only, no message bodies; no judgment, no blocking; recording failures are fail-open.
32
+
33
+ | Sub-key | Default | Description |
34
+ | --- | --- | --- |
35
+ | `enabled` | `true` | When disabled, writes a `telemetry/ingress-disabled` record instead of vanishing silently |
36
+ | `requestEvent` | `llm/stream` | Request egress waterfall (`streamWithRegistration` of `dsh-llm`); the payload is the full request. Verified against the local `resources/app.asar` |
37
+ | `inboundEvent` | `session/event` | Only takes `event.type === 'user/message'`, same source as session logs. Verified against the local asar |
38
+ | `headBytes` | `80` | Keep the first N bytes of each message; `0` = only length and sha256 |
39
+ | `maxSample` | `32` | Maximum number of new-message samples carried per request record |
40
+ | `maxLineBytes` | `8192` | Size cap per record; samples are trimmed first, then coverage |
41
+ | `inboundWindow` | `500` | Inbound matching window kept per session |
42
+ | `inboundTtlMs` | `3600000` | Lifetime of the inbound matching window |
43
+ | `maskSecrets` | `true` | Mask heads (Bearer / `sk-` / `ghp_` / `password=`, best-effort) |
44
+ | `whitelistKinds` | `runtime-context` etc. | `source.kind` values not required to appear in inbound; **by kind, not by role** |
45
+
46
+ Test: "absent in inbound + present in request" = injection; "absent in inbound + absent in request" = model-fabricated (**no alert**, must be guarded by a test case).
47
+ `llm.anomaly` records only; `decision` stays `allow` and `intercepted` stays `false`.
48
+
49
+ Known boundaries: locally we can only see the messages the client sent out. Server-side prefixes appended after the client are not covered;
50
+ `ingress --find` searches only sampled heads — a failed search does not mean it didn't happen.
51
+ The wiring point must be **after the messages are final** (hooking before any trim / rewrite / template expansion will mismatch inbound fingerprints,
52
+ producing `added_user_without_inbound` false positives).
53
+
54
+ ## Browser panel (Agent Dashcam, 1.14.0)
55
+
56
+ A read-only dashboard injected into DSH Web (same origin for desktop and web): a floating **REC badge** in the bottom-right corner
57
+ (elapsed recording time ticks live), draggable to reposition (hold and drag; position is remembered in localStorage), click or `Ctrl+Shift+D`
58
+ to open the popup panel. Three views, all consuming the real ledger:
59
+
60
+ - **Live recording**: metric cards (uptime = since the judgment chain mounted / judged calls / files changed / anomalies),
61
+ live event stream (incremental polling; event rows and the status window both cap at 400 entries — an ever-growing DOM would drag down long-open sessions),
62
+ current step, and judgment-chain status (mode / bypass hits / unmapped calls / version / session / model / interception ratio).
63
+ - **Replay**: a timeline canvas for any recording segment (type ticks + playhead + hover tooltips), play/pause/1×·10×·60×,
64
+ linked event list, event details (path / command / decision / rule). Space toggles playback, ←/→ step through events, Esc closes the panel.
65
+ Segments over 2000 entries are explicitly labeled "truncated".
66
+ - **Recording history**: aggregated into segments by "gap between adjacent records > 15 minutes" (`S-xxxx`), with search + status filter;
67
+ click a segment to enter its replay.
68
+
69
+ Panel semantics (corrected in the 1.14.0 recheck):
70
+
71
+ - **The anomaly card counts only real anomalies**: `intercepted=true` or an ingress anomaly. Under advisory,
72
+ `decision=deny` without interception is a **bypass hit** (guards.js semantics: bypasses still land a deny entry),
73
+ shown separately as gray rows — otherwise advisory mode would keep the red warning permanently lit and it would lose its meaning. The "anomalies" column of recording segments uses the same scope.
74
+ - **Mapped/unmapped counted separately**: `judgments` only covers the five capability categories inside the mapping table;
75
+ MCP / workflow / subagent and other out-of-table tools go through `telemetry/unmapped-tool` and are shown separately as "unmapped calls" —
76
+ otherwise MCP-heavy sessions would show "tool calls" as a constant 0.
77
+ - **The records cursor drops no records**: `lastSeq` takes only the seq of the last record actually returned (previously the max over the whole
78
+ file, so when the first screen was truncated by limit, the cursor jumped straight to the file tail and middle records were lost forever);
79
+ beyond the limit it returns `truncated: true`.
80
+ - **Tail-parse cache**: status/records share a 10s cache keyed on size+mtime (like history),
81
+ so polling no longer re-reads and re-parses a 4MB ledger every 2–3 seconds.
82
+
83
+ Implementation (same approach as dsh-deepseek-tan-pet):
84
+
85
+ - Host half [lib/panel.js](lib/panel.js): three routes `GET /api/dsh-dashcam/status|records|history`,
86
+ loopback Host validation, `no-store`, optional-safe (a missing webServer only loses the panel; the judgment chain and ledger are unaffected).
87
+ - Browser half `lib/client.js` (assembled from `src/client/` by `npm run build`):
88
+ wrapped in `window.__ModuleLoader__.load`; `package.json`'s `dsh.client` declares injection into
89
+ `ui-renderer / ui-slots / shortcuts`; the panel mounts directly on `document.body` (iframes 404 under
90
+ dsh-app://), positioned only with right/bottom; singleton + generation guards against cascades.
91
+ - **Redaction scope**: only coarse-grained facts are exposed (paths / command first word / host / counts / decision / reason);
92
+ the `llm.request` sample (message head samples) and hash-chain fields (hash/prevHash/mac) are not exposed;
93
+ `llm.inbound` exposes only a head truncated to 48 bytes. History clustering reads fully with a 10s cache.
94
+
95
+ After changing the frontend: run `npm run build` to regenerate `lib/client.js`, then restart DSH (the currently running desktop
96
+ process still loads the old bundle).
97
+
98
+ ## Tool mapping table (1.10.13)
99
+
100
+ The public name has its `ops_` prefix stripped first, then matched by bare name (`ops_bash → bash`):
101
+
102
+ | Tool (bare name) | Judgment |
103
+ | --- | --- |
104
+ | `pwsh` / `bash` / `sh` / `zsh` / `cmd` | `proc.spawn` (**first word** scope) + commandline hash archive; `script_path` judged as the whole string |
105
+ | `read` / `read_file` / `read_files` / `read_table` / `read_docx` / `read_pdf` / `read_image` / `inspect_local_file` | `fs.read` per path (credential paths auto-escalate to `cred.read`) |
106
+ | `glob` / `grep` | `fs.read`; when path is a directory, judged as `dir/**` (F3); missing path falls to `unjudgeable` telemetry |
107
+ | `stat` / `diff` | `fs.read`; directories likewise turn into subtree form |
108
+ | `write` / `write_file` / `edit` / `edit_file` / `replace` | `fs.write` (`dryRun: true` downgrades to `fs.read`); replace judged at base directory as subtree form |
109
+ | `copy` | `fs.read`(source) + `fs.write`(destination); when the destination is an existing directory, judged as `to/**` |
110
+ | `move` | `fs.read`(source) + **`fs.delete`(source: for directories, both node + subtree, M1)** + `fs.write`(destination) |
111
+ | `remove` | `fs.delete`; when the target is a directory, **both node + subtree are judged** (M1) |
112
+ | `batch` | expanded per entry of `operations[]` (same scope as copy/move/remove, cap 512); missing operations falls to `unjudgeable` |
113
+ | `web_fetch` / `fetch` | `net.outbound` (host:port) |
114
+ | `discover_tools` / `result` | not mapped, not telemetered (no side effects) |
115
+ | everything else | allowed + `telemetry/unmapped-tool` (the console callout and allow telemetry are recorded once per name); under `enforceUnmapped` **every call** is rejected and **every one lands a deny entry** (R2, 1.10.14: audit does not dedupe); entries hit by `unmappedAllowlist` are exempted and allowed |
116
+
117
+ Unjudgeable calls (batch missing operations, glob missing path, copy/move missing to):
118
+ advisory records `telemetry/unjudgeable-tool` (allow) and passes; enforce lands a deny entry then rejects (F2: audit precedes execution).
119
+
120
+ **M1 (1.10.13) authorization scope for directory deletion**: `remove(directory)` / `move(directory, …)` deletes
121
+ **the node itself + the whole subtree**, judged as two entries — the node (`fs.delete{dir}`, requiring explicit authorization of dir itself)
122
+ and the subtree (`fs.delete{dir/**}`); under enforce both must pass — granting only `dir/**` does not allow it, consistent with the SPEC's
123
+ "`dir/**` does not include dir itself"; to delete a directory node, authorize both `dir` and `dir/**`.
124
+
125
+ ## MAC enablement and ledger migration (F1, guardrails since 1.10.12)
126
+
127
+ **Do not enable MAC in place on an existing v2 ledger.** The production ledger (190 lines) is all v2 records without mac;
128
+ once enabled, `Ledger.verify()` would judge all history as `mac_missing` with `ok=false`, and dashcam's metrics verify
129
+ before computing, exiting non-zero immediately. The host now has a guardrail: with a MAC key configured and the ledger containing
130
+ mac-less history rows → **refuses the mount** and prints the migration steps. A refused mount leaves a side log next to the ledger
131
+ (`<ledgerPath>.guard.log`, L3 — the desktop build cannot see stdout, but you can check afterwards); an unset environment variable
132
+ pointed to by `ledgerMacKeyEnv` reports `stage=mac-env-missing` (L1, distinct message, not masquerading as "can't enable in place").
133
+
134
+ Migration steps (use a new ledger + archive; do not toggle in place):
135
+
136
+ 1. Archive the old ledger: `mv ~/.dsh/dsh-dashcam/ledger.jsonl ~/.dsh/dsh-dashcam/ledger-v2-archive.jsonl`
137
+ (the filename marks v2-without-mac; do not append to it again).
138
+ 2. Keep the `ledgerMacKey` (or `ledgerMacKeyFile` / `ledgerMacKeyEnv`) config and restart DSH —
139
+ the judgment chain starts a v3 ledger on the new file, and the mount record carries a mac.
140
+ 3. If the archive needs auditing, use a keyless `new Ledger(path).verify()`: `ok=false / mac_missing` is the expected result
141
+ (history genuinely has no mac) and does not indicate corruption.
142
+ 4. Key management: a random string >= 32 bytes; never commit it to the repository. Recommended: point `ledgerMacKeyFile`
143
+ at a permission-controlled file, or use `ledgerMacKeyEnv: 'DSH_DASHCAM_MAC_KEY'`.
144
+
145
+ Archive hash note: the commandline archive's `commandLineHash` is the SHA-256 of the **original text** (not the masked text),
146
+ used for offline reconciliation to verify "the hash on the ledger == the command actually executed"; it contains no plaintext.
147
+
148
+ ## Known boundaries
149
+
150
+ - **F5 (semantics, closed)**: telemetry-side records no longer count toward the metrics judgment scope — since core
151
+ dsh-dashcam 1.10.12, `computeMetrics` / `monthlyMetrics` only accept records with
152
+ `capability !== 'telemetry'`; telemetry counts are reported side by side as `telemetryRecords` /
153
+ `telemetryDenies` (see core docs/ROLLOUT.md section 6). This host's side records
154
+ (mount / unmapped-tool / unjudgeable-tool / commandline-audit) have their capability
155
+ fixed at `telemetry` — do not change it.
156
+ - Command judgment scope = first word; side effects nested inside `bash -c "…"` are not covered by the entry gate.
157
+ - `replace` audits at base-directory granularity; the actual files matched by the glob are not expanded individually.
158
+ - `dir/**` grants not including `dir` itself is the SPEC contract (docs/SPEC.md 77); the host maps **read-only / subtree-write**
159
+ semantics to `dir/**` for alignment, while **destructive deletion keeps the node judgment** (M1).
160
+ - **L4 (TOCTOU)**: directory probes cache **positive results** with a 30s TTL and **do not cache negative results** (R1, 1.10.14:
161
+ a negative cache would let a deletion within the 30s "file→directory" window skip the subtree judgment and bypass M1 — fail-open, now fixed;
162
+ positive caching is the reverse — a directory swapped to a file still gets an extra subtree judgment, which is fail-closed and acceptable).
163
+ A race window between judgment and tool execution remains; the entry gate does not promise atomicity — real interception relies on the Bridge.
164
+ - With `patchBuiltins: true` stacked on the host entry gate, directory probes produce extra fs records; pick one or the other.
165
+
166
+ ## Verification
167
+
168
+ ```powershell
169
+ npm run smoke # 96 + 15 + 43 assertions, all passing (including panel routes/redaction/clustering/cursor semantics)
170
+ npm run build # regenerate lib/client.js (required after changing src/client)
171
+ # Main repo regression: cd ..; npm test # 241 tests, 0 fail
172
+ ```
173
+
174
+ Deployment confirmation (F8): after restarting the DSH desktop build, new mount records in the ledger should show
175
+ `pluginVersion: "1.14.0"` with `scope.mode` matching the config, the REC badge in the bottom-right corner,
176
+ and `Ctrl+Shift+D` opens the panel; **on the deployment side, stay in advisory observation for now —
177
+ don't rush to enable enforce / enforceUnmapped** (configure unmappedAllowlist per the table above before enabling enforceUnmapped).
@@ -0,0 +1,181 @@
1
+ # dsh-dashcam-host
2
+
3
+ [English](README.md) | **中文**
4
+
5
+ ![version](https://img.shields.io/badge/version-1.14.0-blue) ![node](https://img.shields.io/badge/node-%3E%3D%2020-green) ![license](https://img.shields.io/badge/license-MIT-lightgrey)
6
+
7
+ 把 [dsh-dashcam](../README.zh-CN.md) 的判定链接入 DeepSeek Harness(cordos 宿主)。本目录自 1.10.12 起并入主仓库(git subtree,历史完整保留)。
8
+
9
+ - 入口:DSH 核心的 `tools/pre-execute` waterfall(`(exec, next)` 签名;放行必须 `return next()`)。
10
+ - 默认 **advisory**:映射到的调用逐条走 `guard.check` 记账(含 deny 明细)但放行;
11
+ `enforce: true` 后判定不允许即拦截。
12
+ - 定位边界:判定链是**入口策略门**(拦工具参数),工具执行后的真实副作用由
13
+ `patchBuiltins: true`(Bridge,默认关闭)看护进程内调用。
14
+
15
+ ## 配置键(cordis 插件 config)
16
+
17
+ | 键 | 默认 | 说明 |
18
+ | --- | --- | --- |
19
+ | `enforce` | `false` | `true` 时判定不允许即拦截;授权见「Two-Key」 |
20
+ | `enforceUnmapped` | `false` | F6:enforce 默认只覆盖映射表;开此键后未映射工具**每次调用**都按 fail-closed 拒绝(1.10.13 复核 P1:拦截不受遥测去重影响)。computer-use / workflow / subagent / MCP 均在映射表外,需靠它兜底 |
21
+ | `unmappedAllowlist` | `[]` | L6(1.10.13):enforceUnmapped 的豁免清单,支持全名与前缀通配(`mcp__demo__*` / `memory_*`)。**开 enforceUnmapped 前必须先配**——否则 `compress` / `workflow` / `subagent` / `send_meme` / `present` / `memory_*` / `mcp__*` 这些宿主/MCP 工具会被全部打死。**非数组(如误写成逗号分隔字符串)fail-fast 拒绝挂载**(R3,1.10.14),不会静默按空数组拦掉全部 |
22
+ | `commandlineArchive` | `'hash'` | F4:`'hash'`(默认)只存整条命令的 SHA-256 + 长度,不含明文;`'full'` 落打码后文本(Bearer / `sk-` / `ghp_` / `password=` 等替换为 `***`,best-effort)。per-挂载生效(L2),多挂载不互串 |
23
+ | `ledgerPath` | `~/.dsh/dsh-dashcam/ledger.jsonl` | 账本文件路径;测试 profile 必须独立(如 `ledger-dashcam-test.jsonl`) |
24
+ | `ledgerMacKey` / `ledgerMacKeyFile` / `ledgerMacKeyEnv` | 空 | F1/1.10.11 D4:账本 HMAC 密钥(≥32 字节)。**启用前必读下节** |
25
+ | `patchBuiltins` | `false` | 进程内 Bridge 补丁;Electron 主进程开启会接管应用自身 fs/net,慎开 |
26
+ | `ingress` | `{}`(默认启用) | 1.12.0:LLM 请求/入站消息的元数据审计。见下节 |
27
+ | `panel` | `{}`(默认启用) | 1.13.0:浏览器面板(Agent Dashcam)。`{ enabled: false }` 关闭 `/api/dsh-dashcam/*` 路由。见下节 |
28
+ | `requests` / `grants` | `[]` | Two-Key:能力声明(对象数组,写成字符串会在 mount 时 fail-fast) |
29
+
30
+ ### ingress 审计(1.12.0)
31
+
32
+ 把「发往模型的 messages」与「进入会话的输入」摘要进账本,用于回答「这条消息是进来的还是编出来的」。
33
+ 只记元数据,不落正文;不判定、不拦截;记录失败 fail-open。
34
+
35
+ | 子键 | 默认 | 说明 |
36
+ | --- | --- | --- |
37
+ | `enabled` | `true` | 关闭时写一条 `telemetry/ingress-disabled`,不静默消失 |
38
+ | `requestEvent` | `llm/stream` | 请求出口 waterfall(`dsh-llm` 的 `streamWithRegistration`),载荷即完整请求。已按本机 `resources/app.asar` 核对 |
39
+ | `inboundEvent` | `session/event` | 只取 `event.type === 'user/message'`,与会话日志同源。已按本机 asar 核对 |
40
+ | `headBytes` | `80` | 每条消息保留的前 N 字;`0` = 只存长度与 sha256 |
41
+ | `maxSample` | `32` | 单条请求记录最多带多少条新增消息样本 |
42
+ | `maxLineBytes` | `8192` | 单条记录体积上限,超出先砍样本再砍 coverage |
43
+ | `inboundWindow` | `500` | 每 session 保留的 inbound 匹配窗口 |
44
+ | `inboundTtlMs` | `3600000` | inbound 匹配窗口的存活时间 |
45
+ | `maskSecrets` | `true` | head 打码(Bearer / `sk-` / `ghp_` / `password=`,best-effort) |
46
+ | `whitelistKinds` | `runtime-context` 等 | 不要求出现在 inbound 的 `source.kind`;**按 kind 不按 role** |
47
+
48
+ 判据:`inbound 无 + request 有` = 注入;`inbound 无 + request 无` = 模型自造(**不告警**,必须有用例守住)。
49
+ `llm.anomaly` 只记录,`decision` 仍是 `allow`、`intercepted` 仍是 `false`。
50
+
51
+ 已知边界:本地只能看到客户端发出去的 messages。服务端在客户端之后追加的前缀不在覆盖范围;
52
+ `ingress --find` 只搜已采样的 head,搜不到不等于没发生。
53
+ 接线位置必须在 messages **定型之后**(任何 trim / 重写 / 模板展开之前会与 inbound 指纹对不上,
54
+ 产生 `added_user_without_inbound` 误报)。
55
+
56
+ ## 浏览器面板(Agent Dashcam,1.14.0)
57
+
58
+ 注入 DSH Web(桌面与网页同源)的只读仪表盘:右下角悬浮 **REC 徽章**(已录时长走秒),
59
+ 可拖拽换位置(按住拖动即可,位置记忆在 localStorage),点击或 `Ctrl+Shift+D` 打开弹窗面板。
60
+ 三个视图,全部消费真实账本:
61
+
62
+ - **实时录制**:指标卡(运行时长 = 判定链挂载至今 / 纳管调用 / 改动文件 / 异常)、
63
+ 实时事件流(增量轮询,事件行与状态窗口都封顶 400 条,DOM 只增不减会拖垮长开会话)、
64
+ 当前步骤、判定链状态(模式 / 旁路命中 / 未纳管调用 / 版本 / 会话 / 模型 / 拦截占比)。
65
+ - **回放**:任一录像段时间轴画布(类型刻度 + 播放头 + 悬浮提示)、播放/暂停/1×·10×·60×、
66
+ 事件列表联动、事件详情(路径 / 命令 / 决策 / 规则)。空格播放暂停、←/→ 逐事件、Esc 关面板。
67
+ 超过 2000 条的录像段明确标注「已截断」。
68
+ - **录像历史**:按「相邻记录间隔 > 15 分钟」聚合成段(`S-xxxx`),搜索 + 状态过滤,
69
+ 点击进入该段回放。
70
+
71
+ 面板口径(1.14.0 复核修正):
72
+
73
+ - **异常卡片只算真异常**:`intercepted=true` 或 ingress anomaly。advisory 下
74
+ `decision=deny` 但未拦截的是**旁路命中**(guards.js 口径:旁路照样落 deny 账),
75
+ 单独以灰色行展示——否则旁路模式下红色常亮,失去警示作用。录像段「异常」列同口径。
76
+ - **纳管/未纳管分开计数**:`judgments` 只覆盖映射表内五类 capability;
77
+ MCP / workflow / subagent 等映射表外工具走 `telemetry/unmapped-tool`,
78
+ 计入「未纳管调用」单独展示——否则 MCP 重的会话「工具调用」恒为 0。
79
+ - **records 游标不丢记录**:`lastSeq` 只取实际返回的最后一条 seq(此前对整个文件取
80
+ max,首屏 limit 截断时游标直接跳到文件尾部,中间记录永久丢失);
81
+ 超出 limit 回 `truncated: true`。
82
+ - **尾部解析缓存**:status/records 共用 size+mtime 键的 10s 缓存(同 history),
83
+ 轮询不再每 2–3 秒重读重解析 4MB 账本。
84
+
85
+ 实现(与 dsh-deepseek-tan-pet 同款姿势):
86
+
87
+ - 宿主半侧 [lib/panel.js](lib/panel.js):`GET /api/dsh-dashcam/status|records|history`
88
+ 三条路由,环回 Host 校验、`no-store`、optional-safe(webServer 缺席只丢面板,
89
+ 判定链与账本不受影响)。
90
+ - 浏览器半侧 `lib/client.js`(由 `npm run build` 从 `src/client/` 拼装):
91
+ `window.__ModuleLoader__.load` 包,`package.json` 的 `dsh.client` 声明注入
92
+ `ui-renderer / ui-slots / shortcuts`;面板直挂 `document.body`(dsh-app:// 下
93
+ iframe 会 404),只用 right/bottom 定位;单例 + generation 防级联。
94
+ - **脱敏口径**:只透出粗粒度事实(路径 / 命令首词 / 主机 / 计数 / 决策 / reason);
95
+ `llm.request` 的 sample(消息 head 样本)与哈希链字段(hash/prevHash/mac)不透出;
96
+ `llm.inbound` 只透出截断 48 字的 head。历史聚类全量读带 10s 缓存。
97
+
98
+ 改前端后:`npm run build` 重新生成 `lib/client.js`,重启 DSH 生效(桌面版当前
99
+ 运行的进程仍加载旧包)。
100
+
101
+ ## 工具映射表(1.10.13)
102
+
103
+ 公开名先剥 `ops_` 前缀再按裸名匹配(`ops_bash → bash`):
104
+
105
+ | 工具(裸名) | 判定 |
106
+ | --- | --- |
107
+ | `pwsh` / `bash` / `sh` / `zsh` / `cmd` | `proc.spawn`(**首词**口径)+ 命令行哈希存档;`script_path` 整串 |
108
+ | `read` / `read_file` / `read_files` / `read_table` / `read_docx` / `read_pdf` / `read_image` / `inspect_local_file` | `fs.read` 逐 path(凭证路径自动升级 `cred.read`) |
109
+ | `glob` / `grep` | `fs.read`;path 是目录时判 `dir/**`(F3);缺 path 落 `unjudgeable` 遥测 |
110
+ | `stat` / `diff` | `fs.read`;目录同上转子树形态 |
111
+ | `write` / `write_file` / `edit` / `edit_file` / `replace` | `fs.write`(`dryRun: true` 降级 `fs.read`);replace 基目录转子树形态 |
112
+ | `copy` | `fs.read`(源) + `fs.write`(目标);目标为已存在目录时判 `to/**` |
113
+ | `move` | `fs.read`(源) + **`fs.delete`(源:目录时节点+子树两条,M1)** + `fs.write`(目标) |
114
+ | `remove` | `fs.delete`;目标是目录时 **节点 + 子树两条都判**(M1) |
115
+ | `batch` | `operations[]` 逐条展开(同 copy/move/remove 口径,上限 512);缺 operations 落 `unjudgeable` |
116
+ | `web_fetch` / `fetch` | `net.outbound`(host:port) |
117
+ | `discover_tools` / `result` | 不映射不遥测(无副作用) |
118
+ | 其余 | 放行 + `telemetry/unmapped-tool`(console 点名与 allow 遥测同名只记一次);`enforceUnmapped` 下**每次调用**拒绝且**每次都落 deny 账**(R2,1.10.14:审计不去重),`unmappedAllowlist` 命中的豁免放行 |
119
+
120
+ 无法判定的调用(batch 缺 operations、glob 缺 path、copy/move 缺 to):
121
+ advisory 落 `telemetry/unjudgeable-tool`(allow)放行;enforce 落 deny 账后拒绝(F2:审计先于执行)。
122
+
123
+ **M1(1.10.13)目录删除的授权口径**:`remove(目录)` / `move(目录, …)` 删的是
124
+ **节点本身 + 整棵子树**,按节点(`fs.delete{dir}`,需显式授权 dir 本身)与子树
125
+ (`fs.delete{dir/**}`)两条判定,enforce 下都要过——仅授 `dir/**` 不放行,
126
+ 与 SPEC「`dir/**` 不含 dir 本身」一致;删除目录节点请同时授权 `dir` 与 `dir/**`。
127
+
128
+ ## MAC 启用与账本迁移(F1,1.10.12 起有防呆)
129
+
130
+ **不要在既有 v2 账本上原地开 MAC。** 生产账本(190 行)全是 v2 无 mac 记录,
131
+ 开启后 `Ledger.verify()` 会把全部历史判成 `mac_missing`、`ok=false`,
132
+ 而 dashcam 的 metrics 先校验后统计,直接非零退出。宿主已加防呆:
133
+ 配置了 MAC 密钥且账本存在无 mac 历史行 → **拒绝挂载**并打印迁移步骤。
134
+ 拒绝挂载会在账本旁留 side 日志 `<ledgerPath>.guard.log`(L3,桌面版看不到
135
+ stdout 也能事后查);`ledgerMacKeyEnv` 指向的环境变量未设置时报
136
+ `stage=mac-env-missing`(L1,独立文案,不冒充「不能原地启用」)。
137
+
138
+ 迁移步骤(换新账本 + 归档,不要原地开关):
139
+
140
+ 1. 归档旧账:`mv ~/.dsh/dsh-dashcam/ledger.jsonl ~/.dsh/dsh-dashcam/ledger-v2-archive.jsonl`
141
+ (文件名标注 v2-无-mac,勿再追加)。
142
+ 2. 保持 `ledgerMacKey`(或 `ledgerMacKeyFile` / `ledgerMacKeyEnv`)配置,重启 DSH——
143
+ 判定链会在新文件上从 v3 起账,mount 记录即带 mac。
144
+ 3. 归档文件如需核对,用无密钥 `new Ledger(path).verify()`:`ok=false / mac_missing`
145
+ 是预期结果(历史本就无 mac),不代表账本损坏。
146
+ 4. 密钥管理:≥32 字节随机串;不进仓库。推荐 `ledgerMacKeyFile` 指向权限受控的文件,
147
+ 或 `ledgerMacKeyEnv: 'DSH_DASHCAM_MAC_KEY'`。
148
+
149
+ 存档哈希说明:命令行存档的 `commandLineHash` 是**原文**的 SHA-256(非打码后文本),
150
+ 用于离线对账时验证「账上哈希 == 实际执行过的命令」,不含明文。
151
+
152
+ ## 已知边界
153
+
154
+ - **F5(口径,已闭合)**:telemetry 附加记录不再计入 metrics 的判定口径——核心
155
+ dsh-dashcam 1.10.12 起 `computeMetrics` / `monthlyMetrics` 只收
156
+ `capability !== 'telemetry'` 的记录,telemetry 数量另列 `telemetryRecords` /
157
+ `telemetryDenies` 并列读数(见核心 docs/ROLLOUT.md 第 6 节)。本宿主附加记录
158
+ (mount / unmapped-tool / unjudgeable-tool / commandline-audit)的 capability
159
+ 固定为 `telemetry`,勿改。
160
+ - 命令判定口径 = 首词;`bash -c "…"` 套娃内部副作用不在入口门覆盖范围。
161
+ - `replace` 以基目录为审计粒度,glob 命中的实际文件不逐个展开。
162
+ - `dir/**` 授权不含 `dir` 本身是 SPEC 契约(docs/SPEC.md 77);宿主对**只读/子树写**
163
+ 语义转 `dir/**` 对齐,**破坏性删除保持节点判定**(M1)。
164
+ - **L4(TOCTOU)**:目录探测**正结果**带 30s TTL 缓存、**负结果不缓存**(R1,1.10.14:
165
+ 负缓存会让「文件→目录」30s 窗口内的删除跳过子树判定绕过 M1,属 fail-open,已修;
166
+ 正缓存方向相反——目录被换成文件后仍多判一条子树,是 fail-closed,可接受)。
167
+ 判定与工具执行之间仍存在竞态窗口,入口门不承诺原子性,真拦截靠 Bridge。
168
+ - `patchBuiltins: true` 与宿主入口门叠加时,目录探测会产生额外 fs 记录;建议二选一。
169
+
170
+ ## 验证
171
+
172
+ ```powershell
173
+ npm run smoke # 96 + 15 + 43 项断言,全过(含 panel 路由/脱敏/聚类/游标口径)
174
+ npm run build # 重新生成 lib/client.js(改 src/client 后必须跑)
175
+ # 主仓库回归:cd ..; npm test # 241 例,0 fail
176
+ ```
177
+
178
+ 部署确认(F8):重启 DSH 桌面后,账本新 mount 记录应为
179
+ `pluginVersion: "1.14.0"`、`scope.mode` 与配置一致,右下角出现 REC 徽章、
180
+ `Ctrl+Shift+D` 可开面板;**部署侧暂保持 advisory 观察,
181
+ 勿急着开 enforce / enforceUnmapped**(开 enforceUnmapped 前先按上表配 unmappedAllowlist)。