pi-verdict 0.5.2 → 0.6.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 +51 -46
- package/README.zh-CN.md +52 -43
- package/extensions/auto-mode.ts +152 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
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.**
|
|
10
10
|
|
|
11
|
-
- Minimal — just
|
|
11
|
+
- Minimal — just 1k+ lines of code
|
|
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
|
|
@@ -22,24 +22,53 @@ pi-verdict adds the missing gate: a model decides whether each call should run,
|
|
|
22
22
|
|
|
23
23
|
## Why three states
|
|
24
24
|
|
|
25
|
-
**verdict is an adjudication, not a switch.** Most classifiers in this space output a binary allow/block. Three states matter: `ask` routes genuinely ambiguous actions to a human (and degrades to `deny` in non-interactive sessions), so "not sure" never silently becomes "go ahead".
|
|
25
|
+
**verdict is an adjudication, not a switch.** Most classifiers in this space output a binary allow/block. Three states matter: `ask` routes genuinely ambiguous actions to a human (and degrades to `deny` in non-interactive sessions), so "not sure" never silently becomes "go ahead" — the goal is safe automation, not maximum automation: both approval fatigue and silent unsafe execution lose.
|
|
26
|
+
|
|
27
|
+
## Design principles
|
|
28
|
+
|
|
29
|
+
- **Fail closed** — uncertainty produces friction, never permission.
|
|
30
|
+
- **Deterministic floor before AI** — hard denies are never overridden by the classifier or user allow rules.
|
|
31
|
+
- **Semantics over syntax** — the classifier judges what an action *does*, not how long it is.
|
|
32
|
+
- **Judgments, not proofs** — a classifier `allow` is an informed opinion; the floor exists because that is all it is.
|
|
33
|
+
- **Minimal trusted input** — no tool results in the transcript (#22), zero path plaintext to the classifier (ADR-0002).
|
|
34
|
+
- **Canonical identity** — lexical + realpath dual-form matching; a workspace-*looking* path is not trusted as one (#20/#21).
|
|
35
|
+
- **The gate guards itself** — self-protection that no configuration can disable (ADR-0001).
|
|
36
|
+
- **A permission gate, not a sandbox** — stack OS isolation on top; this gate never replaces it.
|
|
37
|
+
|
|
38
|
+
Full statement in [docs/security-principles.md](docs/security-principles.md).
|
|
26
39
|
|
|
27
40
|
## Screenshots
|
|
28
41
|
|
|
42
|
+

|
|
43
|
+
|
|
29
44
|

|
|
30
45
|

|
|
31
46
|
|
|
32
47
|
## Quick start
|
|
33
48
|
|
|
34
49
|
```bash
|
|
35
|
-
# install from npm:
|
|
50
|
+
# install from npm (pi):
|
|
36
51
|
pi install npm:pi-verdict
|
|
37
52
|
|
|
53
|
+
# install from npm (oh-my-pi / omp):
|
|
54
|
+
omp plugin install npm:pi-verdict
|
|
55
|
+
|
|
38
56
|
# or directly from git — try it once
|
|
39
57
|
pi --extension ./extensions/auto-mode.ts
|
|
40
58
|
|
|
41
59
|
```
|
|
42
60
|
|
|
61
|
+
### Hosts
|
|
62
|
+
|
|
63
|
+
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).
|
|
64
|
+
|
|
65
|
+
| | pi | omp |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| install | `pi install npm:pi-verdict` | `omp plugin install npm:pi-verdict` |
|
|
68
|
+
| extension copy | `~/.pi/agent/extensions/` | `~/.omp/plugins/node_modules/pi-verdict/` (omp 18.1+; ≤18.0: under `agent/`) |
|
|
69
|
+
| user rules | `~/.pi/agent/config/pi-verdict.json` | `~/.omp/agent/config/pi-verdict.json` |
|
|
70
|
+
| credential file (S0 hard deny) | `~/.pi/agent/auth.json` | `~/.omp/agent/auth.json` |
|
|
71
|
+
|
|
43
72
|
- `/automode` — show current status: on/off + shadow-cache stats for the session
|
|
44
73
|
- `/automode on`
|
|
45
74
|
- `/automode off`
|
|
@@ -68,26 +97,18 @@ pi --extension ./extensions/auto-mode.ts
|
|
|
68
97
|
```
|
|
69
98
|
|
|
70
99
|
- `allow`/`deny` are JS regex arrays; **`deny` wins over `allow`**, both beat the classifier
|
|
71
|
-
- `denyPaths` are plain paths
|
|
72
|
-
|
|
73
|
-
- `
|
|
74
|
-
- `classifierModel: "provider/model-id"` sets the classifier model (e.g. a fast flash-class model); precedence is flag > env > config > session model (self-reflection); an invalid value falls back to the session model with a one-time warning
|
|
75
|
-
- the spec accepts pi's native `--model` thinking suffix: `"zai/glm-5.3-flash:low"` sets classifier thinking to effort low (default without suffix: thinking explicitly off)
|
|
76
|
-
- `toggleShortcut` rebinds the master-switch toggle key (`null` or empty disables it, not persisted)
|
|
77
|
-
- first run generates a template at `~/.pi/agent/config/pi-verdict.json` (honors `PI_CODING_AGENT_DIR`)
|
|
100
|
+
- `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
|
|
101
|
+
- `builtinDenyFloor: false` turns off the built-in danger/path floor (your risk; the self-protection layer below always stays on)
|
|
102
|
+
- `classifierModel` pins the classifier model, e.g. `"zai/glm-5.3-flash:low"` (thinking suffix supported; default: session model with thinking off)
|
|
78
103
|
|
|
79
|
-
|
|
104
|
+
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).
|
|
80
105
|
|
|
81
106
|
### Self-protection (the gate guards itself — [ADR-0001](docs/adr/0001-self-protection-layer.md))
|
|
82
107
|
|
|
83
|
-
The gate's own files —
|
|
108
|
+
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.
|
|
84
109
|
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- **not disableable by any config**: `builtinDenyFloor: false` does not turn this off, and no user `allow` rule can override it. Rationale: user sovereignty is about the risk to *your system*, not about the integrity of the gate itself — a gate its own guarded object can switch off cannot honestly promise "risk accepted by you"
|
|
88
|
-
- **tamper detection** (defense in depth): watched files are snapshotted at `session_start` and re-verified before every verdict, with differential disposal — the installed **extension copy** being changed (or any change in a headless session) is **auto-restored** from the snapshot and the session goes **fail-closed** (all tools denied) until restart; only the **config** changing in an interactive session offers a two-way select whose options state the action themselves: *Accept the new version* (re-baseline and continue — your edit survives, applies next session as usual) / *Decline* (restore the session baseline — revert + fail-closed); dismissing the dialog counts as Decline (safe side). Unconditional auto-restore would mean you can never edit the config while pi runs; warn-only would let a missed warning hand the next session to a tampered config — the rare, stern confirm is the middle path
|
|
89
|
-
|
|
90
|
-
Since everything inside the gate is by definition agent-initiated, denying writes is exactly "only the user can modify" — your editor never passes through the gate. The sudoers/visudo precedent is the closest analogue.
|
|
110
|
+
- **Not disableable by any config** — `builtinDenyFloor: false` and user `allow` rules cannot touch this layer
|
|
111
|
+
- **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)
|
|
91
112
|
|
|
92
113
|
Requires pi ≥ 0.84. Works in interactive and non-interactive (`-p`/json/rpc) sessions; in non-interactive modes `ask` degrades to `deny`.
|
|
93
114
|
|
|
@@ -102,9 +123,7 @@ Requires pi ≥ 0.84. Works in interactive and non-interactive (`-p`/json/rpc) s
|
|
|
102
123
|
|
|
103
124
|
Full landscape: [`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) · convergence analysis with the closest architectural relative: [`research/pi-automode-convergence.md`](research/pi-automode-convergence.md).
|
|
104
125
|
|
|
105
|
-
Honest framing: pi-automode and pi-verdict have **converged on the same architecture** (deny floor → user rules → classifier, fail-closed — see the convergence analysis). What remains distinct here: a classifier that can say `ask` (runtime human-in-the-loop, not just rule-declared), a built-in floor you can turn off (`builtinDenyFloor` — user sovereignty), a self-protection layer that no config can turn off ([ADR-0001](docs/adr/0001-self-protection-layer.md) — gate integrity), a zero-dependency single file (
|
|
106
|
-
|
|
107
|
-
The single-file, zero-dependency shape is deliberate — the whole extension is one readable [file](extensions/auto-mode.ts), ~1.2k lines and growing with features.
|
|
126
|
+
Honest framing: pi-automode and pi-verdict have **converged on the same architecture** (deny floor → user rules → classifier, fail-closed — see the convergence analysis). What remains distinct here: a classifier that can say `ask` (runtime human-in-the-loop, not just rule-declared), a built-in floor you can turn off (`builtinDenyFloor` — user sovereignty), a self-protection layer that no config can turn off ([ADR-0001](docs/adr/0001-self-protection-layer.md) — gate integrity), a zero-dependency single file ([one readable file](extensions/auto-mode.ts), still one file on purpose), and the measurement habit — every design decision in this repo is backed by shipped research.
|
|
108
127
|
|
|
109
128
|
## Pipeline
|
|
110
129
|
|
|
@@ -112,42 +131,28 @@ The single-file, zero-dependency shape is deliberate — the whole extension is
|
|
|
112
131
|
tool_call
|
|
113
132
|
│
|
|
114
133
|
├─ 0. Self-protection layer (ADR-0001; not disableable by any config)
|
|
115
|
-
│ ├─ write/edit/bash touching the gate's own files
|
|
116
|
-
│
|
|
117
|
-
│
|
|
118
|
-
│ └─ tamper detection: re-verify watched files before every verdict;
|
|
119
|
-
│ extension copy changed / headless → auto-restore from snapshot
|
|
120
|
-
│ + fail-closed (deny all) for the rest of the session;
|
|
121
|
-
│ config changed + interactive → one keep/restore confirm
|
|
134
|
+
│ ├─ write/edit/bash touching the gate's own files → deny; reads pass
|
|
135
|
+
│ └─ tamper detection: re-verify before every verdict →
|
|
136
|
+
│ auto-restore + fail-closed, or one keep/restore confirm
|
|
122
137
|
│
|
|
123
138
|
├─ 1. Rule layer (deterministic, zero latency)
|
|
124
|
-
│ ├─ built-in deny floor: bash danger regexes
|
|
125
|
-
│
|
|
126
|
-
│
|
|
127
|
-
│ ├─ your rules: user deny beats user allow (regex, see below)
|
|
128
|
-
│ ├─ denyPaths (ADR-0002): user-declared protected paths, tool-owned
|
|
129
|
-
│ │ normalization (~, $HOME, relative, .., symlink, case on
|
|
130
|
-
│ │ macOS/Windows) → terminal ask,
|
|
139
|
+
│ ├─ built-in deny floor: bash danger regexes + path sensitivity S0–S5
|
|
140
|
+
│ ├─ your rules: user deny beats user allow
|
|
141
|
+
│ ├─ denyPaths (ADR-0002): protected paths → terminal ask,
|
|
131
142
|
│ │ before user allow; classifier sees an existence hint only
|
|
132
143
|
│ └─ no built-in allowlist — every "always allow" claim is yours to make
|
|
133
144
|
│
|
|
134
145
|
├─ 2. Gray zone → model classifier (defaults to session model — "self-reflection")
|
|
135
|
-
│ ├─ input: CC-style <transcript>
|
|
136
|
-
│ │ action under review always last
|
|
137
|
-
│
|
|
138
|
-
│ ├─ existence hint when denyPaths are configured: the classifier knows
|
|
139
|
-
│ │ protected paths exist (never what they are) and judges
|
|
140
|
-
│ │ copy-then-read/archiving/indirection strictly
|
|
141
|
-
│ ├─ thinking explicitly disabled (thinkingEnabled: false) + retry 512→1024
|
|
142
|
-
│ └─ configurable via --auto-mode-model
|
|
146
|
+
│ ├─ input: CC-style <transcript> — recent user intent + tool calls,
|
|
147
|
+
│ │ action under review always last
|
|
148
|
+
│ └─ output contract: <verdict>allow|ask|deny</verdict> prefix-anchored
|
|
143
149
|
│
|
|
144
150
|
└─ 3. Three-state adjudication
|
|
145
151
|
├─ allow → pass
|
|
146
152
|
├─ deny → block, reason returned to the agent
|
|
147
|
-
└─ ask → human confirm
|
|
153
|
+
└─ ask → human confirm; non-interactive modes degrade to deny
|
|
148
154
|
|
|
149
|
-
[shadow cache]
|
|
150
|
-
replays a double-key LRU(128) to measure would-be hit rate
|
|
155
|
+
[shadow cache] observe-only telemetry alongside 2/3, never changes a verdict
|
|
151
156
|
```
|
|
152
157
|
|
|
153
158
|
**fail-closed**: classifier exception / timeout (25s) / contract violation → deny. Never silently allow.
|
package/README.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**pi-verdict 是 [pi](https://pi.dev) 的 Claude Code 风格的 Auto mode 式的极简权限门禁:每次工具调用执行前先过检查——放行、拦截,或先问你。**
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- 只有1k行左右的极简代码
|
|
12
12
|
- 内置危险规则与你的 allow/deny 规则以零延迟先行裁决明确情形
|
|
13
13
|
- 其余交给携带会话上下文的模型分类器
|
|
14
14
|
- 任何不确定或失败一律 fail-closed, 绝不静默放行
|
|
@@ -22,24 +22,54 @@ pi-verdict 补上这道缺失的门禁, 由模型基于上下文和你的意图
|
|
|
22
22
|
|
|
23
23
|
## 为什么是三态
|
|
24
24
|
|
|
25
|
-
**verdict 是裁决,不是开关。** 本品类的分类器大多只输出二值 allow/block。三态有意义的地方在:`ask` 把真正含糊的动作转交人类确认(非交互会话中降级为 `deny`)
|
|
25
|
+
**verdict 是裁决,不是开关。** 本品类的分类器大多只输出二值 allow/block。三态有意义的地方在:`ask` 把真正含糊的动作转交人类确认(非交互会话中降级为 `deny`),「不确定」永远不会静默变成「放行」——目标是安全的自动化而非最大的自动化:审批疲劳与静默危险执行都是危险。
|
|
26
|
+
|
|
27
|
+
## 设计原则
|
|
28
|
+
|
|
29
|
+
- **Fail closed**——不确定产生摩擦,绝不产生许可。
|
|
30
|
+
- **确定性 floor 先于 AI**——硬 deny 永不被分类器或用户 allow 规则覆盖。
|
|
31
|
+
- **语义优先于语法**——分类器判定的是动作**做什么可能会产生什么安全影响**,而不是命令有多长。
|
|
32
|
+
- **是判断,不是证明**——分类器的 `allow` 是有依据的判断;floor 的存在正因为它仅此而已。
|
|
33
|
+
- **最小化可信输入**——transcript 不含工具结果(#22),分类器零路径明文(ADR-0002)。
|
|
34
|
+
- **规范化身份**——词法 + realpath 双形匹配;「看起来在项目内」的路径不因此被信任(#20/#21)。
|
|
35
|
+
- **门禁守护自身**——任何配置都关不掉的自保护层(ADR-0001)。
|
|
36
|
+
- **是权限门禁,不是沙箱**——请在上面叠加 OS 级隔离;本门禁不替代它。
|
|
37
|
+
|
|
38
|
+
完整表述见 [docs/security-principles.md](docs/security-principles.md):
|
|
39
|
+
|
|
26
40
|
|
|
27
41
|
## 截图
|
|
28
42
|
|
|
43
|
+

|
|
44
|
+
|
|
29
45
|

|
|
30
46
|

|
|
31
47
|
|
|
32
48
|
## 快速开始
|
|
33
49
|
|
|
34
50
|
```bash
|
|
35
|
-
# 从 npm
|
|
51
|
+
# 从 npm 安装(pi):
|
|
36
52
|
pi install npm:pi-verdict
|
|
37
53
|
|
|
54
|
+
# 从 npm 安装(oh-my-pi / omp):
|
|
55
|
+
omp plugin install npm:pi-verdict
|
|
56
|
+
|
|
38
57
|
# 或直接从源码 —— 试用一次
|
|
39
58
|
pi --extension ./extensions/auto-mode.ts
|
|
40
59
|
|
|
41
60
|
```
|
|
42
61
|
|
|
62
|
+
### 宿主
|
|
63
|
+
|
|
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
|
+
|
|
66
|
+
| | pi | omp |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| 安装 | `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/config/pi-verdict.json` | `~/.omp/agent/config/pi-verdict.json` |
|
|
71
|
+
| 凭据文件(S0 硬 deny) | `~/.pi/agent/auth.json` | `~/.omp/agent/auth.json` |
|
|
72
|
+
|
|
43
73
|
- `/automode` —— 显示当前状态:开/关 + 本会话影子缓存统计
|
|
44
74
|
- `/automode on`
|
|
45
75
|
- `/automode off`
|
|
@@ -68,26 +98,18 @@ pi --extension ./extensions/auto-mode.ts
|
|
|
68
98
|
```
|
|
69
99
|
|
|
70
100
|
- `allow`/`deny` 为 JS 正则数组;**`deny` 优先于 `allow`**,两者都优先于分类器
|
|
71
|
-
- `denyPaths`
|
|
72
|
-
|
|
73
|
-
- `
|
|
74
|
-
- `classifierModel: "provider/model-id"` 指定分类器模型(如轻量 flash 类);优先级 flag > env > config > 自省;无效值回退会话模型并一次性警告
|
|
75
|
-
- spec 支持 pi 原生 `--model` 思考级别后缀:`"zai/glm-5.3-flash:low"` 将分类器思考设为 effort low(无后缀缺省 = 显式关思考)
|
|
76
|
-
- `toggleShortcut` 重绑主开关快捷键(`null` 或空串禁用, 非持久化)
|
|
77
|
-
- 首次运行自动生成模板 `~/.pi/agent/config/pi-verdict.json`(尊重 `PI_CODING_AGENT_DIR`)
|
|
101
|
+
- `denyPaths` 是你声明**受保护**的普通路径列表:触碰触发**终局 ask** 由你裁决(非交互降级 deny);分类器只被告知路径**存在**,路径明文永不出本机
|
|
102
|
+
- `builtinDenyFloor: false` 整体关闭内置危险/路径拦截(风险自担;下方自保护层永远开启)
|
|
103
|
+
- `classifierModel` 指定分类器模型,如 `"zai/glm-5.3-flash:low"`(支持思考后缀;缺省 = 会话模型且显式关思考)
|
|
78
104
|
|
|
79
|
-
|
|
105
|
+
没有内置白名单——每一条「永远放行」声明都归你([为什么](docs/configuration.md#why-no-built-in-allowlist))。完整参考:[docs/configuration.md](docs/configuration.md)。
|
|
80
106
|
|
|
81
107
|
### 自保护(门禁守护自身——[ADR-0001](docs/adr/0001-self-protection-layer.md))
|
|
82
108
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- `write`/`edit` 触碰 → 硬 **deny**(realpath 归一化比对,含符号链接间接路径);读放行
|
|
86
|
-
- bash/powershell 命令串触碰 → **deny**(字面量/`~`/`$HOME`/`$PI_CODING_AGENT_DIR` 拼写的子串正则——诚实声明:可被混淆绕过,见下方兜底)
|
|
87
|
-
- **不可经任何配置关闭**:`builtinDenyFloor: false` 关不掉它,任何用户 `allow` 规则也越不过它。理由:用户主权豁免的对象是「我的系统的风险」,不是门禁自身的完整性——一个能被其守护对象关掉的门禁,无法诚实兑现「风险自担」的承诺
|
|
88
|
-
- **变更检测**(纵深兜底):受保护文件在 `session_start` 快照、每次裁决前复核,处置按文件差分——**扩展副本**被改(或 headless 会话中的任何变更)→ 从快照**自动还原** + 本会话 **fail-closed**(全量拦截)直至重启;交互会话中仅**配置文件**被改 → 弹一次双选,选项文案即动作本身:*接受新版本*(重建基线、会话照常——你的编辑得以保留,照旧下一会话生效)/*拒绝*(还原会话基线:回滚 + fail-closed);关闭对话框等同拒绝(安全侧)。无条件自动还原等于「pi 运行期间你永远改不了配置」;纯警告则可能让被忽略的警告把下一会话交给被篡改的配置——罕见而郑重的一次确认是中间道路
|
|
109
|
+
门禁自身的文件——配置与扩展安装副本——**仅用户可改**:门禁之内的写入一律硬 deny(读放行);你的编辑器修改不经门禁,最近的同构先例是 sudoers 必须经 visudo。
|
|
89
110
|
|
|
90
|
-
|
|
111
|
+
- **不可经任何配置关闭**——`builtinDenyFloor: false` 与用户 `allow` 规则都动不了这一层
|
|
112
|
+
- **变更检测**作纵深兜底:受保护文件在 `session_start` 快照、每次裁决前复核——扩展副本被改 → 自动还原 + 本会话 fail-closed;配置被改 → 一次明确的双选确认(差分处置的完整语义见 [ADR-0001](docs/adr/0001-self-protection-layer.md))
|
|
91
113
|
|
|
92
114
|
需要 pi ≥ 0.84。交互与非交互(`-p`/json/rpc)会话均支持;非交互模式下 `ask` 降级为 `deny`。
|
|
93
115
|
|
|
@@ -102,9 +124,7 @@ pi --extension ./extensions/auto-mode.ts
|
|
|
102
124
|
|
|
103
125
|
完整全景:[`research/pi-permission-landscape.md`](research/pi-permission-landscape.md) · 与最近架构亲缘的收敛分析:[`research/pi-automode-convergence.md`](research/pi-automode-convergence.md)。
|
|
104
126
|
|
|
105
|
-
诚实地说:pi-automode 与 pi-verdict 在**架构上已收敛**(deny floor → 用户规则 → 分类器,fail-closed——见收敛分析)。这里仍然不同的是:分类器能说 `ask`(运行时人工介入,而非仅由规则预声明)、内置 floor 可以关(`builtinDenyFloor`——用户主权)、任何配置都关不掉的自保护层([ADR-0001](docs/adr/0001-self-protection-layer.md)——门禁完整性)
|
|
106
|
-
|
|
107
|
-
零依赖单文件形态是有意为之——整个扩展就是一个可通读的[单文件](extensions/auto-mode.ts),~1.2k 行,随功能增长。
|
|
127
|
+
诚实地说:pi-automode 与 pi-verdict 在**架构上已收敛**(deny floor → 用户规则 → 分类器,fail-closed——见收敛分析)。这里仍然不同的是:分类器能说 `ask`(运行时人工介入,而非仅由规则预声明)、内置 floor 可以关(`builtinDenyFloor`——用户主权)、任何配置都关不掉的自保护层([ADR-0001](docs/adr/0001-self-protection-layer.md)——门禁完整性)、零依赖的[可通读单文件](extensions/auto-mode.ts)(仍刻意单文件)、以及测量的习惯——本仓库每个设计决策都有随库研究背书。
|
|
108
128
|
|
|
109
129
|
## 管线
|
|
110
130
|
|
|
@@ -112,39 +132,28 @@ pi --extension ./extensions/auto-mode.ts
|
|
|
112
132
|
tool_call
|
|
113
133
|
│
|
|
114
134
|
├─ 0. 自保护层(ADR-0001;不可经任何配置关闭)
|
|
115
|
-
│ ├─ write/edit/bash 触碰门禁自身文件
|
|
116
|
-
│
|
|
117
|
-
│
|
|
118
|
-
│ └─ 变更检测:每次裁决前复核受保护文件;
|
|
119
|
-
│ 扩展副本被改 / headless → 从会话快照自动还原
|
|
120
|
-
│ + 本会话 fail-closed(全量拦截);
|
|
121
|
-
│ 仅配置被改且有 UI → 一次保留/还原确认
|
|
135
|
+
│ ├─ write/edit/bash 触碰门禁自身文件 → deny;读放行
|
|
136
|
+
│ └─ 变更检测:每次裁决前复核 →
|
|
137
|
+
│ 自动还原 + fail-closed,或一次保留/还原确认
|
|
122
138
|
│
|
|
123
139
|
├─ 1. 规则层(确定性,零延迟)
|
|
124
|
-
│ ├─ 内置 deny floor:bash 危险正则
|
|
125
|
-
│
|
|
126
|
-
│ ├─
|
|
127
|
-
│ ├─ denyPaths(ADR-0002):用户声明的受保护路径,工具负责归一化
|
|
128
|
-
│ │ (~、$HOME、相对、..、symlink、macOS/Windows 大小写)→ 终局 ask,先于用户 allow;
|
|
140
|
+
│ ├─ 内置 deny floor:bash 危险正则 + 路径敏感度 S0–S5
|
|
141
|
+
│ ├─ 用户规则:deny 优先于 allow
|
|
142
|
+
│ ├─ denyPaths(ADR-0002):受保护路径 → 终局 ask,先于用户 allow;
|
|
129
143
|
│ │ 分类器只见存在性话术
|
|
130
144
|
│ └─ 无内置白名单 —— 「永远放行」的声明由你自己做
|
|
131
145
|
│
|
|
132
146
|
├─ 2. 灰区 → 模型分类器(默认继承会话模型 —— "自省")
|
|
133
|
-
│ ├─ 输入:CC 风格 <transcript>
|
|
134
|
-
│ │ 待审动作固定在末尾
|
|
135
|
-
│
|
|
136
|
-
│ ├─ 配置了 denyPaths 时注入存在性话术:分类器知道受保护路径存在
|
|
137
|
-
│ │ (永不知其内容),对先拷贝再读取/打包/间接引用从紧裁决
|
|
138
|
-
│ ├─ 显式关思考(thinkingEnabled: false)+ 两档重试 512→1024
|
|
139
|
-
│ └─ 可用 --auto-mode-model 配置
|
|
147
|
+
│ ├─ 输入:CC 风格 <transcript> —— 近期用户意图 + 工具调用,
|
|
148
|
+
│ │ 待审动作固定在末尾
|
|
149
|
+
│ └─ 输出契约:<verdict>allow|ask|deny</verdict> 前缀锚定
|
|
140
150
|
│
|
|
141
151
|
└─ 3. 三态裁决
|
|
142
152
|
├─ allow → 放行
|
|
143
153
|
├─ deny → 拦截,理由回传 agent
|
|
144
|
-
└─ ask →
|
|
154
|
+
└─ ask → 人工确认;非交互模式降级为 deny
|
|
145
155
|
|
|
146
|
-
[影子缓存]
|
|
147
|
-
回放双键 LRU(128)测量 would-be 命中率
|
|
156
|
+
[影子缓存] observe-only 遥测,与 2/3 并行,永不改变裁决
|
|
148
157
|
```
|
|
149
158
|
|
|
150
159
|
**fail-closed**:分类器异常 / 超时(25s)/ 输出违反契约 → 拦截,绝不静默放行。
|
package/extensions/auto-mode.ts
CHANGED
|
@@ -204,8 +204,46 @@ interface UserRules {
|
|
|
204
204
|
|
|
205
205
|
const EMPTY_RULES: UserRules = { allow: [], deny: [], denyPaths: [], builtinDenyFloor: true, classifierModel: null, toggleShortcut: DEFAULT_TOGGLE_SHORTCUT };
|
|
206
206
|
|
|
207
|
+
/** This module's own file location (import.meta.url resolved; null = unresolvable). */
|
|
208
|
+
const OWN_FILE_PATH: string | null = (() => {
|
|
209
|
+
try {
|
|
210
|
+
return fileURLToPath(import.meta.url);
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolve the agent directory the gate is anchored to (#35, dual-host):
|
|
218
|
+
* 1. PI_CODING_AGENT_DIR — explicit user override, always wins.
|
|
219
|
+
* 2. Self-anchoring from the extension's own installed path: a copy at
|
|
220
|
+
* <home>/<dot-dir>/(agent/)?(plugins/node_modules/<pkg>/)?extensions/…
|
|
221
|
+
* anchors to <home>/<dot-dir>/agent. Covers the pi forms
|
|
222
|
+
* (~/.pi/agent/extensions[/pkg]/…) and the two omp npm layouts:
|
|
223
|
+
* under the agent dir (~/.omp/agent/plugins/node_modules/<pkg>/…) and,
|
|
224
|
+
* since omp 18.1, next to it (~/.omp/plugins/node_modules/<pkg>/…) —
|
|
225
|
+
* omp keeps its config tree under <dot-dir>/agent in both layouts.
|
|
226
|
+
* Deliberately NO host-tree existence probing: on a dual-install machine
|
|
227
|
+
* running under pi, a present ~/.omp must not misroute the gate.
|
|
228
|
+
* 3. Fallback: today's default (~/.pi/agent) — dev checkouts and any
|
|
229
|
+
* unanchored location.
|
|
230
|
+
* Both the lexical and the realpath form of ownFile are tried (symlinked
|
|
231
|
+
* agent trees, macOS firmlink homes).
|
|
232
|
+
*/
|
|
233
|
+
export function resolveAgentDir(ownFile: string | null, home: string, envAgentDir: string | undefined): string {
|
|
234
|
+
if (envAgentDir) return envAgentDir;
|
|
235
|
+
if (ownFile) {
|
|
236
|
+
const anchor = new RegExp(`^${escapeRegExp(home)}(/(\\.[^/]+)/(?:agent/)?(?:plugins/node_modules/(?:@[^/]+/)?[^/]+/)?extensions/)`);
|
|
237
|
+
for (const f of [ownFile, tryRealpath(ownFile)]) {
|
|
238
|
+
const m = f.match(anchor);
|
|
239
|
+
if (m) return path.join(home, m[2], "agent");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return path.join(home, ".pi", "agent");
|
|
243
|
+
}
|
|
244
|
+
|
|
207
245
|
function agentDirPath(): string {
|
|
208
|
-
return
|
|
246
|
+
return resolveAgentDir(OWN_FILE_PATH, os.homedir(), process.env.PI_CODING_AGENT_DIR);
|
|
209
247
|
}
|
|
210
248
|
|
|
211
249
|
function userConfigPath(): string {
|
|
@@ -298,7 +336,7 @@ function expandHome(p: string): string {
|
|
|
298
336
|
// the occasional false positive fails toward deny (safe direction).
|
|
299
337
|
const S0_SECRET = [
|
|
300
338
|
/\.ssh(\/|$)/i, /\.aws(\/|$)/i, /\.gnupg(\/|$)/i, /(^|\/)\.env(\.|$)/i, /credentials?(\.|\/|$)/i,
|
|
301
|
-
/(^|\/)id_rsa/i, /\.pem$/i, /_history$/i, /\.config\/gh(\/|$)/i, /\.pi\/agent\/auth\.json$/i,
|
|
339
|
+
/(^|\/)id_rsa/i, /\.pem$/i, /_history$/i, /\.config\/gh(\/|$)/i, /\.(?:pi|omp)\/agent\/auth\.json$/i,
|
|
302
340
|
// V8(安全审计):常见明文凭证文件补全
|
|
303
341
|
/(^|\/)\.netrc$/i, /(^|\/)\.npmrc$/i, /(^|\/)\.pypirc$/i, /(^|\/)\.envrc$/i, /(^|\/)\.vault-token$/i,
|
|
304
342
|
/\.kube(\/|$)/i, /\.docker\/config\.json$/i, /\.gem\/credentials$/i,
|
|
@@ -469,8 +507,9 @@ function hitDenyPaths(toolName: string, input: Record<string, unknown>, cwd: str
|
|
|
469
507
|
// 门禁自身的完整性不受任何配置豁免:builtinDenyFloor:false 只关危险正则与路径
|
|
470
508
|
// 敏感度,关不掉本层;用户 allow 规则亦不可越过。保护对象:
|
|
471
509
|
// - <agentDir>/config/pi-verdict.json(用户规则 = 门禁的判定输入)
|
|
472
|
-
// - 本扩展的安装副本(<agentDir>/extensions
|
|
473
|
-
//
|
|
510
|
+
// - 本扩展的安装副本(pi under <agentDir>/extensions/, omp under
|
|
511
|
+
// plugins/node_modules/<pkg>/ in its config root — install forms listed
|
|
512
|
+
// with resolveAgentDir; dev checkouts are not in scope)
|
|
474
513
|
// 语义:门禁内一切写入按定义均由 agent 发起 → 恒 deny(reason 指引手工编辑);
|
|
475
514
|
// 读放行(读门禁文件无害);用户经编辑器的修改不经门禁,不受影响。
|
|
476
515
|
// bash 侧:命令串正则覆盖字面量/~/\$HOME/\$PI_CODING_AGENT_DIR 变体,可被混淆
|
|
@@ -517,8 +556,10 @@ function escapeRegExp(s: string): string {
|
|
|
517
556
|
/**
|
|
518
557
|
* 构建受保护集合。
|
|
519
558
|
* ownFile:本模块文件路径(import.meta.url 解析;null = 不可解析,仅保护配置)。
|
|
520
|
-
*
|
|
521
|
-
*
|
|
559
|
+
* The installed copy is protected only when ownFile sits under one of the
|
|
560
|
+
* install roots (forms listed with resolveAgentDir; #35). Dev checkouts
|
|
561
|
+
* (source inside the cwd) are NOT protected — in-project development writes
|
|
562
|
+
* are legitimate daily work (ADR-0001).
|
|
522
563
|
*/
|
|
523
564
|
export function buildProtectedSet(agentDir: string, ownFile: string | null): ProtectedSet {
|
|
524
565
|
const exact = new Set<string>();
|
|
@@ -572,14 +613,32 @@ export function buildProtectedSet(agentDir: string, ownFile: string | null): Pro
|
|
|
572
613
|
watchBases.push({ file: ownFile, kind: "extension" });
|
|
573
614
|
const seenWatch = new Set<string>([ownFile]);
|
|
574
615
|
let pkgRoot: string | null = null;
|
|
575
|
-
|
|
616
|
+
// Install roots, lexical + realpath forms (#35): <agentDir>/extensions
|
|
617
|
+
// (pi) and plugins/node_modules under agentDir or its parent dir (the
|
|
618
|
+
// two omp layouts — see resolveAgentDir for the layout history). The
|
|
619
|
+
// path segments under the matched root name the install target: a file
|
|
620
|
+
// → exact, a package dir (`@scope/pkg` or `pkg`) → prefix, so every
|
|
621
|
+
// npm form gets whole-package-dir protection (#26).
|
|
622
|
+
const extRoots = new Set<string>();
|
|
623
|
+
const agentBases = new Set([agentDir, tryRealpath(agentDir)]);
|
|
624
|
+
const configRootBases = new Set([...agentBases].map((b) => path.dirname(b)));
|
|
625
|
+
for (const seg of [["extensions"], ["plugins", "node_modules"]]) {
|
|
626
|
+
const bases = seg.length === 2 ? new Set([...agentBases, ...configRootBases]) : agentBases;
|
|
627
|
+
for (const base of bases) {
|
|
628
|
+
const root = path.join(base, ...seg);
|
|
629
|
+
extRoots.add(root);
|
|
630
|
+
extRoots.add(tryRealpath(root));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
576
633
|
const ownForms = new Set([ownFile, tryRealpath(ownFile)]);
|
|
577
634
|
for (const extRoot of extRoots) {
|
|
578
635
|
for (const own of ownForms) {
|
|
579
636
|
if (!own.startsWith(extRoot + path.sep)) continue;
|
|
580
|
-
const
|
|
581
|
-
const singleFile =
|
|
582
|
-
|
|
637
|
+
const segs = path.relative(extRoot, own).split(path.sep);
|
|
638
|
+
const singleFile = segs.length === 1;
|
|
639
|
+
// npm scopes are two-segment dirs (@scope/pkg): the install
|
|
640
|
+
// target is the package, not the whole scope dir
|
|
641
|
+
const target = singleFile ? own : path.join(extRoot, ...segs.slice(0, segs[0].startsWith("@") ? 2 : 1));
|
|
583
642
|
for (const f of pathForms(target)) {
|
|
584
643
|
(singleFile ? exact : prefixes).add(f);
|
|
585
644
|
extTargets.add(f);
|
|
@@ -840,9 +899,60 @@ const CLASSIFIER_TIMEOUT_MS = 25_000; // 本网关 CC 分类器分布 p90=19.8s(
|
|
|
840
899
|
const CLASSIFIER_MAX_TOKENS = 512;
|
|
841
900
|
const CLASSIFIER_RETRY_MAX_TOKENS = 1024; // 防御重试档:覆盖无视 reasoning:off 或轻思考仍超预算的模型
|
|
842
901
|
|
|
902
|
+
/**
|
|
903
|
+
* Minimal structural shape of a completion call (#35). pi exposes it as
|
|
904
|
+
* ModelRegistry.complete; omp 18 does not, but the pi-ai compat module exports
|
|
905
|
+
* a functionally identical `complete`. Options pass through verbatim on both
|
|
906
|
+
* hosts (thinkingEnabled/effort/cacheRetention included — see
|
|
907
|
+
* research/thinking-param-blackhole.md for why API-native fields matter).
|
|
908
|
+
*/
|
|
909
|
+
export type CompletionFn = (
|
|
910
|
+
model: NonNullable<ExtensionContext["model"]>,
|
|
911
|
+
context: { systemPrompt?: string; messages: unknown[] },
|
|
912
|
+
options?: Record<string, unknown>,
|
|
913
|
+
) => Promise<{ content: Array<{ type: string; text: string }>; stopReason?: string }>;
|
|
914
|
+
|
|
915
|
+
type CompatLoader = () => Promise<{ complete: CompletionFn }>;
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Bind the host runtime's completion capability (#35): registry.complete when
|
|
919
|
+
* present (pi), else the pi-ai compat module (omp 18). The literal dynamic
|
|
920
|
+
* import specifier must stay inline — omp's legacy compat rewrites exactly
|
|
921
|
+
* this literal to its bundled pi-ai; the ./compat subpath also exists on pi,
|
|
922
|
+
* so resolution is safe on both hosts. The loader promise is cached; any
|
|
923
|
+
* rejection propagates to the caller (the classifier's fail-closed path owns it).
|
|
924
|
+
*/
|
|
925
|
+
export function bindCompletion(
|
|
926
|
+
registry: { complete?: unknown },
|
|
927
|
+
compatLoader: CompatLoader = () => import("@earendil-works/pi-ai/compat") as Promise<{ complete: CompletionFn }>,
|
|
928
|
+
): CompletionFn {
|
|
929
|
+
if (typeof registry.complete === "function") {
|
|
930
|
+
const complete = registry.complete as CompletionFn;
|
|
931
|
+
return (m, c, o) => complete.call(registry, m, c, o);
|
|
932
|
+
}
|
|
933
|
+
let compat: Promise<{ complete: CompletionFn }> | undefined;
|
|
934
|
+
return async (m, c, o) => {
|
|
935
|
+
compat ??= compatLoader();
|
|
936
|
+
const { complete } = await compat;
|
|
937
|
+
return complete(m, c, o);
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Session-lifetime cache keyed by registry instance: resolve once per registry.
|
|
942
|
+
const completionCache = new WeakMap<object, CompletionFn>();
|
|
943
|
+
function completionFor(registry: { complete?: unknown }, compatLoader?: CompatLoader): CompletionFn {
|
|
944
|
+
let fn = completionCache.get(registry);
|
|
945
|
+
if (!fn) {
|
|
946
|
+
fn = bindCompletion(registry, compatLoader);
|
|
947
|
+
completionCache.set(registry, fn);
|
|
948
|
+
}
|
|
949
|
+
return fn;
|
|
950
|
+
}
|
|
951
|
+
|
|
843
952
|
/** 单次分类器调用:显式 reasoning:"off"(见下方注释),失败返回错误串而非抛出 */
|
|
844
953
|
async function callClassifierOnce(
|
|
845
954
|
ctx: ExtensionContext,
|
|
955
|
+
complete: CompletionFn,
|
|
846
956
|
model: NonNullable<ExtensionContext["model"]>,
|
|
847
957
|
userMessage: string,
|
|
848
958
|
maxTokens: number,
|
|
@@ -852,7 +962,7 @@ async function callClassifierOnce(
|
|
|
852
962
|
const signals = [AbortSignal.timeout(CLASSIFIER_TIMEOUT_MS)];
|
|
853
963
|
if (ctx.signal) signals.push(ctx.signal);
|
|
854
964
|
try {
|
|
855
|
-
const response = await
|
|
965
|
+
const response = await complete(
|
|
856
966
|
model,
|
|
857
967
|
{
|
|
858
968
|
systemPrompt,
|
|
@@ -862,14 +972,25 @@ async function callClassifierOnce(
|
|
|
862
972
|
signal: AbortSignal.any(signals),
|
|
863
973
|
maxTokens,
|
|
864
974
|
temperature: 0,
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
//
|
|
975
|
+
// Thinking params go out in both hosts' native dialects (#35):
|
|
976
|
+
// pi's registry.complete consumes thinkingEnabled/effort (the
|
|
977
|
+
// API-native fields, per the blackhole findings in
|
|
978
|
+
// research/thinking-param-blackhole.md); omp's compat complete
|
|
979
|
+
// consumes reasoning/disableReasoning. Both sides ignore unknown
|
|
980
|
+
// option fields, so dual-send lets each host pick its own.
|
|
981
|
+
// pi off = explicitly disabled (verified to send
|
|
982
|
+
// thinking:{"type":"disabled"}; GLM downgrades to effort-low light
|
|
983
|
+
// thinking); suffix levels arrive via adaptive effort (minimal→low).
|
|
984
|
+
// omp off = disableReasoning (without it, an absent `reasoning`
|
|
985
|
+
// leaves the model default undefined); level vocabularies share the
|
|
986
|
+
// ThinkingLevel word list, reasoning passes through as-is.
|
|
870
987
|
...(thinking === "off"
|
|
871
|
-
? { thinkingEnabled: false }
|
|
872
|
-
: {
|
|
988
|
+
? { thinkingEnabled: false, disableReasoning: true }
|
|
989
|
+
: {
|
|
990
|
+
thinkingEnabled: true,
|
|
991
|
+
effort: thinking === "minimal" ? ("low" as const) : thinking,
|
|
992
|
+
reasoning: thinking === "minimal" ? ("low" as const) : thinking,
|
|
993
|
+
}),
|
|
873
994
|
cacheRetention: "short",
|
|
874
995
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
875
996
|
},
|
|
@@ -878,7 +999,7 @@ async function callClassifierOnce(
|
|
|
878
999
|
.filter((b) => b.type === "text")
|
|
879
1000
|
.map((b) => b.text)
|
|
880
1001
|
.join("");
|
|
881
|
-
return { ok: true, text, stopReason: response.stopReason };
|
|
1002
|
+
return { ok: true, text, stopReason: response.stopReason ?? "unknown" };
|
|
882
1003
|
} catch (err) {
|
|
883
1004
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
884
1005
|
}
|
|
@@ -892,6 +1013,7 @@ async function callClassifierOnce(
|
|
|
892
1013
|
*/
|
|
893
1014
|
async function classifyWithModel(
|
|
894
1015
|
ctx: ExtensionContext,
|
|
1016
|
+
complete: CompletionFn,
|
|
895
1017
|
model: NonNullable<ExtensionContext["model"]>,
|
|
896
1018
|
actionLine: string,
|
|
897
1019
|
thinking: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" = "off",
|
|
@@ -904,7 +1026,7 @@ async function classifyWithModel(
|
|
|
904
1026
|
const failures: string[] = [];
|
|
905
1027
|
for (const [n, maxTokens] of attempts) {
|
|
906
1028
|
if (ctx.signal?.aborted) break; // 用户已取消,不再重试
|
|
907
|
-
const r = await callClassifierOnce(ctx, model, userMessage, maxTokens, thinking, systemPrompt);
|
|
1029
|
+
const r = await callClassifierOnce(ctx, complete, model, userMessage, maxTokens, thinking, systemPrompt);
|
|
908
1030
|
if (r.ok) {
|
|
909
1031
|
const diag = `stopReason=${r.stopReason}, model=${model.id}, raw output=${JSON.stringify(r.text.slice(0, 200))}`;
|
|
910
1032
|
if (r.stopReason !== "error" && r.stopReason !== "aborted") {
|
|
@@ -1044,7 +1166,12 @@ function shadowTag(probe: ShadowProbe): string {
|
|
|
1044
1166
|
// 扩展主体
|
|
1045
1167
|
// ============================================================================
|
|
1046
1168
|
|
|
1047
|
-
|
|
1169
|
+
/** Optional dependency injection for tests (#35): fake the compat fallback loader. */
|
|
1170
|
+
export interface AutoModeDeps {
|
|
1171
|
+
compatLoader?: CompatLoader;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export default function autoMode(pi: ExtensionAPI, deps: AutoModeDeps = {}) {
|
|
1048
1175
|
pi.registerFlag("auto-mode", { description: "Enable Auto Mode (rules + model classifier gating for tool calls)", type: "boolean", default: true });
|
|
1049
1176
|
pi.registerFlag("auto-mode-model", { description: "Classifier model as provider/id[:thinking] (pi --model syntax; default: inherit session model)", type: "string" });
|
|
1050
1177
|
pi.registerFlag("auto-mode-debug", { description: "Notify every verdict incl. allows, with shadow-cache annotation", type: "boolean", default: false });
|
|
@@ -1063,15 +1190,9 @@ export default function autoMode(pi: ExtensionAPI) {
|
|
|
1063
1190
|
return denyPathBases;
|
|
1064
1191
|
};
|
|
1065
1192
|
|
|
1066
|
-
//
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
return fileURLToPath(import.meta.url);
|
|
1070
|
-
} catch {
|
|
1071
|
-
return null;
|
|
1072
|
-
}
|
|
1073
|
-
})();
|
|
1074
|
-
const prot = buildProtectedSet(agentDirPath(), ownFilePath);
|
|
1193
|
+
// Self-protection layer (ADR-0001): self-anchored protected set + tamper
|
|
1194
|
+
// baseline (in-memory, per session)
|
|
1195
|
+
const prot = buildProtectedSet(agentDirPath(), OWN_FILE_PATH);
|
|
1075
1196
|
let snapshots = takeSnapshots(prot.watchBases);
|
|
1076
1197
|
let tampered = false;
|
|
1077
1198
|
|
|
@@ -1303,7 +1424,7 @@ export default function autoMode(pi: ExtensionAPI) {
|
|
|
1303
1424
|
const ctxKey = shadowContextKey(ctx);
|
|
1304
1425
|
const probe = shadow.probe(cmdKey, ctxKey);
|
|
1305
1426
|
|
|
1306
|
-
const outcome = await classifyWithModel(ctx, model, action, classifierThinking, userRules.denyPaths.length > 0);
|
|
1427
|
+
const outcome = await classifyWithModel(ctx, completionFor(ctx.modelRegistry, deps.compatLoader), model, action, classifierThinking, userRules.denyPaths.length > 0);
|
|
1307
1428
|
|
|
1308
1429
|
// 影子回记:真实模型 allow/deny 入缓存;ask 与 fail-closed 不入(#5 定案);
|
|
1309
1430
|
// 命中且本次为可缓存裁决时,对比反事实一致性
|