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 +47 -10
- package/README.zh-CN.md +47 -14
- package/cordis.patch.yml +5 -0
- package/docs/SPEC.md +1 -1
- package/dsh-dashcam-host/README.md +177 -0
- package/dsh-dashcam-host/README.zh-CN.md +181 -0
- package/dsh-dashcam-host/lib/client.js +1206 -0
- package/dsh-dashcam-host/lib/index.js +971 -0
- package/dsh-dashcam-host/lib/ingress.js +218 -0
- package/dsh-dashcam-host/lib/panel.js +487 -0
- package/dsh-dashcam-host/package.json +29 -0
- package/package.json +27 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
     
|
|
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 —
|
|
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
|
-
##
|
|
189
|
+
## Install (DSH host)
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
One command installs the complete plugin — judgment chain, ledger, and the Agent Dashcam browser panel:
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
     
|
|
6
6
|
|
|
7
7
|
**dsh-dashcam 是给 AI 插件装的行车记录仪——它记录插件实际做了什么,不替你决定插件能做什么。**
|
|
8
8
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
**不放心就别装。** 这是主要建议,不是免责声明:账本本身就是高价值目标(它精确知道你碰过哪些文件、什么时候碰的),宿主适配包跑在 agent 主进程里,而「它只记录」恰恰意味着它什么都看得见。校验 clone 来源,别把账本放进你不信任的备份,在意防篡改就配 HMAC 密钥(`ledgerMacKey`)——或者干脆不装;行车记录仪最不怕的就是这种怀疑。
|
|
37
37
|
|
|
38
|
-
>
|
|
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
|
-
##
|
|
203
|
+
## 安装(DSH 宿主)
|
|
203
204
|
|
|
204
|
-
|
|
205
|
-
`dsh.profile.bundles` 决定启用谁;bundle 自带的 `cordis.patch.yml` 负责把插件插进宿主配置树。
|
|
206
|
-
按这个机制把判定链挂进去只需四步:
|
|
205
|
+
一条命令装出完整插件——判定链、账本、Agent Dashcam 浏览器面板:
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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'); // 过守卫链
|
package/cordis.patch.yml
ADDED
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
|
+
  
|
|
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
|
+
  
|
|
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)。
|