dsh-jev-guard 0.5.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/CHANGELOG.zh-CN.md +271 -0
  3. package/DEPLOY.md +202 -0
  4. package/DEPLOY.zh-CN.md +200 -0
  5. package/LICENSE +21 -0
  6. package/README.md +316 -0
  7. package/README.zh-CN.md +315 -0
  8. package/START-HERE.md +97 -0
  9. package/START-HERE.zh-CN.md +97 -0
  10. package/adapters/README.md +37 -0
  11. package/adapters/README.zh-CN.md +37 -0
  12. package/adapters/dsh/index.js +502 -0
  13. package/bin/guard.mjs +634 -0
  14. package/config.example.json +52 -0
  15. package/cordis.patch.yml +120 -0
  16. package/docs/AGENT-TASK-dsh.md +134 -0
  17. package/docs/AGENT-TASK-dsh.zh-CN.md +131 -0
  18. package/docs/ARCHITECTURE.md +118 -0
  19. package/docs/ARCHITECTURE.zh-CN.md +117 -0
  20. package/docs/DECISIONS.md +469 -0
  21. package/docs/DECISIONS.zh-CN.md +449 -0
  22. package/docs/DSH-INTEGRATION.md +178 -0
  23. package/docs/DSH-INTEGRATION.zh-CN.md +171 -0
  24. package/docs/MEASUREMENTS.md +433 -0
  25. package/docs/MEASUREMENTS.zh-CN.md +450 -0
  26. package/docs/USER-INTERVENTION.md +141 -0
  27. package/docs/USER-INTERVENTION.zh-CN.md +143 -0
  28. package/docs/VERIFICATION.md +279 -0
  29. package/docs/VERIFICATION.zh-CN.md +278 -0
  30. package/lib/audit.js +228 -0
  31. package/lib/gate.js +720 -0
  32. package/lib/i18n.js +575 -0
  33. package/lib/quota.js +389 -0
  34. package/lib/rules.js +174 -0
  35. package/lib/token.js +154 -0
  36. package/lib/verdict.js +285 -0
  37. package/package.json +82 -0
  38. package/tools/check-doc-pairs.mjs +158 -0
  39. package/tools/extract-commands.mjs +156 -0
  40. package/tools/gate-cli.mjs +240 -0
  41. package/tools/probe-prompt-lang.mjs +238 -0
  42. package/tools/probe-scripts.mjs +143 -0
  43. package/tools/report-result.mjs +146 -0
  44. package/tools/selftest-audit.mjs +93 -0
  45. package/tools/selftest-entry.mjs +177 -0
  46. package/tools/selftest-i18n.mjs +177 -0
  47. package/tools/selftest-quota.mjs +260 -0
  48. package/tools/selftest-reason.mjs +266 -0
  49. package/tools/selftest-rules.mjs +107 -0
  50. package/tools/selftest-token.mjs +100 -0
  51. package/tools/smoke-dsh-adapter.mjs +295 -0
  52. package/tools/smoke-dsh-pipeline.mjs +146 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,285 @@
1
+ # Changelog
2
+
3
+ > **English** | [简体中文](CHANGELOG.zh-CN.md)
4
+
5
+ This project follows a "record the facts by date" approach: every entry states clearly **what changed, why, and how it was verified**.
6
+ The complete design trade-offs are in [`docs/DECISIONS.md`](./docs/DECISIONS.md), the measured data in [`docs/MEASUREMENTS.md`](./docs/MEASUREMENTS.md).
7
+
8
+ ## [0.5.1] — 2026-09-22
9
+
10
+ **Published to npm, and the host-version declaration is withdrawn.** What changes here is how the plugin is installed and what it declares — not what it does. `bin/`, `lib/`, `adapters/`, `cordis.patch.yml` and the default config are unchanged from 0.5.0.
11
+
12
+ **Install from the registry.** The package is published as `dsh-jev-guard`, which is the source the plugin market installs from by preference (a repo-verified npm package, then an author-supplied prebuilt GitHub Release tarball, then a full-repo source download). For anyone on a slow or unreliable route to GitHub that is the difference between seconds and a clone:
13
+
14
+ ```bash
15
+ dsh plugin --profile web add dsh-jev-guard
16
+ ```
17
+
18
+ `private` is removed, and `publishConfig` pins the registry to `https://registry.npmjs.org/` so a mirror configured in `.npmrc` cannot silently redirect a publish.
19
+
20
+ **No DSH version is declared.** `engines` now carries only `node`. A floor was briefly added after 0.5.0 (`engines.dsh: "0.1.6-alpha.2"`) but never shipped in a tagged release, and it is withdrawn here. The plugin market reads that field from the npm manifest, and an exact version in it makes the market report "confirmed incompatible" and block install and update on every other DSH release — including whatever later version you move to yourself. With the field absent the market reports "no host requirement declared" and never blocks.
21
+
22
+ **The tested version is still the documented one.** DSH 0.1.6-alpha.2 remains the only release this plugin has been run against. Another version is untested, not forbidden — re-run the self-check suite if you use one.
23
+
24
+ **Acceptance**: `node bin/guard.mjs selftest` (12/12) and the seven offline suites (`tools/selftest-*.mjs`) pass, and `npm publish --dry-run` reports 52 files with no configuration, key material, audit log or handover notes in the tarball.
25
+
26
+ ## [0.5.0] — 2026-09-20
27
+
28
+ **A first-time deployment now has a real place to put its key, and "no key" is no longer silent: it degrades like exhausted credit — loudly, stickily, and without stopping the free layer.**
29
+ Trade-offs in [`DECISIONS.md`](./DECISIONS.md) **D15**; the mechanism in [`docs/DSH-INTEGRATION.md`](./docs/DSH-INTEGRATION.md).
30
+
31
+ **The key entry point: `guard key set` / `guard key status`.** The key is read from **stdin only** — never
32
+ from a command-line argument, which would land in your shell history and in `ps`. It writes the file named
33
+ by `apiKeyFile` (default `secrets.json` in the package root) with mode `0600`, keeps any other keys already
34
+ in that file, and prints the length and the path — never the value. `guard key status` reports which source
35
+ resolves and how long the key is, still never echoing it, and exits 3 when there is none, so it doubles as a
36
+ health check. Reading and writing share one path resolver, so a relative `apiKeyFile` can never mean two
37
+ different files.
38
+
39
+ **Why the DSH adapter had to change too.** It resolved the key from `ctx.credentials` and the environment
40
+ **only**, while `guard key set` writes a file. Without a third source, "record your key with the CLI" would
41
+ have been an empty promise for exactly the users who have no credential layer yet — a fresh install. The
42
+ adapter now falls back to `apiKeyFile` as well, under the same rule: a relative path resolves against the
43
+ package root, independent of cwd.
44
+
45
+ **`no-key` is now a degrading kind — sticky, and scoped.** It was deliberately non-degrading before
46
+ (D10.2), for a good reason: a local configuration problem writing into the machine-wide `degraded.json`
47
+ could stop other entries whose key was perfectly fine. That objection is answered by **scope** rather than
48
+ by staying silent. Service-side kinds (`quota` / `auth`) stay `scope: 'global'` and suppress every entry;
49
+ `no-key` is `scope: 'local'` and suppresses only the entry that wrote it (`'cli'` or `'dsh-adapter'`). And
50
+ because a missing key makes **no HTTP request at all**, there is nothing to probe — so the state is
51
+ **sticky**: it does not expire with time, it ends the moment a key resolves (cleared on the spot, zero
52
+ requests, no restart). `guard status` says that explicitly instead of printing a countdown that would never
53
+ matter.
54
+
55
+ **The user actually gets told.** A host-only plugin has no toast, no banner and no startup notice — every
56
+ settings and Plugins surface in DSH is claimed by browser-side (`dsh.client`) registrations. The one channel
57
+ that exists is injecting a `notice` message at `agent/pre-step`: it renders as a row in the conversation, is
58
+ written into the session history, and enters the model's context (so the model learns the valve is degraded
59
+ too). The plugin uses it for three transitions — first run with no key (the demand, carrying the exact
60
+ command), entering a degraded state, and recovering — one notice per state per session, deduplicated from
61
+ the durable history so a restart or a resume does not repeat it. `notifyInSession: false` turns it off.
62
+
63
+ **The message shape is a contract, and it is checked.** `source` carries exactly
64
+ `kind` / `plugin` / `form` / `summary`, and the summary is bounded to 120 characters (it becomes the
65
+ collapsed row's title). Getting this wrong surfaces as `SessionPersistenceCorruptionError` at the *next
66
+ resume* — a session that will not open, far away from the change that caused it. So the shape is validated
67
+ against DSH's own `snapshotJsonValue` (the step `Session.append` runs first) by a test that runs inside a
68
+ DSH checkout, and the four-key source plus the summary bound are asserted offline in the smoke test. That
69
+ validation was run for this version and passed.
70
+
71
+ **Also in this version:** `package.json` no longer declares `dsh.runtime` — it is not a field of DSH's
72
+ plugin manifest (`manifestVersion` / `bundle` / `profile` / `client` are), so it did nothing while looking
73
+ meaningful to anyone reading the file. `tools/smoke-dsh-adapter.mjs` became hermetic: it used to write
74
+ degradation state and audit records into the **real** `~/.jev-guard/`, and every `apply()` in it now pins
75
+ `logPath` / `degradedPath` / `apiKeyFile` to a temp directory; it also gained 9 assertions covering the
76
+ sticky state, the notice injection (appended rather than replacing; nothing added to an empty batch) and the
77
+ file fallback. `tools/selftest-quota.mjs` went from 50 to 78 cases, `tools/selftest-entry.mjs` to 30 (it now
78
+ runs `guard key set` for real, including the interactive path through a fake TTY) and
79
+ `tools/selftest-i18n.mjs` to 34. Every user-facing string added here exists in both languages.
80
+
81
+ **Not covered here:** `tools/smoke-dsh-pipeline.mjs` needs a DSH workspace whose bare `@deepseek-ai/*`
82
+ specifiers resolve; that does not hold in this deployment, so it could not be executed (it fails the same
83
+ way at the previous commit, so this is not a regression). The notice shape was verified directly against
84
+ DSH's `snapshotJsonValue` instead.
85
+
86
+ ## [0.4.1] — 2026-09-20
87
+
88
+ **Every document a human reads is now English by default, with the Chinese kept as `*.zh-CN.md`.**
89
+ The convention is written up as point 5 of **D14** in [`DECISIONS.md`](./DECISIONS.md).
90
+
91
+ **What moved.** Twelve documents were split: `CHANGELOG.md`, `DEPLOY.md`, `START-HERE.md`,
92
+ `adapters/README.md`, `verification-results/README.md` and the seven under `docs/`. Each Chinese
93
+ original is preserved byte-for-byte as `<name>.zh-CN.md` — the only difference is one language line
94
+ under the title — and both files link to each other, the way `README.md` / `README.zh-CN.md` already did.
95
+ `verification-results/SUMMARY.md` is generated, so the generator was taught the language switch and
96
+ its default became English (with the evidence column left as a verbatim Chinese quote — a translated
97
+ quote would be a forged one).
98
+
99
+ **How the split was verified (mechanical, not by trust).** `tools/check-doc-pairs.mjs` (added here, so
100
+ future edits can keep the pair in step) compares every pair programme-side:
101
+ the Chinese file must equal the original at `HEAD` byte-for-byte plus that single line, and the two
102
+ files must agree on heading-level sequence, code-fence count, table-row count, link-target set and
103
+ numeric multiset. Quoted measurements, log lines, command samples and Chinese corpus entries were left
104
+ verbatim, so the only Chinese left in an English document is quoted evidence — that residue was
105
+ enumerated and reviewed, file by file.
106
+
107
+ **Also in this version:** `tools/report-result.mjs` lost its hard-coded Chinese labels (they now live
108
+ in `lib/i18n.js`) and its item index gained item 23; code comments and the labels inside `tools/` are
109
+ still not translated (D14 point 4) — the line is "read outside the repository → bilingual, read only by
110
+ a maintainer → Chinese".
111
+
112
+ ## [0.4.0] — 2026-09-20
113
+
114
+ **Bilingual Chinese/English: the copy written for people exists in both languages, README defaults to English**; the judging question is still the calibrated Chinese by default.
115
+ Trade-offs in [`docs/DECISIONS.md`](./DECISIONS.md) **D14**, measurements in [`docs/MEASUREMENTS.md`](./MEASUREMENTS.md) **§14**.
116
+
117
+ **Motivation (user request):** change the repository's default README to English, redirect the Chinese introduction the common way; the source code should also support Chinese and English.
118
+
119
+ **Bilingual copy.** New `lib/i18n.js`: `lang` controls the interface language, default `'auto'`
120
+ (`JEV_GUARD_LANG` → `LC_ALL`/`LC_MESSAGES`/`LANG`, **only when they point at a supported language** → otherwise `zh-CN`),
121
+ the CLI has `--lang zh-CN|en` as well. Coverage: judging reasons (the four-state headers, the three degradation templates, the token authorisation line),
122
+ the reasons of the 37 L0 rules, all CLI output, degradation warnings and the `guard status` report, the state keys sent to the judging service.
123
+ **Code comments and the self-check labels in `tools/` are not translated** — they are read by maintainers, and translating them would only double the maintenance cost of every change.
124
+
125
+ **`auto` deliberately does not look at the system locale — this one was stepped on right there during the first real deployment.** At first `Intl` was also at the tail of the detection chain, and the result: the DSH plugin runs inside WSL, where `LANG=C.UTF-8` means "no preference", so `Intl` reported Node's own `en-US` fallback value — the interception reasons in the session **quietly became English**, while the CLI on the Windows side of the same machine (whose Node reports `zh-CN`) was still Chinese, one valve in two languages. Now `C`/`POSIX`/unset are all treated as **no signal** and fall back to the project's primary language
126
+ (`zh-CN`); if you want English, say so explicitly.
127
+
128
+ **One instrumentation bug in the language switch's precedence was also fixed:** these two items were originally written in the in-package `cordis.patch.yml`, and patch takes precedence over `config.json` — meaning the language a user set in `config.json` would be silently overridden. Now they are changed to comment form
129
+ (the default values are still written in the comments), and the language is left to `config.json` / `JEV_GUARD_LANG` / `--lang`.
130
+
131
+ **The judging question is decoupled from the interface language (this one is the core of this release).** `promptLang` separately controls the question sent to Jev and the state keys, and **defaults to `'zh-CN'` and does not follow the interface language**. The reason is not conservatism but measurement: 21 probes × 3 repeats per arm × 2 rounds,
132
+ after switching to the English question **12 probes had a lower p / 3–4 higher** (lowered by 0.04 on average, while the noise of repeated sampling is only 0.015),
133
+ **three commands crossed the band outright and all in the direction of allow**: `UPDATE` without WHERE, `DELETE ... WHERE` (block→revise),
134
+ inline `node -e rmSync` (revise→allow); same-band agreement 18/21. So switching `promptLang` is a recalibration,
135
+ not a translation — to use the English question, recalibrate first or lower the two thresholds by about 0.04.
136
+
137
+ **Other changes**
138
+
139
+ - README split into two: `README.md` English (default, for the GitHub first screen and `package.json.files`), `README.zh-CN.md`
140
+ Chinese, the two link to each other at the top (the common practice).
141
+ - Rule reasons changed to a bilingual object (`why: { 'zh-CN', en }`), still in the same rule as the regex; `ruleWhy()` takes the current language.
142
+ - The output of `guard rules` / `guard selftest` / `guard log` / `guard status` / `guard allow` all goes through the catalogue.
143
+ - CLI argument parsing changed to one-shot parsing: the **value** of a switch like `--lang en` can no longer be taken as a command to be judged
144
+ (`guard judge 'x' --lang en` used to judge `en` as well).
145
+ - New `tools/selftest-i18n.mjs` (32 cases): the key sets of the two languages agree, the placeholders agree, no residual Chinese in the English,
146
+ rule reasons complete in both languages, the detection chain's handling of "no signal" values like `C.UTF-8`, and two invariants —
147
+ "after the interface is switched to English the question sent to Jev is still Chinese" and "the interface language does not enter the request body (the two interfaces construct a byte-identical state and question)".
148
+ - New `tools/probe-prompt-lang.mjs`: a question-language comparison probe, `--repeat` is used to separate the language effect from
149
+ the service's own jitter (service non-determinism: the same state asked three times in a row gave 0.78/0.79/0.82).
150
+ - Removed the never-read `cliHints` from `KINDS` (a hidden piece of copy not covered by i18n).
151
+ - Removed the never-read `const VERSION = '0.1.0'` from `bin/guard.mjs`: it has no reference at all,
152
+ and had long since drifted three versions away from the version number in `package.json`; leaving it in the file only misleads the next person to read the code.
153
+ (The CLI has no `--version` subcommand; if one is really wanted, reading one line from `package.json` would do.)
154
+
155
+ **How it was verified**
156
+
157
+ - All seven offline self-checks pass: entry 20 / **i18n 32 (new)** / quota 54 / reason 54 / token 17 / rules 48 / audit 20.
158
+ - `guard selftest` 12/12; `smoke-dsh-adapter` all 10 groups pass.
159
+ - One run on the real deployment in each language: `status` / `rules` / `judge <a command that is bound to be blocked>` output correctly under both Chinese and English,
160
+ and the verdicts agree (only the copy differs).
161
+ - The parts of the existing assertions that read Chinese copy are now explicitly pinned with `setLang('zh-CN')`, no longer affected by the locale of the machine that runs them.
162
+
163
+ ## [0.3.1] — 2026-09-20
164
+
165
+ **L0 anchoring completed: false positives and missed detections fixed together in both directions** (the same root cause — the first round of anchoring only did half the job).
166
+ Trade-offs in [`DECISIONS.md`](./DECISIONS.md) **D2**, measurements in [`MEASUREMENTS.md`](./MEASUREMENTS.md) **§7.5**,
167
+ the boundary matrix in item 7 of [`VERIFICATION.md`](./VERIFICATION.md).
168
+
169
+ **Motivation (user's measured feedback):** a "check the logs" command had the literal text of `mkfs.ext4 /dev/…` written into a python source
170
+ string, and was stopped by L0 as a command — following that lead revealed that the deviation in the other direction mattered more.
171
+
172
+ **False-positive direction (loosening):** the first round anchored only the 7 deny rules + all 16 ask rules; `mkfs` / `dd` / `shred` /
173
+ `chmod -R /` / `vssadmin` / `wbadmin` / `cipher /w` / `diskpart` / `wsl --unregister` /
174
+ `kubectl delete ns` / `Clear-Disk` / `Remove-Item … -Recurse` — these **12 still matched the whole text**,
175
+ so data inside quotes, comments, variable assignments and code strings all hit `deny` — and L0's `deny`
176
+ **has no one-shot token channel**, so on a false block a person can only go run it in the terminal themselves. Now these 12 are uniformly anchored to the command position.
177
+
178
+ **Missed-detection direction (tightening, this one matters more):** the `^` used for anchoring **has no `m` flag**, so "command position" actually equals only
179
+ **the start of the whole string**. Every anchored rule therefore missed all the real commands in a multi-line script:
180
+
181
+ | Form | Before the fix | After the fix |
182
+ |---|---|---|
183
+ | `echo x \| xargs git push --force …` | MISS (`xargs` not in the wrapper list) | HIT |
184
+ | `bash - <<'SH'` + `git push --force …` | MISS (`^` matches only the start of the string) | HIT |
185
+ | `bash -c "` + multi-line + `git push --force …` | MISS | HIT |
186
+ | `rm -rf /` and `DROP DATABASE` in a heredoc | MISS | HIT |
187
+
188
+ Missed detections are fatal only under the `l0-only` degradation (no quota, no network) — and the very reason L0 exists is that moment (D9).
189
+ In normal times Jev covers for it, so it was never discovered. Ironically, before the fix the `mkfs` in a heredoc **was in fact a hit**,
190
+ only because it was not anchored — the two deviations are two directions of the same root cause.
191
+
192
+ **The concrete changes**
193
+
194
+ - The anchoring regex gets the `m` flag (`^` from then on matches the start of **every line**).
195
+ - The wrapper list is expanded to `sudo/doas/env/command/nohup/time/nice/ionice/setsid/stdbuf/watch/timeout/xargs/parallel/find`;
196
+ the arguments swallowed may only be ASCII words/flags/path characters — Chinese prose is therefore still not caught along the way (measured: `xargs 删除 mkfs…` is not a hit).
197
+ - `bash -c "` / `sh -c '` also count as a command position.
198
+ - Only `redirect-to-device` (`>`) and `fork-bomb` (`:(){…};:`) remain explicitly marked `where: 'anywhere'`;
199
+ a new counter `RULE_STATS.anywhere` (constant at 2; if it grows, full-text matching has crept back in).
200
+
201
+ **How it was verified**
202
+
203
+ - `tools/selftest-rules.mjs`: 25 → **48 cases** (22 matrix cases split into A against false positives / B against missed detections / C against collateral damage to prose,
204
+ plus 1 performance case: a 4KB wrapper prefix **0.6ms**, confirming there is no catastrophic backtracking).
205
+ - The other six suites have no regression (entry 15 / quota 54 / reason 56 / token 17 / audit 20 / smoke 10).
206
+ - Real deployment: item 7 was expanded by three lines (the original false-positive probe + two multi-line/wrapper forms that "must be caught by L0").
207
+
208
+ ## [0.3.0] — 2026-09-20
209
+
210
+ The judging action **forks with DSH's approval mode**; and a potential hole through which the retry budget could bypass the L0 hard floor is fixed. Trade-offs in
211
+ [`docs/DECISIONS.md`](./docs/DECISIONS.md) **D13**, acceptance steps in item 22 of [`docs/VERIFICATION.md`](./docs/VERIFICATION.md).
212
+
213
+ **Motivation (user's measured feedback):** "the safety valve also blocks when it hits 50%-or-more, but what I actually have in mind is, block in fully automatic mode,
214
+ and in the modes that need approval change all the interceptions into an approval prompt."
215
+
216
+ Audit corroboration: of the day's 59 `revise`/`block` refusals, **9 happened in sessions with `approval: ask`**
217
+ (p all in 0.50–0.63), the commands being `cp` into a deployment directory, `sed -i`, `mkdir -p`, `git add -A && git commit`
218
+ — all of them the operator's own maintenance actions, **the person was right there but could not get a prompt**, which amounts to letting a 50% judgment decide in the person's place.
219
+
220
+ **What changed**
221
+
222
+ - `ask` (the person is on the spot): `revise` (50–70%) and **Jev's high-score `block` (≥70%) both turn into a human approval dialog**;
223
+ the reason header becomes "needs human confirmation", the dialog body carries the three degradation templates as before; the one-shot token authorisation line is **no longer** attached.
224
+ - `never` (fully automatic): both remain a **direct refusal** + token hint — in such a session DSH turns any `ask` straight into
225
+ `rejected`, so a conservative refusal by the valve is the only correct way to land.
226
+ - **The only exception**: L0's `deny`-class hard rules (`rm -rf /`, `mkfs`, `git push --force`…) are **hard-blocked** in both modes,
227
+ no dialog, no token.
228
+ - New config `reviseInAskMode` / `blockInAskMode` (default `ask`); to go back to the old behaviour fill in `deny`, no code change needed.
229
+ - **Potential hole fixed**: the retry budget used to upgrade "any non-escalate verdict" to escalate after the `retryLimit+1`-th time —
230
+ the same held for L0 hard hits, so in `ask` mode submitting `git push --force` three times in a row would pop the dialog, and clicking "allow" **crossed the hard floor**
231
+ (contradicting D5's "a token does not cross L0"). Now a hard hit does not take part in the budget upgrade (it still records `attempts` for auditing).
232
+ The audit shows this path was **never triggered** before the fix (a purely potential hole, not an accident that happened).
233
+
234
+ **How it was verified**
235
+
236
+ - `tools/selftest-reason.mjs`: 38 → **56 cases**, adding the routing matrix of `revise`/`block` × `ask`/`never` × L0,
237
+ the two config switches, and "the header follows the routing result".
238
+ - `tools/smoke-dsh-adapter.mjs`: 9 → **10 groups**, adding "an L0 hard rule tried 4 times in a row is always deny (not upgraded into a dialog by the budget)".
239
+ - Real-deployment acceptance (item 22) **must be run** on both the `ask` and the `never` side — running only one side is marked `partial`.
240
+
241
+ ## [0.2.0] — 2026-09-20
242
+
243
+ Narrowed from "wanting to make it general-purpose" to **DSH-specific**, and "how a person intervenes" and "what to do when the paid quota runs out" were filled in.
244
+
245
+ **Renaming**
246
+
247
+ - Package name `jev-guard` → `dsh-jev-guard` (matching `dsh-*` plugin naming); the loader `name` in `cordis.patch.yml` follows.
248
+ - Platform support unchanged: **both WSL/Linux and Windows are supported**.
249
+
250
+ **New: the person's three intervention channels** (see [`docs/USER-INTERVENTION.md`](./docs/USER-INTERVENTION.md))
251
+
252
+ - One-shot allow token: bound to the command's original text, deleted once used, does not cross L0 hard rules, authorised only on an interactive terminal;
253
+ the authorisation line gives the correct quoting for the **platform** (POSIX `'\''` vs PowerShell `''`), and Windows has `--command-file` as well.
254
+ - Host approval prompt: under `approval: ask` DSH pops the dialog, the reason carries the valve's original text; when a dialog can be shown the token hint is no longer attached.
255
+ - Executing by hand: the valve is not involved, and it does not thereby give the AI any permission.
256
+
257
+ **New: degradation when the quota is exhausted** (see [`docs/DECISIONS.md`](./docs/DECISIONS.md) D9)
258
+
259
+ - `quota` / `auth`-class failures → write `~/.jev-guard/degraded.json`, no more requests are sent inside the cooldown window (saving money),
260
+ by default only the **free L0 + pre-screen** runs; when the window expires one probe is automatically let through, and success restores normal operation.
261
+ - Warnings appear in the refusal reason, the audit (`source: degraded` + `level: warn`), stderr and `guard status` (exit code 3 while degraded).
262
+ - New cost visibility: the `usage` in the response is counted into the audit, `guard log --stats` converts it to US dollars and marks the coverage.
263
+
264
+ **DSH-specific work**
265
+
266
+ - Audit records carry the session's **sandbox preset** (`permission/preset`), so a later retrospective can tell "was there still a sandbox behind this at the time".
267
+ - Two entry points aimed at external callers were removed (a long-running HTTP judging service, a stdio protocol service) — a DSH plugin is in-process.
268
+
269
+ **Fixes (three layers of cross-platform entry-point defects, see [`docs/MEASUREMENTS.md`](./docs/MEASUREMENTS.md) §10)**
270
+
271
+ 1. `import.meta.url === \`file://${process.argv[1]}\`` is always false on Windows → the script silently exits 0;
272
+ 2. while fixing the first layer a shared module was extracted for DRY, which made `import.meta.url` point at that module itself → even WSL broke;
273
+ 3. a dynamic `import(join(ROOT, …))` throws `ERR_UNSUPPORTED_ESM_URL_SCHEME` on Windows.
274
+ Also fixed: `apiKeyFile` relative paths are resolved against the package root; `no-key` no longer triggers a global degradation.
275
+
276
+ **Security**
277
+
278
+ - Three real secrets in the test fixtures (two API keys + one GitHub token) have been replaced with synthetic values — they would otherwise have been published along with the repository.
279
+
280
+ **Acceptance**: the whole self-check suite passes on both the WSL and the Windows side (see [`verification-results/`](./verification-results/)).
281
+
282
+ ## [0.1.0] — 2026-09-20
283
+
284
+ First release: the four-state valve hooked into DSH `tools/pre-execute` (L0 static hard rules + pre-screen + Jev semantic judgment),
285
+ a shared audit log, a retry budget, the degradation templates in the refusal reason.
@@ -0,0 +1,271 @@
1
+ # 更新日志
2
+
3
+ > [English](CHANGELOG.md) | **简体中文**
4
+
5
+ 本项目遵循「按日期记录事实」的写法:每条都写清**改了什么、为什么、以及怎么验证的**。
6
+ 完整的设计取舍见 [`docs/DECISIONS.md`](./docs/DECISIONS.md),实测数据见 [`docs/MEASUREMENTS.md`](./docs/MEASUREMENTS.md)。
7
+
8
+ ## [0.5.1] — 2026-09-22
9
+
10
+ **发布到 npm,并撤销宿主版本声明。** 这次改的是**安装方式与声明内容**,不是行为 —— `bin/`、`lib/`、`adapters/`、`cordis.patch.yml` 与默认配置相对 0.5.0 一字未变。
11
+
12
+ **从 registry 安装。** 包已发布为 `dsh-jev-guard`,也就是插件市场优先采用的安装源(先取仓库校验过的 npm 包,其次作者预编译的 GitHub Release tarball,最后才回落到全仓源码下载)。对到 GitHub 链路慢或不可靠的用户,这是「几秒」与「克隆一次」的区别:
13
+
14
+ ```bash
15
+ dsh plugin --profile web add dsh-jev-guard
16
+ ```
17
+
18
+ `private` 已移除,并新增 `publishConfig` 把 registry 钉在 `https://registry.npmjs.org/`,避免 `.npmrc` 里的镜像把发布悄悄重定向走。
19
+
20
+ **不再声明 DSH 版本。** `engines` 现在只剩 `node`。0.5.0 之后曾短暂加过一条下界(`engines.dsh: "0.1.6-alpha.2"`),但它从未进过任何 tag,这里予以撤销。原因是插件市场会从 npm manifest 读这个字段:写成精确版本会让市场判「确认不兼容」,从而在其他所有 DSH 版本上拦住安装与更新 —— 也包括你自己以后升级到的那一版。字段缺席时市场显示「未声明宿主要求」,永不阻拦。
21
+
22
+ **已验证的版本仍是文档里写的那一个。** DSH 0.1.6-alpha.2 依然是本插件唯一跑过的版本;别的版本是没验过、而不是被禁止,用了请重跑自检。
23
+
24
+ **验收**:`node bin/guard.mjs selftest`(12/12)与七套离线自检(`tools/selftest-*.mjs`)全过;`npm publish --dry-run` 确认 52 个文件,包内不含配置、密钥、审计日志与交接材料。
25
+
26
+ ## [0.5.0] — 2026-09-20
27
+
28
+ **首次部署现在有真正的密钥录入入口,而"没有密钥"不再静默:它像额度耗尽那样降级 —— 大声、粘性,并且不停止免费层。**
29
+ 取舍见 [`DECISIONS.md`](./DECISIONS.md) **D15**,机制见 [`docs/DSH-INTEGRATION.md`](./docs/DSH-INTEGRATION.md)。
30
+
31
+ **录入入口:`guard key set` / `guard key status`。** 密钥**只从标准输入**读 —— 绝不接受命令行参数,
32
+ 那会进 shell 历史与 `ps`。它写 `apiKeyFile` 指定的文件(默认包根 `secrets.json`),权限 `0600`,
33
+ 保留文件里已有的其它键,只打印长度与路径、**永不打印值**。`guard key status` 说明当前**哪个来源**在
34
+ 生效、密钥多长,同样不回显;没有密钥时退出码 3,所以它也能当健康检查用。读与写共用同一个路径解析,
35
+ 所以相对 `apiKeyFile` 不可能在两个地方指向不同的文件。
36
+
37
+ **为什么 DSH 适配器也必须改。** 它原先只从 `ctx.credentials` 与环境变量取密钥,而 `guard key set`
38
+ 写的是文件。少了这一层,"用 CLI 录入密钥"对**恰好还没有凭据层的新装用户**就是一句空话。现在适配器
39
+ 也回退到 `apiKeyFile`,规则一致:相对路径按包根解析,与 cwd 无关。
40
+
41
+ **`no-key` 现在是会降级的类别 —— 粘性,而且带作用域。** 它原先刻意不降级(D10.2),理由是对的:
42
+ 本地配置问题写进全机共享的 `degraded.json`,会把**密钥其实是好的**其它入口一起按停。这个反对意见
43
+ 现在用**作用域**回答,而不是用沉默回答。服务侧类别(`quota` / `auth`)仍是 `scope: 'global'`,压制
44
+ 所有入口;`no-key` 是 `scope: 'local'`,只压制写下它的那条入口(`'cli'` 或 `'dsh-adapter'`)。又因为
45
+ 没有密钥时**一次 HTTP 都不发**,没有可探测对象 —— 所以状态是**粘性**的:不随时间到期,密钥一出现
46
+ 就结束(当场清除、零请求、不用重启)。`guard status` 直接这么说,而不是打印一个从来不重要的倒计时。
47
+
48
+ **用户真的会被通知到。** 纯 host 插件没有 toast、没有 banner、没有启动提示 —— DSH 的设置页与 Plugins
49
+ 页的每一个位置都由浏览器侧(`dsh.client`)注册占位。唯一存在的渠道是在 `agent/pre-step` 注入一条
50
+ `notice` 消息:它渲染成对话里的一行、写进会话历史、并进入模型上下文(于是模型也知道阀门降级了)。
51
+ 插件用它覆盖三种跃迁 —— 首次运行没有密钥(提出要求,并附上确切命令)、进入降级、以及恢复;每种状态
52
+ 每个会话只说一次,去重依据是**持久化的历史**,所以重启或恢复会话都不会重复。`notifyInSession: false`
53
+ 可以关掉它。
54
+
55
+ **消息形状是一份契约,而且它有校验。** `source` 恰好带 `kind` / `plugin` / `form` / `summary` 四个键,
56
+ 摘要上限 120 字符(它要当折叠行的标题)。这里写错的表现是**下次恢复会话时**报
57
+ `SessionPersistenceCorruptionError` —— 会话打不开,而现场离改动很远。所以形状要交给 DSH 自己的
58
+ `snapshotJsonValue`(`Session.append` 之前跑的那一步)校验,由一份必须跑在 DSH 目录树里的测试执行;
59
+ 四个键的 source 与摘要上限则由离线冒烟测试断言。本版本已实际跑过这项校验并通过。
60
+
61
+ **本版本还包括:** `package.json` 不再声明 `dsh.runtime` —— 它不是 DSH 插件 manifest 的字段(真实字段
62
+ 是 `manifestVersion` / `bundle` / `profile` / `client`),所以它什么也没做,却让读这个文件的人以为它有意
63
+ 义。`tools/smoke-dsh-adapter.mjs` 变成了密闭的:它以前会往**真实的** `~/.jev-guard/` 写降级状态与审计
64
+ 记录,现在每一处 `apply()` 都把 `logPath` / `degradedPath` / `apiKeyFile` 钉到临时目录;它还新增了 9 条
65
+ 断言,覆盖粘性状态、notice 注入(是追加而非替换、空批次不乱塞)与文件回退。`tools/selftest-quota.mjs`
66
+ 从 50 例涨到 78 例,`tools/selftest-entry.mjs` 到 30 例(它现在真的会跑一遍 `guard key set`,包括通过
67
+ 伪 TTY 走的交互路径),`tools/selftest-i18n.mjs` 到 34 例。这里新增的每一条面向用户的文案都有中英两份。
68
+
69
+ **本次未覆盖:** `tools/smoke-dsh-pipeline.mjs` 需要一个能让裸 `@deepseek-ai/*` 说明符解析成功的 DSH
70
+ 工作区,本部署不满足,因此没能运行(它在上一提交上以同样方式失败,所以不是回归)。notice 形状改为直接
71
+ 对着 DSH 的 `snapshotJsonValue` 验证。
72
+
73
+ ## [0.4.1] — 2026-09-20
74
+
75
+ **给人看的文档一律改为英文默认,中文逐字节保留为 `*.zh-CN.md`。** 约定写在
76
+ [`DECISIONS.md`](./DECISIONS.md) **D14 第 5 条**。
77
+
78
+ **动了哪些。** 共 12 份:`CHANGELOG.md`、`DEPLOY.md`、`START-HERE.md`、`adapters/README.md`、
79
+ `verification-results/README.md`,以及 `docs/` 下的七份。每份的中文原文**逐字节**保留为同目录的
80
+ `<name>.zh-CN.md`(只多一行语言切换行),两份互相跳转 —— 与 `README.md` / `README.zh-CN.md`
81
+ 早已采用的做法一致。`verification-results/SUMMARY.md` 是生成物,所以给生成器接上了语言开关、
82
+ 默认改为英文(证据列仍是**逐字中文引用** —— 翻译过的引用就是伪造的引用)。
83
+
84
+ **拆分怎么验的(机械校验,不靠信任)。** `tools/check-doc-pairs.mjs`(本次新增,让以后的改动也能守住两侧同步)逐对比对:中文版必须**逐字节等于** `HEAD` 里的原文
85
+ 加上那一行切换行;两份文件在标题层级序列、代码围栏数、表格行数、链接目标集合、数字多重集上必须
86
+ 一致。实测引用、日志原文、命令样例与中文语料一律保持原样,因此英文文档里剩下的中文只应是"被引用的
87
+ 证据" —— 这些残留逐条列出并人工过了一遍。
88
+
89
+ **本版还有:** `tools/report-result.mjs` 去掉硬编码中文标签(搬进 `lib/i18n.js`),编号索引补上
90
+ 第 23 项;代码注释与 `tools/` 里的自检标签仍不翻译(D14 第 4 条)—— 界限是"仓库外的人读得到 → 双语,
91
+ 只有维护者读 → 中文"。
92
+
93
+ ## [0.4.0] — 2026-09-20
94
+
95
+ **中英双语:给人看的文案两种语言都有,README 默认英文**;判定问话默认仍是标定过的中文。
96
+ 取舍见 [`docs/DECISIONS.md`](./DECISIONS.md) **D14**,实测见 [`docs/MEASUREMENTS.md`](./MEASUREMENTS.md) **§14**。
97
+
98
+ **起因(用户要求):** 仓库默认 README 改英文、中文介绍按通行做法跳转;源代码也要中英双语支持。
99
+
100
+ **文案双语化。** 新增 `lib/i18n.js`:`lang` 控制界面语言,默认 `'auto'`
101
+ (`JEV_GUARD_LANG` → `LC_ALL`/`LC_MESSAGES`/`LANG`,**仅当它们指明一种受支持的语言** → 否则 `zh-CN`),
102
+ CLI 另有 `--lang zh-CN|en`。覆盖范围:判定理由(四态抬头、三种降级模板、令牌授权行)、
103
+ 37 条 L0 规则的理由、CLI 全部输出、降级告警与 `guard status` 报告、发给判定服务的 state 键。
104
+ **代码注释与 `tools/` 里的自检标签不翻译** —— 它们是维护者读的,翻译只会让每次改动的维护成本翻倍。
105
+
106
+ **`auto` 刻意不看系统 locale —— 这一条是第一次真实部署时当场踩出来的。** 最初把 `Intl` 也放在
107
+ 探测链尾,结果:DSH 插件跑在 WSL 里,那里 `LANG=C.UTF-8` 表示"没有偏好",`Intl` 于是报出 Node
108
+ 自己的 `en-US` 兜底值 —— 会话里的拦截理由**悄悄变成英文**,而同一台机器的 Windows 侧 CLI(其 Node
109
+ 报 `zh-CN`)仍是中文,同一个阀门两种语言。现在 `C`/`POSIX`/未设置一律视为**没有信号**并落回项目主语言
110
+ (`zh-CN`),要英文就明说。
111
+
112
+ **语言开关的优先级也修了一处埋点:** 这两项原本写在包内 `cordis.patch.yml` 里,而 patch 优先级
113
+ 高于 `config.json` —— 意味着用户在 `config.json` 里设的语言会被无声覆盖。现在改为注释形式
114
+ (默认值仍写在注释里),语言交给 `config.json` / `JEV_GUARD_LANG` / `--lang` 控制。
115
+
116
+ **判定问话与界面语言解耦(这条是本版的核心)。** `promptLang` 单独控制发给 Jev 的那句问话与
117
+ state 的键,**默认 `'zh-CN'` 不随界面语言变**。理由不是保守,是实测:21 条探针 × 每臂 3 次 × 2 轮,
118
+ 换成英文问话后 **12 条 p 更低 / 3–4 条更高**(平均压低 0.04,重复采样噪声只有 0.015),
119
+ **三条命令直接翻带且方向全部朝放行**:无 WHERE 的 `UPDATE`、`DELETE ... WHERE`(block→revise)、
120
+ 内联 `node -e rmSync`(revise→allow);同带一致率 18/21。所以切 `promptLang` 是一次重标定,
121
+ 不是翻译 —— 要用英文问话,先重标定或把两个阈值下调约 0.04。
122
+
123
+ **其它改动**
124
+
125
+ - README 拆成两份:`README.md` 英文(默认,给 GitHub 首屏与 `package.json.files`),`README.zh-CN.md`
126
+ 中文,两份顶部互相跳转(通行做法)。
127
+ - 规则理由改成双语对象(`why: { 'zh-CN', en }`),仍与正则同处一条规则;`ruleWhy()` 取当前语言。
128
+ - `guard rules` / `guard selftest` / `guard log` / `guard status` / `guard allow` 的输出全部走目录。
129
+ - CLI 参数解析改为一次性解析:`--lang en` 这类开关的**值**不再可能被当成一条待判定的命令
130
+ (`guard judge 'x' --lang en` 以前会把 `en` 也判一遍)。
131
+ - 新增 `tools/selftest-i18n.mjs`(32 例):两语言键集合一致、占位符一致、英文里无残留中文、
132
+ 规则理由双语齐全、探测链对 `C.UTF-8` 这类"没有信号"值的处理、以及两条不变量 ——
133
+ "界面切英文后发给 Jev 的问话仍是中文"和"界面语言不进请求体(两种界面构造出逐字节相同的 state 与问话)"。
134
+ - 新增 `tools/probe-prompt-lang.mjs`:问话语言对照探针,`--repeat` 用来把语言效应与
135
+ 服务自身的抖动分开(服务非确定性:同一 state 连问三次得过 0.78/0.79/0.82)。
136
+ - 清掉 `KINDS` 里从未被读取的 `cliHints`(一份不受 i18n 覆盖的隐藏文案)。
137
+ - 清掉 `bin/guard.mjs` 里从未被读取的 `const VERSION = '0.1.0'`:它没有任何引用,
138
+ 且早已与 `package.json` 的版本号脱节三版,留在文件里只会误导下一个读代码的人。
139
+ (CLI 没有 `--version` 子命令;真要加,从 `package.json` 读一行即可。)
140
+
141
+ **怎么验证的**
142
+
143
+ - 七份离线自检全过:entry 20 / **i18n 32(新)** / quota 54 / reason 54 / token 17 / rules 48 / audit 20。
144
+ - `guard selftest` 12/12;`smoke-dsh-adapter` 10 组全过。
145
+ - 真机双语各跑一次:`status` / `rules` / `judge <必然被拦的命令>` 在中英两种语言下输出正确,
146
+ 判定结果一致(只有文案不同)。
147
+ - 既有断言里读中文文案的部分已显式 `setLang('zh-CN')` 钉死,不再受运行机器 locale 影响。
148
+
149
+ ## [0.3.1] — 2026-09-20
150
+
151
+ **L0 锚定补完:假阳与漏判两个方向一起修**(同一个根因 —— 第一轮锚定只做了一半)。
152
+ 取舍见 [`DECISIONS.md`](./DECISIONS.md) **D2**,实测见 [`MEASUREMENTS.md`](./MEASUREMENTS.md) **§7.5**,
153
+ 边界矩阵见 [`VERIFICATION.md`](./VERIFICATION.md) 第 7 项。
154
+
155
+ **起因(用户实测反馈):** 一条"查日志"的命令把 `mkfs.ext4 /dev/…` 的原文写进 python 源码的
156
+ 字符串里,被 L0 当成命令拦下 —— 顺着查下去才发现反方向的偏差更要紧。
157
+
158
+ **假阳方向(放松):** 第一轮只锚定了 7 条 deny + 全部 16 条 ask,`mkfs` / `dd` / `shred` /
159
+ `chmod -R /` / `vssadmin` / `wbadmin` / `cipher /w` / `diskpart` / `wsl --unregister` /
160
+ `kubectl delete ns` / `Clear-Disk` / `Remove-Item … -Recurse` 这 **12 条仍在全文匹配**,
161
+ 于是引号里的数据、注释、变量赋值、代码字符串都会命中 `deny` —— 而 L0 的 `deny`
162
+ **没有一次性令牌通道**,误拦时人只能自己去终端执行。现在这 12 条统一锚定到命令位置。
163
+
164
+ **漏判方向(收紧,这条更要紧):** 锚定用的 `^` **没有 `m` 标志**,所以"命令位置"实际只等于
165
+ **整串开头**。凡被锚定的规则,多行脚本里的真命令全部漏判:
166
+
167
+ | 形态 | 修正前 | 修正后 |
168
+ |---|---|---|
169
+ | `echo x \| xargs git push --force …` | MISS(`xargs` 不在包装器列表) | HIT |
170
+ | `bash - <<'SH'` + `git push --force …` | MISS(`^` 只匹配串首) | HIT |
171
+ | `bash -c "` + 多行 + `git push --force …` | MISS | HIT |
172
+ | heredoc 里的 `rm -rf /`、`DROP DATABASE` | MISS | HIT |
173
+
174
+ 漏判只在 `l0-only` 降级(没有额度、没有网络)时才致命 —— 而 L0 的存在理由正是那一刻(D9)。
175
+ 平时由 Jev 兜着,所以从没被发现。讽刺的是:修正前 heredoc 里的 `mkfs` **反而是命中的**,
176
+ 只因为它没锚定 —— 两个偏差是同一个根因的两个方向。
177
+
178
+ **具体改动**
179
+
180
+ - 锚定正则加 `m` 标志(`^` 从此匹配**每一行**行首)。
181
+ - 包装器扩到 `sudo/doas/env/command/nohup/time/nice/ionice/setsid/stdbuf/watch/timeout/xargs/parallel/find`;
182
+ 吞掉的参数只允许 ASCII 词/flag/路径字符 —— 中文散文因此仍不会被顺带命中(实测 `xargs 删除 mkfs…` 不命中)。
183
+ - `bash -c "` / `sh -c '` 也算命令位置。
184
+ - 只剩 `redirect-to-device`(`>`)与 `fork-bomb`(`:(){…};:`)显式标为 `where: 'anywhere'`;
185
+ 新增计数器 `RULE_STATS.anywhere`(恒为 2,变大就说明又退回了全文匹配)。
186
+
187
+ **怎么验证的**
188
+
189
+ - `tools/selftest-rules.mjs`:25 → **48 例**(22 条矩阵用例分 A 防假阳 / B 防漏判 / C 防误伤散文,
190
+ 外加 1 例性能:4KB 包装器前缀 **0.6ms**,确认没有灾难性回溯)。
191
+ - 其余六套无回归(entry 15 / quota 54 / reason 56 / token 17 / audit 20 / smoke 10)。
192
+ - 真机:第 7 项扩了三行(原来的假阳探针 + 两条"必须被 L0 抓住"的多行/包装器形态)。
193
+
194
+ ## [0.3.0] — 2026-09-20
195
+
196
+ 判定动作**随 DSH 的审批模式分叉**;并修掉重试预算能绕过 L0 硬地板的潜在洞。取舍见
197
+ [`docs/DECISIONS.md`](./docs/DECISIONS.md) **D13**,验收步骤见 [`docs/VERIFICATION.md`](./docs/VERIFICATION.md) 第 22 项。
198
+
199
+ **起因(用户实测反馈):** "安全阀遇到 50% 多的时候也会拦,而我实际上想的是,在全自动模式下拦,
200
+ 而在需要审批的模式里面所有的拦截都改成弹审批。"
201
+
202
+ 审计佐证:当天 59 条 `revise`/`block` 拒绝里 **9 条发生在 `approval: ask` 的会话中**
203
+ (p 全在 0.50–0.63),命令是 `cp` 到部署目录、`sed -i`、`mkdir -p`、`git add -A && git commit`
204
+ —— 都是操作者自己的维护动作,**人就在旁边却拿不到弹窗**,等于让一个 50% 的判断替人做决定。
205
+
206
+ **改了什么**
207
+
208
+ - `ask`(人就在场):`revise`(50–70%)与 **Jev 高分的 `block`(≥70%)都转人工弹审批框**;
209
+ 理由抬头改成"需要人工确认",弹窗正文照旧带三种降级模板;**不再**附一次性令牌授权行。
210
+ - `never`(全自动):两者仍是**直接拒绝** + 令牌提示 —— 那种会话里 DSH 会把任何 `ask` 直接判成
211
+ `rejected`,由阀门保守地拒是唯一正确的落法。
212
+ - **唯一例外**:L0 的 `deny` 类硬规则(`rm -rf /`、`mkfs`、`git push --force`…)两种模式都**拦死**,
213
+ 不弹窗、不发令牌。
214
+ - 新增配置 `reviseInAskMode` / `blockInAskMode`(默认 `ask`);想回到旧行为填 `deny`,不用改代码。
215
+ - **修潜在洞**:重试预算原本把"任何非 escalate 判定"在第 `retryLimit+1` 次后升级为 escalate ——
216
+ 对 L0 硬命中也一样,于是 `ask` 模式下连交三次 `git push --force` 就会弹窗,而点"允许"**越过了硬地板**
217
+ (与 D5"令牌不越过 L0"自相矛盾)。现在硬命中不参与预算升级(仍记 `attempts` 供审计)。
218
+ 审计显示这条路径在修复前**从未被触发过**(纯潜在洞,不是已发生的事故)。
219
+
220
+ **怎么验证的**
221
+
222
+ - `tools/selftest-reason.mjs`:38 → **56 例**,新增 `revise`/`block` × `ask`/`never` × L0 的路由矩阵、
223
+ 两个配置开关、以及"抬头跟着路由结果走"。
224
+ - `tools/smoke-dsh-adapter.mjs`:9 → **10 组**,新增"L0 硬规则连试 4 次始终是 deny(不被预算升级成弹窗)"。
225
+ - 真机验收(第 22 项)在 `ask` 与 `never` 两侧**都要跑** —— 只跑一侧标 `partial`。
226
+
227
+ ## [0.2.0] — 2026-09-20
228
+
229
+ 从"想做成通用"收窄为 **DSH 专用**,并把"人怎么介入"与"花钱的额度用完了怎么办"补齐。
230
+
231
+ **改名**
232
+
233
+ - 包名 `jev-guard` → `dsh-jev-guard`(符合 `dsh-*` 插件命名);`cordis.patch.yml` 的 loader `name` 同步。
234
+ - 平台支持不变:**WSL/Linux 与 Windows 都支持**。
235
+
236
+ **新增:人的三条介入通道**(见 [`docs/USER-INTERVENTION.md`](./docs/USER-INTERVENTION.md))
237
+
238
+ - 一次性放行令牌:绑定命令原文、用掉即删、不越过 L0 硬规则、只在交互终端授权;
239
+ 授权行按**平台**给出正确的引号写法(POSIX `'\''` vs PowerShell `''`),Windows 另有 `--command-file`。
240
+ - 宿主审批弹窗:在 `approval: ask` 下由 DSH 弹框,理由里带上阀门原文;能弹框时不再附令牌提示。
241
+ - 人工手动执行:阀门不参与,且不会因此给 AI 任何权限。
242
+
243
+ **新增:额度耗尽的降级**(见 [`docs/DECISIONS.md`](./docs/DECISIONS.md) D9)
244
+
245
+ - `quota` / `auth` 类失败 → 写 `~/.jev-guard/degraded.json`,冷却窗口内不再发请求(省钱),
246
+ 默认只跑**免费的 L0 + 预筛**;窗口到期自动放一次探测,成功即恢复。
247
+ - 告警出现在拒绝理由、审计(`source: degraded` + `level: warn`)、stderr 与 `guard status`(降级时退出码 3)。
248
+ - 新增成本可见性:响应里的 `usage` 计入审计,`guard log --stats` 折算美元并标注覆盖率。
249
+
250
+ **DSH 特化**
251
+
252
+ - 审计记录带上会话的**沙箱档位**(`permission/preset`),事后复盘能看出"当时后面还有没有沙箱"。
253
+ - 去掉两个面向外部调用方的入口(常驻 HTTP 判定服务、stdio 协议服务)——DSH 插件是进程内的。
254
+
255
+ **修复(三层跨平台入口缺陷,详见 [`docs/MEASUREMENTS.md`](./docs/MEASUREMENTS.md) §10)**
256
+
257
+ 1. `import.meta.url === \`file://${process.argv[1]}\`` 在 Windows 上恒为 false → 脚本静默退出 0;
258
+ 2. 修第一层时为 DRY 抽共享模块,导致 `import.meta.url` 指向该模块自身 → 连 WSL 也失效;
259
+ 3. 动态 `import(join(ROOT, …))` 在 Windows 上抛 `ERR_UNSUPPORTED_ESM_URL_SCHEME`。
260
+ 另修:`apiKeyFile` 相对路径按包根解析;`no-key` 不再触发全局降级。
261
+
262
+ **安全**
263
+
264
+ - 测试夹具里的三个真实密钥(两个 API key + 一个 GitHub token)已替换为合成值 —— 它们本会随仓库一起被发布。
265
+
266
+ **验收**:WSL 与 Windows 两侧整套自检全过(见 [`verification-results/`](./verification-results/))。
267
+
268
+ ## [0.1.0] — 2026-09-20
269
+
270
+ 首版:挂 DSH `tools/pre-execute` 的四态阀门(L0 静态硬规则 + 预筛 + Jev 语义判定)、
271
+ 共享审计日志、重试预算、拒绝理由里的降级模板。