pi-verdict 0.11.0 → 0.12.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.
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
  [![npm](https://img.shields.io/npm/v/pi-verdict)](https://www.npmjs.com/package/pi-verdict)
7
7
  [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://pi.dev)
8
8
 
9
- **pi-verdict is a minimal permission gate for [pi](https://pi.dev) in the style of Claude Code's auto mode: every tool call gets checked before it runs — allow, deny, or ask you first.**
9
+ **pi-verdict is a minimal permission gate for [pi](https://pi.dev), inspired by Claude Code's auto mode: every tool call gets checked before it runs — allow, deny, or ask you first.**
10
10
 
11
- - Minimal — just 1k+ lines of code
11
+ - Minimal — a ~2k-line single-file core (plus a small bundled jev adapter)
12
12
  - Built-in danger rules and your own allow/deny rules settle the clear cases first, at zero latency
13
13
  - Everything else goes to a model classifier that sees the conversation context
14
14
  - Any uncertainty or failure fails closed; nothing ever runs silently
@@ -39,10 +39,10 @@ Full statement in [docs/security-principles.md](docs/security-principles.md).
39
39
 
40
40
  ## Screenshots
41
41
 
42
- ![Demo: protected-path ask declined](docs/demo.gif)
42
+ ![Demo: protected-path ask declined](https://raw.githubusercontent.com/jesset/pi-verdict/main/docs/demo.gif)
43
43
 
44
- ![Automode Status](docs/images/status.png)
45
- ![Ask Permission](docs/images/asked.png)
44
+ ![Automode Status](https://raw.githubusercontent.com/jesset/pi-verdict/main/docs/images/status.png)
45
+ ![Ask Permission](https://raw.githubusercontent.com/jesset/pi-verdict/main/docs/images/asked.png)
46
46
 
47
47
  ## Quick start
48
48
 
@@ -58,6 +58,8 @@ pi --extension ./extensions/pi-verdict.ts
58
58
 
59
59
  ```
60
60
 
61
+ Requires pi ≥ 0.84. Works in interactive and non-interactive (`-p`/json/rpc) sessions; in non-interactive modes `ask` degrades to `deny`.
62
+
61
63
  ### Hosts
62
64
 
63
65
  pi-verdict runs on both [pi](https://github.com/badlogic/pi-mono) and [oh-my-pi](https://github.com/can1357/oh-my-pi) (omp) — it self-anchors to whichever agent tree it is installed in, and follows the extension copy's own location on dual-install machines. On omp 18 the classifier's completion call falls back to the pi-ai compat API (still fail-closed). Details: [docs/configuration.md](docs/configuration.md#host-notes-pi-and-oh-my-pi).
@@ -69,7 +71,7 @@ pi-verdict runs on both [pi](https://github.com/badlogic/pi-mono) and [oh-my-pi]
69
71
  | user rules | `~/.pi/agent/config/pi-verdict.json` | `~/.omp/agent/config/pi-verdict.json` |
70
72
  | credential file (S0 hard deny) | `~/.pi/agent/auth.json` | `~/.omp/agent/auth.json` |
71
73
 
72
- - `/automode` — show current status: on/off + shadow-cache stats for the session
74
+ - `/automode` — show current status: on/off
73
75
  - `/automode on`
74
76
  - `/automode off`
75
77
  - `ctrl+shift+a` — toggle the master switch silently (the always-on footer is the only feedback; rebind or disable via `toggleShortcut`)
@@ -94,9 +96,16 @@ pi-verdict runs on both [pi](https://github.com/badlogic/pi-mono) and [oh-my-pi]
94
96
  "~/.profile",
95
97
  "~/.gnupg",
96
98
  "~/.mc",
99
+ "~/.kube",
97
100
  "~/.zshrc",
98
101
  "~/.bashrc"
99
102
  ],
103
+ "ignoreTools": [
104
+ "todo",
105
+ "ask_user_question",
106
+ "memory_write",
107
+ "memory_search"
108
+ ],
100
109
  "builtinDenyFloor": true,
101
110
  "classifierModel": null,
102
111
  "toggleShortcut": "ctrl+shift+a",
@@ -104,18 +113,19 @@ pi-verdict runs on both [pi](https://github.com/badlogic/pi-mono) and [oh-my-pi]
104
113
  "notifyAllows": false,
105
114
  "classifierMinConfidence": null,
106
115
  "classifierFallbackModel": null,
107
- "classifierFallbackMode": "shadow"
116
+ "classifierFallbackMode": "enforce"
108
117
  }
109
118
  ```
110
119
 
111
120
  - `allow`/`deny` are JS regex arrays; **`deny` wins over `allow`**, both beat the classifier
112
- - `denyPaths` are plain paths you declare **protected** — touches trigger a terminal ask you adjudicate (non-interactive → deny); the classifier never learns the paths themselves, only that they exist. `grep`/`find`/`ls` compare their whole **search scope**: an omitted `path` (pi's default: the current directory) or a parent directory of a declared path triggers the ask as well. A fresh install pre-fills a **starter list** (`~/.ssh/`, `~/.gnupg`, `~/.mc`, shell rc/profile files), active from the first session after the initial run (any config change applies to new sessions) — a pre-filled *user declaration*, not a built-in floor: edit or empty it freely, add your own (`~/Documents/private`, …) alongside; existing configs are never rewritten
121
+ - `denyPaths` are plain paths you declare **protected** — touches trigger a terminal ask you adjudicate (non-interactive → deny); the classifier never learns the paths themselves, only that they exist. `grep`/`find`/`ls` compare their whole **search scope**: an omitted `path` (pi's default: the current directory) or a parent directory of a declared path triggers the ask as well. A fresh install pre-fills a **starter list** (`~/.ssh/`, `~/.gnupg`, `~/.mc`, shell rc/profile files)
122
+ - `ignoreTools` names uncovered tools (`todo`, `web_search`, MCP/custom tools) that skip adjudication — **allow with zero model calls**; entries naming covered tools (`bash`/`read`/`write`/`edit`/`grep`/`find`/`ls`/`powershell`) are inert: those stay governed by the deny floor and your allow/deny rules, and the self-protection layer always runs first. A fresh install pre-fills a **starter list** (`todo`, `ask_user_question`, `memory_write`, `memory_search` — observed harmless across the 1265-verdict production audit). Caveat: an exempted tool loses the classifier's `denyPaths` existence-hint vigilance (uncovered tools never hit the path extractor anyway)
113
123
  - `builtinDenyFloor: false` turns off the built-in danger/path floor (your risk; the self-protection layer below always stays on)
114
124
  - `classifierModel` pins the classifier model, e.g. `"zai/glm-5.3-flash:low"` (thinking suffix supported; default: session model with thinking off)
115
125
  - `classifierModel: "typesafe/jev-latest"` opts into the bundled **jev decisions adapter** — gray-zone verdicts via TypeSafe's jev (OpenRouter by default, or TypeSafe's official API directly with `PI_VERDICT_JEV_TRANSPORT=typesafe`); experimental, see [ADR-0003](docs/adr/0003-jev-decisions-adapter.md)
116
126
  - `audit: true` records every **gray-zone adjudication** (the full transcript sent to the classifier, its raw response, the parsed verdict) as JSONL under `~/.pi/agent/verdicts/<sessionId>.jsonl` — one file per session, the 20 most recent kept. Interactive asks also record your answer (`userAnswer` ground truth, written after the confirm resolves), and protected-path asks are recorded too (#62); rule allow/deny stays unaudited. Local-only and full-fidelity (protected-path plaintext may appear — it never leaves your machine; [ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md) boundary note); the agent can neither read nor write the directory. `/automode` shows the audit state and path while on
117
- - `notifyAllows: true` notifies on every **classifier allow** (reason + action line — e.g. jev's probability breakdown); default `false` keeps passes silent. Mechanical passes (your own allow rules, protected-path confirms) never notify; shadow-cache annotations stay debug-only; with both switches on the notification appears once
118
- - `classifierMinConfidence` (optional, [ADR-0004](docs/adr/0004-classifier-fallback-cascade.md)) sets the **confidence floor**: a jev verdict below it is demoted — cascaded to `classifierFallbackModel` if set (`shadow` = the second layer records its opinion and you are asked; `enforce` = the second layer adjudicates, except a demoted deny can never be auto-allowed), otherwise asked of you directly. At/above the floor the first layer is autonomous. A natural pairing: jev first + a haiku-class fallback
127
+ - `notifyAllows: true` notifies on every **classifier allow** (reason + action line — e.g. jev's probability breakdown); default `false` keeps passes silent. Mechanical passes (your own allow rules, protected-path confirms) never notify; with both switches on the notification appears once
128
+ - `classifierMinConfidence` (optional, [ADR-0004](docs/adr/0004-classifier-fallback-cascade.md)) sets the **confidence floor**: a jev verdict below it is demoted — cascaded to `classifierFallbackModel` if set (`enforce`, the default = the second layer adjudicates; a demoted **deny or ask** can never be auto-relaxed to an allow; a fail-closed layer emitted no verdict, so its rescue stands; `shadow` = records its opinion only and you are asked — `/automode` hints the activation switch), otherwise asked of you directly. At/above the floor the first layer is autonomous. The floor applies to decisions models only — with an LLM classifier it is inert, and a one-time warning says so. A natural pairing: jev first + a haiku/flash-class fallback
119
129
 
120
130
  No built-in allowlist — every "always allow" claim is yours ([why](docs/configuration.md#why-no-built-in-allowlist)). Full reference: [docs/configuration.md](docs/configuration.md).
121
131
 
@@ -134,16 +144,16 @@ No built-in allowlist — every "always allow" claim is yours ([why](docs/config
134
144
  - **Hosts**: pi only. On omp the setting warns and falls back to the session model; and it must never be selected as the session model (no text generation — selecting it warns)
135
145
  - **Escape hatch**: `PI_VERDICT_JEV_URL` overrides the active transport's endpoint (OpenRouter's is an alpha API)
136
146
 
137
- jev's calibrated confidence is exactly what the confidence floor keys on — pair it with a second layer (`"classifierMinConfidence": 50, "classifierFallbackModel": "anthropic/claude-haiku-4-5"`) so its low-confidence calls go to a deeper model instead of standing ([ADR-0004](docs/adr/0004-classifier-fallback-cascade.md)).
147
+ jev's calibrated confidence is exactly what the confidence floor keys on — pair it with a second layer (`"classifierMinConfidence", "classifierFallbackModel"`) so its low-confidence calls go to a deeper model instead of standing ([ADR-0004](docs/adr/0004-classifier-fallback-cascade.md)).
138
148
 
139
149
  ### Self-protection (the gate guards itself — [ADR-0001](docs/adr/0001-self-protection-layer.md))
140
150
 
141
151
  The gate's own files — the config and the installed extension copy — are **user-editable only**: writes from inside the gate hard-deny (reads pass); your editor never passes through the gate, the sudoers/visudo precedent.
142
152
 
143
153
  - **Not disableable by any config** — `builtinDenyFloor: false` and user `allow` rules cannot touch this layer
144
- - **Tamper detection** as the backstop: watched files are snapshotted at `session_start` and re-verified before every verdict — a changed extension copy is auto-restored and the session goes fail-closed; a changed config gets one explicit keep/restore confirm ([ADR-0001](docs/adr/0001-self-protection-layer.md) for the differential-disposal rationale)
154
+ - **Tamper detection** as the backstop: watched files are snapshotted at `session_start` and re-verified before every verdict — a changed extension copy is auto-restored and the session goes fail-closed; a changed config gets one explicit keep/restore confirm when a UI is available (headless auto-restores, same as the extension copy; [ADR-0001](docs/adr/0001-self-protection-layer.md) for the differential-disposal rationale)
145
155
 
146
- Requires pi ≥ 0.84. Works in interactive and non-interactive (`-p`/json/rpc) sessions; in non-interactive modes `ask` degrades to `deny`.
156
+ ---
147
157
 
148
158
  ## How it compares
149
159
 
@@ -160,6 +170,10 @@ Honest framing: pi-automode and pi-verdict have **converged on the same architec
160
170
 
161
171
  ## Pipeline
162
172
 
173
+ ![pi-verdict security gate — tool-call adjudication pipeline](https://cdn.jsdelivr.net/gh/jesset/pi-verdict@main/docs/diagrams/security-pipeline.en.svg)
174
+
175
+ *Diagram source & regeneration: [docs/diagrams/](docs/diagrams/README.md). Pipeline as of v0.12 — the ASCII version below is the text-faithful equivalent.*
176
+
163
177
  ```
164
178
  tool_call
165
179
  │
@@ -173,6 +187,7 @@ tool_call
173
187
  │ ├─ your rules: user deny beats user allow
174
188
  │ ├─ denyPaths (ADR-0002): protected paths → terminal ask,
175
189
  │ │ before user allow; classifier sees an existence hint only
190
+ │ ├─ ignoreTools: your declared uncovered tools → allow, zero model calls
176
191
  │ └─ no built-in allowlist — every "always allow" claim is yours to make
177
192
  │
178
193
  ├─ 2. Gray zone → model classifier (defaults to session model — "self-reflection")
@@ -185,7 +200,6 @@ tool_call
185
200
  ├─ deny → block, reason returned to the agent
186
201
  └─ ask → human confirm; non-interactive modes degrade to deny
187
202
 
188
- [shadow cache] observe-only telemetry alongside 2/3, never changes a verdict
189
203
  ```
190
204
 
191
205
  **fail-closed**: classifier exception / timeout (25s) / contract violation → deny. Never silently allow.
@@ -194,7 +208,7 @@ tool_call
194
208
 
195
209
  Design decisions here are settled by measurement, and the lab notes ship with the repo:
196
210
 
197
- - [`research/cache-sim`](research/cache-sim/README.md) — replayed 1.2k+ real classifier verdicts to measure verdict-cache hit rate (**3.2%** → cache deferred, shadow-mode telemetry built instead)
211
+ - [`research/cache-sim`](research/cache-sim/README.md) — replayed 1.2k+ real classifier verdicts to measure verdict-cache hit rate (**3.2%** → serving cache declined; the runtime shadow telemetry built afterwards measured 3.3% and was later removed too, #73)
198
212
  - [`research/thinking-param-blackhole.md`](research/thinking-param-blackhole.md) — three-layer forensic root-cause of thinking models burning the classifier budget; why the fix is `thinkingEnabled: false`
199
213
  - [`research/rule-engine-sim`](research/rule-engine-sim/README.md) — measured a tree-sitter rule-engine port against 746 real bash calls (**absorbs 0 gray calls**) and rejected it
200
214
  - [`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) — the competitive landscape this README's positioning is checked against
@@ -210,7 +224,6 @@ Design decisions here are settled by measurement, and the lab notes ship with th
210
224
  - AGENTS.md is not passed to the classifier as downweighted intent evidence (Claude Code does this)
211
225
  - parallel gray-zone calls are adjudicated serially
212
226
  - self-reflection means the session model adjudicates — point `--auto-mode-model` at a lighter model if verdict latency/cost matters (open question tracked in the issue tracker)
213
- - shadow cache is observe-only by decision; the serving switch is a one-line change once measured hit rates justify it
214
227
  - `denyPaths` bash extraction is token-level ([ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md)): command substitution, base64-embedded paths and external script contents produce no hit signal — those calls fall back to the classifier's existence-hint vigilance. MCP and custom tools bypass the extractor entirely (their gray-zone adjudication still carries the hint). Path normalization is base-tier only (ADR-0002): a nonexistent target written through a symlinked directory rebuilds no real form and produces no hit — that indirection falls to the hint vigilance too (the ancestor-rebuilding tier applies to the self-protection layer and the sensitivity floor, not denyPaths). Honest framing, same as the self-protection substring precedent: the deterministic layer is obfuscatable, which is exactly why a hit routes to *you* rather than silently deciding
215
228
  - `denyPaths` bash tokens contain no spaces: a *declared* path containing spaces cannot be spelled in a bash command in a way the extractor sees — `cat "/path with space/x"` splits into two tokens and never hits (file tools still hit, their path is not tokenized). A glob covering the final segment of a base (`cat /proj/pers*` against `denyPaths: ["/proj/personal"]`) also misses — the base's own name never appears literally. A recursive search issued from a shell misses in both spellings — no path argument (defaults to the cwd, e.g. a bare `rg foo`) or a parent-directory argument (`rg foo <parent-of-a-declared-path>`): an argument-less command contributes no token at all and bash tokens otherwise compare one-directionally, while the file tools' bidirectional subtree compare covers the same shapes issued through `grep`/`find`/`ls`. All three holes fall back to the classifier's existence hint, alongside substitution/base64 above
216
229
  - self-protection bash matching is substring regex — obfuscatable; the tamper-detection backstop catches within-session bypasses, but a cross-session baseline (hash + change confirmation at startup, incl. upgrade UX) is phase 2 per [ADR-0001](docs/adr/0001-self-protection-layer.md)
@@ -225,7 +238,7 @@ The name: the three-state **verdict** is the core concept. The UX keeps `/automo
225
238
  ```bash
226
239
  bun install
227
240
  bun run typecheck
228
- bun test # offline stub tests: self-protection, tamper detection, deny floor, user rules, denyPaths, bypass regression, classifier retry, shadow cache, commands, toggle shortcut
241
+ bun test # offline stub tests: self-protection, tamper detection, deny floor, user rules, denyPaths, bypass regression, classifier retry, commands, toggle shortcut
229
242
  ```
230
243
 
231
244
  Issue tracker and decision records live in the GitHub issues ("map" issue #1 indexes them).
package/README.zh-CN.md CHANGED
@@ -6,44 +6,43 @@
6
6
  [![npm](https://img.shields.io/npm/v/pi-verdict)](https://www.npmjs.com/package/pi-verdict)
7
7
  [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://pi.dev)
8
8
 
9
- **pi-verdict 是 [pi](https://pi.dev) 的 Claude Code 风格的 Auto mode 式的极简权限门禁:每次工具调用执行前先过检查——放行、拦截,或先问你。**
9
+ **pi-verdict 是 [pi](https://pi.dev) 的极简权限门禁,灵感来自 Claude Code 的 auto mode:每次工具调用执行前先过检查——放行、拦截,或先问你。**
10
10
 
11
- - 只有1k行左右的极简代码
11
+ - 极简——核心单文件约 2k 行(另含一个小型 jev 适配器)
12
12
  - 内置危险规则与你的 allow/deny 规则以零延迟先行裁决明确情形
13
13
  - 其余交给携带会话上下文的模型分类器
14
- - 任何不确定或失败一律 fail-closed, 绝不静默放行
15
- - 自我保护: 防止被窥探和篡改
14
+ - 任何不确定或失败一律 fail-closed,绝不静默放行
15
+ - 自保护:门禁守护自身,防窥探与篡改
16
16
 
17
17
  ## 问题
18
18
 
19
19
  pi 没有内置的逐次权限确认——每次工具调用都以 pi 进程自身的权限直接执行([pi 安全文档](https://pi.dev/docs/latest/security))。
20
20
 
21
- pi-verdict 补上这道缺失的门禁, 由模型基于上下文和你的意图判定是否可以运行.
21
+ pi-verdict 补上这道缺失的门禁,由模型基于上下文和你的意图判定是否可以运行。
22
22
 
23
23
  ## 为什么是三态
24
24
 
25
- **verdict 是裁决,不是开关。** 本品类的分类器大多只输出二值 allow/block。三态有意义的地方在:`ask` 把真正含糊的动作转交人类确认(非交互会话中降级为 `deny`),「不确定」永远不会静默变成「放行」——目标是安全的自动化而非最大的自动化:审批疲劳与静默危险执行都是危险。
25
+ **verdict 是裁决,不是开关。** 本品类的分类器大多只输出二值 allow/block。三态有意义的地方在:`ask` 把真正含糊的动作转交人类确认(非交互会话中降级为 `deny`),「不确定」永远不会静默变成「放行」——目标是安全的自动化而非最大的自动化:审批疲劳与静默危险执行都是败因。
26
26
 
27
27
  ## 设计原则
28
28
 
29
- - **Fail closed**——不确定产生摩擦,绝不产生许可。
29
+ - **Fail closed**——不确定产生摩擦,绝不产生许可。
30
30
  - **确定性 floor 先于 AI**——硬 deny 永不被分类器或用户 allow 规则覆盖。
31
- - **语义优先于语法**——分类器判定的是动作**做什么可能会产生什么安全影响**,而不是命令有多长。
32
- - **是判断,不是证明**——分类器的 `allow` 是有依据的判断;floor 的存在正因为它仅此而已。
33
- - **最小化可信输入**——transcript 不含工具结果(#22),分类器零路径明文(ADR-0002)。
34
- - **规范化身份**——词法 + realpath 双形匹配;「看起来在项目内」的路径不因此被信任(#20/#21)。
31
+ - **语义优先于语法**——分类器判定的是动作**做什么可能会产生什么安全影响**,而不是命令有多长。
32
+ - **是判断,不是证明**——分类器的 `allow` 是有依据的判断;floor 的存在正因 allow 仅是判断。
33
+ - **最小化可信输入**——transcript 不含工具结果(#22),分类器零路径明文(ADR-0002)。
34
+ - **规范化身份**——词法 + realpath 双形匹配;「看起来在项目内」的路径不因此被信任(#20/#21)。
35
35
  - **门禁守护自身**——任何配置都关不掉的自保护层(ADR-0001)。
36
- - **是权限门禁,不是沙箱**——请在上面叠加 OS 级隔离;本门禁不替代它。
37
-
38
- 完整表述见 [docs/security-principles.md](docs/security-principles.md):
36
+ - **是权限门禁,不是沙箱**——请在上面叠加 OS 级隔离;本门禁不替代它。
39
37
 
38
+ 完整表述见 [docs/security-principles.md](docs/security-principles.md)。
40
39
 
41
40
  ## 截图
42
41
 
43
- ![演示:受保护路径 ask 被拒绝](docs/demo.gif)
42
+ ![演示:受保护路径 ask 被拒绝](https://raw.githubusercontent.com/jesset/pi-verdict/main/docs/demo.gif)
44
43
 
45
- ![Automode Status](docs/images/status.png)
46
- ![Ask Permission](docs/images/asked.png)
44
+ ![Automode Status](https://raw.githubusercontent.com/jesset/pi-verdict/main/docs/images/status.png)
45
+ ![Ask Permission](https://raw.githubusercontent.com/jesset/pi-verdict/main/docs/images/asked.png)
47
46
 
48
47
  ## 快速开始
49
48
 
@@ -59,32 +58,34 @@ pi --extension ./extensions/pi-verdict.ts
59
58
 
60
59
  ```
61
60
 
61
+ 需要 pi ≥ 0.84。交互与非交互(`-p`/json/rpc)会话均支持;非交互模式下 `ask` 降级为 `deny`。
62
+
62
63
  ### 宿主
63
64
 
64
- pi-verdict 同时支持 [pi](https://github.com/badlogic/pi-mono) 与 [oh-my-pi](https://github.com/can1357/oh-my-pi)(omp)——扩展按自身安装位置自锚定到所在宿主的目录树,双宿主并存的机器上跟随扩展副本自身的位置。omp 18 下分类器的模型调用经 pi-ai compat API 降级(仍然 fail-closed)。细节见 [docs/configuration.md](docs/configuration.md#host-notes-pi-and-oh-my-pi)。
65
+ pi-verdict 同时支持 [pi](https://github.com/badlogic/pi-mono) 与 [oh-my-pi](https://github.com/can1357/oh-my-pi)(omp)——扩展按自身安装位置自锚定到所在宿主的目录树,双宿主并存的机器上跟随扩展副本自身的位置。omp 18 下分类器的模型调用回退到 pi-ai compat API(仍然 fail-closed)。细节见 [docs/configuration.md](docs/configuration.md#host-notes-pi-and-oh-my-pi)。
65
66
 
66
67
  | | pi | omp |
67
68
  |---|---|---|
68
69
  | 安装 | `pi install npm:pi-verdict` | `omp plugin install npm:pi-verdict` |
69
- | 扩展副本 | `~/.pi/agent/extensions/` | `~/.omp/plugins/node_modules/pi-verdict/`(omp 18.1+;≤18.0 在 `agent/` 下) |
70
+ | 扩展副本 | `~/.pi/agent/extensions/` | `~/.omp/plugins/node_modules/pi-verdict/`(omp 18.1+;≤18.0 在 `agent/` 下) |
70
71
  | 用户规则 | `~/.pi/agent/config/pi-verdict.json` | `~/.omp/agent/config/pi-verdict.json` |
71
72
  | 凭据文件(S0 硬 deny) | `~/.pi/agent/auth.json` | `~/.omp/agent/auth.json` |
72
73
 
73
- - `/automode` —— 显示当前状态:开/关 + 本会话影子缓存统计
74
+ - `/automode` —— 显示当前状态:开/关
74
75
  - `/automode on`
75
76
  - `/automode off`
76
- - `ctrl+shift+a` —— 静默切换主开关(footer 始终显示为唯一反馈;键位可经 `toggleShortcut` 重绑或禁用)
77
+ - `ctrl+shift+a` —— 静默切换主开关(footer 始终显示为唯一反馈;键位可经 `toggleShortcut` 重绑或禁用)
77
78
  - footer 恒显 `auto mode on`(绿色)/ `auto mode off`(黄色)
78
79
 
79
80
  | 配置 | 默认 | 说明 |
80
81
  |---|---|---|
81
- | `--auto-mode` / `--no-auto-mode` | 开 | 总开关 |
82
+ | `--auto-mode` / `--no-auto-mode` | 开 | 主开关 |
82
83
  | `--auto-mode-model provider/id` | 会话模型 | 分类器模型(默认"自省") |
83
84
  | `--auto-mode-debug` | 关 | 全量裁决通知 |
84
85
  | `PI_AUTO_MODE_MODEL` | — | 模型配置的环境变量形式 |
85
86
  | `PI_AUTO_MODE_DEBUG=1` | 关 | 调试的环境变量形式(flag 优先) |
86
87
 
87
- ### 用户自定义规则(`~/.pi/agent/config/pi-verdict.json`)
88
+ ### 用户自定义规则(`pi-verdict.json`)
88
89
 
89
90
  ```json
90
91
  {
@@ -99,6 +100,12 @@ pi-verdict 同时支持 [pi](https://github.com/badlogic/pi-mono) 与 [oh-my-pi]
99
100
  "~/.zshrc",
100
101
  "~/.bashrc"
101
102
  ],
103
+ "ignoreTools": [
104
+ "todo",
105
+ "ask_user_question",
106
+ "memory_write",
107
+ "memory_search"
108
+ ],
102
109
  "builtinDenyFloor": true,
103
110
  "classifierModel": null,
104
111
  "toggleShortcut": "ctrl+shift+a",
@@ -106,61 +113,66 @@ pi-verdict 同时支持 [pi](https://github.com/badlogic/pi-mono) 与 [oh-my-pi]
106
113
  "notifyAllows": false,
107
114
  "classifierMinConfidence": null,
108
115
  "classifierFallbackModel": null,
109
- "classifierFallbackMode": "shadow"
116
+ "classifierFallbackMode": "enforce"
110
117
  }
111
118
  ```
112
119
 
113
- - `allow`/`deny` 为 JS 正则数组;**`deny` 优先于 `allow`**,两者都优先于分类器
114
- - `denyPaths` 是你声明**受保护**的普通路径列表:触碰触发**终局 ask** 由你裁决(非交互降级 deny);分类器只被告知路径**存在**,路径明文永不出本机。`grep`/`find`/`ls` 按**整个搜索范围**比较:省略 `path`(pi 默认:当前目录)或传入位于声明路径之上的父目录,同样触发 ask。全新安装会预填一份**入门列表**(`~/.ssh/`、`~/.gnupg`、`~/.mc`、shell rc/profile 文件),自初次运行后的第一个会话起生效(一切配置变更均自新会话生效)——它是预填的*用户声明*而非内置 floor:可随意增删清空,也可与自己的路径(`~/Documents/private`、……)并列;既有配置永不被改写
115
- - `builtinDenyFloor: false` 整体关闭内置危险/路径拦截(风险自担;下方自保护层永远开启)
116
- - `classifierModel` 指定分类器模型,如 `"zai/glm-5.3-flash:low"`(支持思考后缀;缺省 = 会话模型且显式关思考)
117
- - `classifierModel: "typesafe/jev-latest"` 启用随包的 **jev 决策适配器**——灰区裁决经 TypeSafe jev 完成(默认 OpenRouter,或 `PI_VERDICT_JEV_TRANSPORT=typesafe` 直连官方 API);实验性质,详见 [ADR-0003](docs/adr/0003-jev-decisions-adapter.md)
118
- - `audit: true` 把每次**灰区裁决**(发给分类器的完整转录、其原始响应、解析出的裁决)以 JSONL 记录到 `~/.pi/agent/verdicts/<sessionId>.jsonl`——按会话一分文件,保留最近 20 个。交互式 ask 还会记录你的应答(`userAnswer` ground truth,确认结束后落盘),protected-path ask 也入审计(#62);规则 allow/deny 仍不入。仅存本机且全保真(受保护路径明文可能出现——永不出本机;[ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md) 边界注);agent 对该目录读写双拒。开启时 `/automode` 会显示审计状态与路径
119
- - `notifyAllows: true` 对每次 **classifier 放行**发通知(reason + action 行——如 jev 的概率分解);默认 `false` 保持放行静默。机械放行(你自己的 allow 规则、protected-path 确认)永不通知;shadow 标注仍属 debug;两开关同开时通知只出现一次
120
- - `classifierMinConfidence`(可选,[ADR-0004](docs/adr/0004-classifier-fallback-cascade.md))设定**置信地板**:低于它的 jev 裁决被降级——配置了 `classifierFallbackModel` 则级联(`shadow` = 第二层只记录意见、由你裁决;`enforce` = 第二层全权裁决,但降级 deny 永不被自动翻成 allow),否则直接问你。不低于地板时第一层自主。天然搭配:jev 打头 + haiku 级兜底
120
+ - `allow`/`deny` 为 JS 正则数组;**`deny` 优先于 `allow`**,两者都优先于分类器
121
+ - `denyPaths` 是你声明**受保护**的普通路径列表:触碰触发**终局 ask** 由你裁决(非交互降级 deny);分类器只被告知路径**存在**,路径明文永不出本机。`grep`/`find`/`ls` 按**整个搜索范围**比较:省略 `path`(pi 默认:当前目录)或传入位于声明路径之上的父目录,同样触发 ask。全新安装会预填一份**入门列表**(`~/.ssh/`、`~/.gnupg`、`~/.mc`、shell rc/profile 文件)
122
+ - `ignoreTools` 列出规则未覆盖的工具(`todo`、`web_search`、MCP/自定义工具):**直接放行、零模型调用**;列出已覆盖工具(`bash`/`read`/`write`/`edit`/`grep`/`find`/`ls`/`powershell`)的条目无效:它们仍受 deny floor 与你的 allow/deny 规则约束,自保护层也永远先行。全新安装会预填一份**入门列表**(`todo`、`ask_user_question`、`memory_write`、`memory_search`——来自项目 1265 条生产审计的观察)。注意:被豁免的工具失去分类器对 `denyPaths` 的存在性话术警戒(未覆盖工具本就不进路径提取器)
123
+ - `builtinDenyFloor: false` 整体关闭内置危险/路径拦截(风险自担;下方自保护层永远开启)
124
+ - `classifierModel` 指定分类器模型,如 `"zai/glm-5.3-flash:low"`(支持思考后缀;缺省 = 会话模型且显式关思考)
125
+ - `classifierModel: "typesafe/jev-latest"` 启用随包的 **jev 决策适配器**——灰区裁决经 TypeSafe jev 完成(默认 OpenRouter,或 `PI_VERDICT_JEV_TRANSPORT=typesafe` 直连官方 API);实验性质,详见 [ADR-0003](docs/adr/0003-jev-decisions-adapter.md)
126
+ - `audit: true` 把每次**灰区裁决**(发给分类器的完整转录、其原始响应、解析出的裁决)以 JSONL 记录到 `~/.pi/agent/verdicts/<sessionId>.jsonl`——按会话一分文件,保留最近 20 个。交互式 ask 还会记录你的应答(`userAnswer` ground truth,确认结束后落盘),protected-path ask 也入审计(#62);规则 allow/deny 仍不入。仅存本机且全保真(受保护路径明文可能出现——永不出本机;[ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md) 边界注);agent 对该目录读写双拒。开启时 `/automode` 会显示审计状态与路径
127
+ - `notifyAllows: true` 对每次 **classifier 放行**发通知(reason + action 行——如 jev 的概率分解);默认 `false` 保持放行静默。机械放行(你自己的 allow 规则、protected-path 确认)永不通知;两开关同开时通知只出现一次
128
+ - `classifierMinConfidence`(可选,[ADR-0004](docs/adr/0004-classifier-fallback-cascade.md))设定**置信地板**:低于它的 jev 裁决被降级——配置了 `classifierFallbackModel` 则级联(`enforce`,默认 = 第二层全权裁决;例外:降级的 **deny 与 ask** 永不被自动放宽为 allow;fail-closed 未产生裁决,其获救裁决照常生效;`shadow` = 只记录意见、由你裁决——`/automode` 会提示激活开关),否则直接问你。不低于地板时第一层自主。地板仅作用于 decisions 模型——LLM 分类器下不生效,会有一次中性警告提示。天然搭配:jev 在前 + haiku/flash 级回退
121
129
 
122
- 没有内置白名单——每一条「永远放行」声明都归你([为什么](docs/configuration.md#why-no-built-in-allowlist))。完整参考:[docs/configuration.md](docs/configuration.md)。
130
+ 没有内置白名单——每一条「永远放行」声明都归你([为什么](docs/configuration.md#why-no-built-in-allowlist))。完整参考:[docs/configuration.md](docs/configuration.md)。
123
131
 
124
132
  ### Jev 决策后端(实验性——[ADR-0003](docs/adr/0003-jev-decisions-adapter.md))
125
133
 
126
- 1. 安装含适配器的版本( v0.8 及以上): `pi install npm:pi-verdict`
127
- 2. 选一条 transport(两条走同一 decisions wire 契约):
128
- - **OpenRouter(默认)**: pi 内执行 `/login openrouter`,或 shell 里 `export OPENROUTER_API_KEY=sk-or-v1...`
129
- - **TypeSafe 直连(官方 v1 API)**: 在 console.typesafe.ai 自助发 key,然后 `export TYPESAFE_API_KEY=apikey_...` 并 `export PI_VERDICT_JEV_TRANSPORT=typesafe`
130
- 3. 把分类器指到 jev(新会话生效)
134
+ 1. 安装含适配器的版本(v0.8 及以上):`pi install npm:pi-verdict`
135
+ 2. 选一条 transport(两条走同一 decisions wire 契约):
136
+ - **OpenRouter(默认)**:pi 内执行 `/login openrouter`,或 shell 里 `export OPENROUTER_API_KEY=sk-or-v1...`
137
+ - **TypeSafe 直连(官方 v1 API)**:在 console.typesafe.ai 自助发 key,然后 `export TYPESAFE_API_KEY=apikey_...` 并 `export PI_VERDICT_JEV_TRANSPORT=typesafe`
138
+ 3. 将分类器指向 jev(新会话生效)
131
139
  - 持久:在 pi 之外编辑 `~/.pi/agent/config/pi-verdict.json` 并设置 `{ "classifierModel": "typesafe/jev-latest" }`
132
- - 或者临时试一把:`PI_AUTO_MODE_MODEL=typesafe/jev-latest pi`
140
+ - 或者临时试用一次:`PI_AUTO_MODE_MODEL=typesafe/jev-latest pi`
133
141
 
134
- **限制**:
135
- - **Transport**: OpenRouter decisions(默认)或 TypeSafe 直连——TypeSafe 侧单次成本显示 $0(其 API 不返回 cost)
136
- - **宿主**:仅支持pi。omp 上该设置会警告并回退会话模型。也绝不能选作会话主模型(不生成文本,选中即警告)
137
- - **逃生口**:`PI_VERDICT_JEV_URL` 可覆盖当前 transport 的端点(OpenRouter 侧为 alpha 接口)
142
+ **限制**:
143
+ - **Transport**:OpenRouter decisions(默认)或 TypeSafe 直连——TypeSafe 侧单次成本显示 $0(其 API 不返回 cost)
144
+ - **宿主**:仅支持 pi。omp 上该设置会警告并回退会话模型。也绝不能选作会话主模型(不生成文本,选中即警告)
145
+ - **逃生口**:`PI_VERDICT_JEV_URL` 可覆盖当前 transport 的端点(OpenRouter 侧为 alpha 接口)
138
146
 
139
- jev 的校准 confidence 正是置信地板的判定依据——搭配第二层使用(`"classifierMinConfidence": 50, "classifierFallbackModel": "anthropic/claude-haiku-4-5"`),让低置信调用交给更深的模型而非直接生效([ADR-0004](docs/adr/0004-classifier-fallback-cascade.md))。
147
+ jev 的校准 confidence 正是置信地板的判定依据——搭配第二层使用(`"classifierMinConfidence", "classifierFallbackModel"`),让低置信调用交由更深的模型复裁,而非就地生效([ADR-0004](docs/adr/0004-classifier-fallback-cascade.md))。
140
148
 
141
149
  ### 自保护(门禁守护自身——[ADR-0001](docs/adr/0001-self-protection-layer.md))
142
150
 
143
- 门禁自身的文件——配置与扩展安装副本——**仅用户可改**:门禁之内的写入一律硬 deny(读放行);你的编辑器修改不经门禁,最近的同构先例是 sudoers 必须经 visudo。
151
+ 门禁自身的文件——配置与扩展安装副本——**仅用户可改**:门禁之内的写入一律硬 deny(读放行);你的编辑器修改不经门禁,同类先例是 sudoers 必须经 visudo。
144
152
 
145
153
  - **不可经任何配置关闭**——`builtinDenyFloor: false` 与用户 `allow` 规则都动不了这一层
146
- - **变更检测**作纵深兜底:受保护文件在 `session_start` 快照、每次裁决前复核——扩展副本被改 → 自动还原 + 本会话 fail-closed;配置被改 → 一次明确的双选确认(差分处置的完整语义见 [ADR-0001](docs/adr/0001-self-protection-layer.md))
154
+ - **变更检测**作纵深兜底:受保护文件在 `session_start` 快照、每次裁决前复核——扩展副本被改 → 自动还原 + 本会话 fail-closed;配置被改 → 有 UI 时一次明确的双选确认(无 UI 时与扩展副本同样自动还原;差分处置的完整语义见 [ADR-0001](docs/adr/0001-self-protection-layer.md))
147
155
 
148
- 需要 pi ≥ 0.84。交互与非交互(`-p`/json/rpc)会话均支持;非交互模式下 `ask` 降级为 `deny`。
156
+ ---
149
157
 
150
158
  ## 与品类对比
151
159
 
152
160
  | | 三态裁决 | 分类器携带上下文 | fail 方向 | 运行时依赖 |
153
161
  |---|---|---|---|---|
154
- | **pi-verdict** | ✅ allow / ask / deny | ✅ 近期用户意图 + 工具调用 | **closed**(异常/超时/违约 → deny;非交互 ask → deny) | **0** |
155
- | [@czottmann/pi-automode](https://github.com/czottmann/pi-automode) | 规则三态,分类器二态 | ✅ 预算化 transcript | closed | 1 |
162
+ | **pi-verdict** | ✅ allow / ask / deny | ✅ 近期用户意图 + 工具调用 | **closed**(异常/超时/违约 → deny;非交互 ask → deny) | **0** |
163
+ | [@czottmann/pi-automode](https://github.com/czottmann/pi-automode) | 规则三态,分类器二态 | ✅ 预算内裁剪的 transcript | closed | 1 |
156
164
  | [@zhushanwen/pi-permission](https://www.npmjs.com/package/@zhushanwen/pi-permission) | ✅(outcome) | ❌ 单轮无上下文 | closed(→ ask) | 4 |
157
165
  | [@gotgenes/pi-permission-system](https://github.com/gotgenes/pi-packages) | ✅ 纯确定性 | —(无内置分类器) | closed | 3 |
158
166
 
159
- 完整全景:[`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) · 与最近架构亲缘的收敛分析:[`research/pi-automode-convergence.md`](research/pi-automode-convergence.md)。
167
+ 完整全景:[`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) · 与最近架构亲缘的收敛分析:[`research/pi-automode-convergence.md`](research/pi-automode-convergence.md)。
168
+
169
+ 诚实地说:pi-automode 与 pi-verdict 在**架构上已收敛**(deny floor → 用户规则 → 分类器,fail-closed——见收敛分析)。这里仍然不同的是:分类器能说 `ask`(运行时人工介入,而非仅由规则预声明)、内置 floor 可以关(`builtinDenyFloor`——用户主权)、任何配置都关不掉的自保护层([ADR-0001](docs/adr/0001-self-protection-layer.md)——门禁完整性)、零依赖的[可通读单文件](extensions/pi-verdict.ts)(仍刻意单文件)、以及测量的习惯——本仓库每个设计决策都有随库研究背书。
170
+
171
+ ## 判定管线
160
172
 
161
- 诚实地说:pi-automode 与 pi-verdict 在**架构上已收敛**(deny floor → 用户规则 → 分类器,fail-closed——见收敛分析)。这里仍然不同的是:分类器能说 `ask`(运行时人工介入,而非仅由规则预声明)、内置 floor 可以关(`builtinDenyFloor`——用户主权)、任何配置都关不掉的自保护层([ADR-0001](docs/adr/0001-self-protection-layer.md)——门禁完整性)、零依赖的[可通读单文件](extensions/pi-verdict.ts)(仍刻意单文件)、以及测量的习惯——本仓库每个设计决策都有随库研究背书。
173
+ ![pi-verdict 安全门禁——工具调用判定管线](https://cdn.jsdelivr.net/gh/jesset/pi-verdict@main/docs/diagrams/security-pipeline.zh.svg)
162
174
 
163
- ## 管线
175
+ *图源与再生成:[docs/diagrams/](docs/diagrams/README.md)。管线基准:v0.12——下方 ASCII 为文本等价版。*
164
176
 
165
177
  ```
166
178
  tool_call
@@ -175,6 +187,7 @@ tool_call
175
187
  │ ├─ 用户规则:deny 优先于 allow
176
188
  │ ├─ denyPaths(ADR-0002):受保护路径 → 终局 ask,先于用户 allow;
177
189
  │ │ 分类器只见存在性话术
190
+ │ ├─ ignoreTools:用户声明的未覆盖工具 → 直接放行,零模型调用
178
191
  │ └─ 无内置白名单 —— 「永远放行」的声明由你自己做
179
192
  │
180
193
  ├─ 2. 灰区 → 模型分类器(默认继承会话模型 —— "自省")
@@ -187,47 +200,45 @@ tool_call
187
200
  ├─ deny → 拦截,理由回传 agent
188
201
  └─ ask → 人工确认;非交互模式降级为 deny
189
202
 
190
- [影子缓存] observe-only 遥测,与 2/3 并行,永不改变裁决
191
203
  ```
192
204
 
193
- **fail-closed**:分类器异常 / 超时(25s)/ 输出违反契约 → 拦截,绝不静默放行。
205
+ **fail-closed**:分类器异常 / 超时(25s)/ 输出违反契约 → 拦截,绝不静默放行。
194
206
 
195
- ## 证据驱动,不靠直觉
207
+ ## 证据驱动,不靠直觉
196
208
 
197
- 这里的设计决策用测量收敛,实验记录随仓库发布:
209
+ 这里的设计决策以测量定案,实验记录随仓库发布:
198
210
 
199
- - [`research/cache-sim`](research/cache-sim/README.md) —— 回放 1.2k+ 条真实分类器裁决,实测裁决缓存命中率(**3.2%** → 缓存暂缓,改建影子模式遥测)
200
- - [`research/thinking-param-blackhole.md`](research/thinking-param-blackhole.md) —— 思考模型烧尽分类器预算的三层取证,以及为什么修复是 `thinkingEnabled: false`
211
+ - [`research/cache-sim`](research/cache-sim/README.md) —— 回放 1.2k+ 条真实分类器裁决,实测裁决缓存命中率(**3.2%** → 缓存暂缓;其后建成的运行时影子遥测实测 **3.3%**,后来一并移除,#73)
212
+ - [`research/thinking-param-blackhole.md`](research/thinking-param-blackhole.md) —— 思考模型烧尽分类器预算的三层取证,以及为什么修复是 `thinkingEnabled: false`
201
213
  - [`research/rule-engine-sim`](research/rule-engine-sim/README.md) —— 用 746 条真实 bash 调用实测 tree-sitter 规则引擎移植(**灰区吸收 0 条**)并否决
202
214
  - [`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) —— 本 README 定位所对照的竞品全景
203
215
  - [`research/rule-layer-security-audit.md`](research/rule-layer-security-audit.md) —— 规则层绕过测试(8/8 复现 → 0.2.0 架构性修复)
204
216
  - [`research/pi-automode-convergence.md`](research/pi-automode-convergence.md) —— 与 pi-automode 何处真正收敛、何处仍然不同
205
- - [`research/claude-code-classifier-prompts.md`](research/claude-code-classifier-prompts.md) —— Claude Code 分类器设计的结构化还原(基于自托管 Langfuse 观测),本扩展 transcript 契约的血统来源
217
+ - [`research/claude-code-classifier-prompts.md`](research/claude-code-classifier-prompts.md) —— Claude Code 分类器设计的结构化还原(基于自托管 Langfuse 观测),本扩展 transcript 契约的承袭来源
206
218
 
207
219
  ## 状态与限制
208
220
 
209
- - 设计上无内置白名单(见[绕过测试](research/rule-layer-security-audit.md)与[用户规则](#用户规则configpi-verdictjson));allow 配置为空时大多数命令进分类器 —— 延迟敏感可 `--auto-mode-model` 指向轻量模型
210
- - 路径敏感度 floor 只作用于文件类工具:bash 命令串仅匹配危险正则——`cat ~/.ssh/id_rsa` 走分类器而非确定性 S0 拦截(文件工具拼写 `read ~/.ssh/id_rsa` 会拦截)
221
+ - 设计上无内置白名单(见[绕过测试](research/rule-layer-security-audit.md)与[用户自定义规则](#用户自定义规则pi-verdictjson));allow 配置为空时大多数命令进分类器 —— 延迟敏感可 `--auto-mode-model` 指向轻量模型
222
+ - 路径敏感度 floor 只作用于文件类工具:bash 命令串仅匹配危险正则——`cat ~/.ssh/id_rsa` 走分类器而非确定性 S0 拦截(文件工具拼写 `read ~/.ssh/id_rsa` 会拦截)
211
223
  - Windows 下内置 floor 仅覆盖 bash 形态模式——PowerShell 原生危险命令(`Remove-Item -Recurse -Force`、`Invoke-Expression`、`Set-ExecutionPolicy` 等)依赖分类器兜底(fail-closed)
212
224
  - AGENTS.md 未作为降权意图证据传入分类器(Claude Code 有此设计)
213
225
  - 并行灰区调用串行裁决
214
- - 自省意味着会话模型亲自裁决 —— 若延迟/成本敏感,用 `--auto-mode-model` 指向轻量模型(开放问题见 issue tracker)
215
- - 影子缓存按决议仅观察不生效;实测命中率达标后,生效开关是一行改动
216
- - `denyPaths` 的 bash 提取是 token 级([ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md)):命令替换、base64 内嵌路径、外部脚本内容不产生命中信号——这些调用回落到分类器的存在性话术警戒。MCP 与自定义工具完全绕过提取器(其灰区裁决仍带话术)。路径归一化亦为基础档(ADR-0002):经符号链接目录写入尚不存在的目标不重建真实形、不产生命中——该间接路径同样由话术警戒覆盖(祖先重建档只适用于自保护层与路径敏感度 floor,不适用 denyPaths)。诚实表述,与自保护子串正则同例:确定性层可被混淆——这正是命中交由**你**裁决而非静默决定的原因
217
- - `denyPaths` 的 bash token 不含空格:**声明路径本身含空格时**,bash 拼写无法被提取器识别——`cat "/path with space/x"` 被拆成两个 token 永不命中(文件类工具仍命中,其路径不经 token 化)。glob 覆盖基名末段(`denyPaths: ["/proj/personal"]` 时 `cat /proj/pers*`)同样漏过——基名自身从未字面出现。经 shell 发起的递归搜索在两种拼写下都漏过——不带路径参数(默认搜 cwd,如裸 `rg foo`)或带父目录参数(`rg foo <声明路径的父目录>`):无参命令根本不产生 token,带参时 bash token 只做单向比较;同一形状经 `grep`/`find`/`ls` 工具发起则由双向子树比较覆盖。三个洞与上述替换/base64 一样回落到分类器的存在性话术
218
- - 自保护 bash 匹配是子串正则——可被混淆绕过;变更检测兜底覆盖会话内绕过,跨会话基线(启动时哈希比对与变更确认,含升级 UX)按 ADR-0001 为二期
226
+ - 自省意味着会话模型自身裁决 —— 若延迟/成本敏感,用 `--auto-mode-model` 指向轻量模型(开放问题见 issue tracker)
227
+ - `denyPaths` 的 bash 提取是 token 级([ADR-0002](docs/adr/0002-deny-paths-deterministic-ask.md)):命令替换、base64 内嵌路径、外部脚本内容不产生命中信号——这些调用回落到分类器的存在性话术警戒。MCP 与自定义工具完全绕过提取器(其灰区裁决仍带话术)。路径归一化亦为基础档(ADR-0002):经符号链接目录写入尚不存在的目标不重建真实形、不产生命中——该间接路径同样由话术警戒覆盖(祖先重建档只适用于自保护层与路径敏感度 floor,不适用 denyPaths)。诚实表述,与自保护子串正则同例:确定性层可被混淆——这正是命中交由**你**裁决而非静默决定的原因
228
+ - `denyPaths` 的 bash token 不含空格:**声明路径本身含空格时**,bash 拼写无法被提取器识别——`cat "/path with space/x"` 被拆成两个 token 永不命中(文件类工具仍命中,其路径不经 token 化)。glob 覆盖基名末段(`denyPaths: ["/proj/personal"]` 时 `cat /proj/pers*`)同样漏过——基名自身从未字面出现。经 shell 发起的递归搜索在两种拼写下都漏过——不带路径参数(默认搜 cwd,如裸 `rg foo`)或带父目录参数(`rg foo <声明路径的父目录>`):无参命令根本不产生 token,带参时 bash token 只做单向比较;同一形状经 `grep`/`find`/`ls` 工具发起则由双向子树比较覆盖。三个洞与上述替换/base64 一样回落到分类器的存在性话术
229
+ - 自保护 bash 匹配是子串正则——可被混淆绕过;变更检测兜底覆盖会话内绕过,跨会话基线(启动时哈希比对与变更确认,含升级 UX)按 ADR-0001 为二期
219
230
  - dev checkout(从仓库而非 `<agentDir>/extensions/` 运行扩展)不受自保护——下一个正常会话加载的安装副本只在其自身会话的门禁内受保护
220
231
 
221
- **verdict 不是沙箱。** 它在 pi 进程内裁决工具调用;不能遏制恶意代码、不能防护被攻陷的进程、不守护手工 `!` shell 逃逸。需要隔离请用操作系统级沙箱。
232
+ **verdict 不是沙箱。** 它在 pi 进程内裁决工具调用;不能遏制恶意代码、不能防护被攻陷的进程、不守护手工 `!` shell 逃逸。需要隔离请用操作系统级沙箱。
222
233
 
223
- 命名:三态**裁决(verdict)**是核心概念。UX 保留 `/automode` —— 模式概念上溯 Claude Code 的 auto mode,本项目亦借鉴了其 transcript 设计。
234
+ 命名:三态**裁决(verdict)**是核心概念。UX 保留 `/automode` —— 模式概念上溯 Claude Code 的 auto mode,本项目亦借鉴了其 transcript 设计。
224
235
 
225
236
  ## 开发
226
237
 
227
238
  ```bash
228
239
  bun install
229
240
  bun run typecheck
230
- bun test # 离线桩测试:自保护 / 变更检测 / deny floor / 用户规则 / denyPaths / 绕过回归 / 分类器重试 / 影子缓存 / 命令 / toggle 快捷键
241
+ bun test # 离线桩测试:自保护 / 变更检测 / deny floor / 用户规则 / denyPaths / 绕过回归 / 分类器重试 / 命令 / toggle 快捷键
231
242
  ```
232
243
 
233
244
  Issue tracker 与决策记录在 GitHub issues(「地图」issue #1 为索引)。
@@ -195,6 +195,22 @@ export function parseJevConfidence(reason: string): number | null {
195
195
  return m ? Number(m[1]) : null;
196
196
  }
197
197
 
198
+ /** #81: protocol identity — `api === API_ID` names the decisions protocol, the
199
+ * capability axis the extension actually gates on (numeric confidence is a
200
+ * decisions-contract property, not a vendor trait). Structural type so callers
201
+ * can pass any model-shaped object. */
202
+ export function isDecisionsModel(model: { api?: string }): boolean {
203
+ return model.api === API_ID;
204
+ }
205
+
206
+ /** #81: exact match for the one decisions spec pi-verdict registers
207
+ * ("typesafe/jev-latest"). The registry only ever holds that one slug, so prefix
208
+ * tolerance would only ever catch typos — and would mislead with the jev-specific
209
+ * wording keyed on this predicate. */
210
+ export function isJevSpec(spec: string): boolean {
211
+ return spec === `${PROVIDER_ID}/${MODEL_ID}`;
212
+ }
213
+
198
214
  function mapUsage(u: unknown): AssistantMessage["usage"] {
199
215
  const usage = (u ?? {}) as { input_tokens?: unknown; output_tokens?: unknown; cost?: unknown };
200
216
  const input = Number(usage.input_tokens) || 0;