pi-verdict 0.6.0 → 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 +7 -5
- package/README.zh-CN.md +9 -6
- package/extensions/auto-mode.ts +28 -23
- 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
|
|
@@ -26,19 +26,21 @@ pi-verdict adds the missing gate: a model decides whether each call should run,
|
|
|
26
26
|
|
|
27
27
|
## Design principles
|
|
28
28
|
|
|
29
|
-
A small set of security principles shapes the whole gate — the full statement, with the honest edges, lives in [docs/security-principles.md](docs/security-principles.md):
|
|
30
|
-
|
|
31
29
|
- **Fail closed** — uncertainty produces friction, never permission.
|
|
32
30
|
- **Deterministic floor before AI** — hard denies are never overridden by the classifier or user allow rules.
|
|
33
|
-
- **Semantics over syntax** —
|
|
31
|
+
- **Semantics over syntax** — the classifier judges what an action *does*, not how long it is.
|
|
34
32
|
- **Judgments, not proofs** — a classifier `allow` is an informed opinion; the floor exists because that is all it is.
|
|
35
33
|
- **Minimal trusted input** — no tool results in the transcript (#22), zero path plaintext to the classifier (ADR-0002).
|
|
36
34
|
- **Canonical identity** — lexical + realpath dual-form matching; a workspace-*looking* path is not trusted as one (#20/#21).
|
|
37
35
|
- **The gate guards itself** — self-protection that no configuration can disable (ADR-0001).
|
|
38
36
|
- **A permission gate, not a sandbox** — stack OS isolation on top; this gate never replaces it.
|
|
39
37
|
|
|
38
|
+
Full statement in [docs/security-principles.md](docs/security-principles.md).
|
|
39
|
+
|
|
40
40
|
## Screenshots
|
|
41
41
|
|
|
42
|
+

|
|
43
|
+
|
|
42
44
|

|
|
43
45
|

|
|
44
46
|
|
|
@@ -63,7 +65,7 @@ pi-verdict runs on both [pi](https://github.com/badlogic/pi-mono) and [oh-my-pi]
|
|
|
63
65
|
| | pi | omp |
|
|
64
66
|
|---|---|---|
|
|
65
67
|
| install | `pi install npm:pi-verdict` | `omp plugin install npm:pi-verdict` |
|
|
66
|
-
| extension copy | `~/.pi/agent/extensions/` | `~/.omp/
|
|
68
|
+
| extension copy | `~/.pi/agent/extensions/` | `~/.omp/plugins/node_modules/pi-verdict/` (omp 18.1+; ≤18.0: under `agent/`) |
|
|
67
69
|
| user rules | `~/.pi/agent/config/pi-verdict.json` | `~/.omp/agent/config/pi-verdict.json` |
|
|
68
70
|
| credential file (S0 hard deny) | `~/.pi/agent/auth.json` | `~/.omp/agent/auth.json` |
|
|
69
71
|
|
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,23 +22,26 @@ 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
|
-
一小组安全设计原则塑形了整个门禁——完整表述(含诚实的边界说明)见 [docs/security-principles.md](docs/security-principles.md):
|
|
30
|
-
|
|
31
29
|
- **Fail closed**——不确定产生摩擦,绝不产生许可。
|
|
32
30
|
- **确定性 floor 先于 AI**——硬 deny 永不被分类器或用户 allow 规则覆盖。
|
|
33
|
-
-
|
|
31
|
+
- **语义优先于语法**——分类器判定的是动作**做什么可能会产生什么安全影响**,而不是命令有多长。
|
|
34
32
|
- **是判断,不是证明**——分类器的 `allow` 是有依据的判断;floor 的存在正因为它仅此而已。
|
|
35
33
|
- **最小化可信输入**——transcript 不含工具结果(#22),分类器零路径明文(ADR-0002)。
|
|
36
34
|
- **规范化身份**——词法 + realpath 双形匹配;「看起来在项目内」的路径不因此被信任(#20/#21)。
|
|
37
35
|
- **门禁守护自身**——任何配置都关不掉的自保护层(ADR-0001)。
|
|
38
36
|
- **是权限门禁,不是沙箱**——请在上面叠加 OS 级隔离;本门禁不替代它。
|
|
39
37
|
|
|
38
|
+
完整表述见 [docs/security-principles.md](docs/security-principles.md):
|
|
39
|
+
|
|
40
|
+
|
|
40
41
|
## 截图
|
|
41
42
|
|
|
43
|
+

|
|
44
|
+
|
|
42
45
|

|
|
43
46
|

|
|
44
47
|
|
|
@@ -63,7 +66,7 @@ pi-verdict 同时支持 [pi](https://github.com/badlogic/pi-mono) 与 [oh-my-pi]
|
|
|
63
66
|
| | pi | omp |
|
|
64
67
|
|---|---|---|
|
|
65
68
|
| 安装 | `pi install npm:pi-verdict` | `omp plugin install npm:pi-verdict` |
|
|
66
|
-
| 扩展副本 | `~/.pi/agent/extensions/` | `~/.omp/
|
|
69
|
+
| 扩展副本 | `~/.pi/agent/extensions/` | `~/.omp/plugins/node_modules/pi-verdict/`(omp 18.1+;≤18.0 在 `agent/` 下) |
|
|
67
70
|
| 用户规则 | `~/.pi/agent/config/pi-verdict.json` | `~/.omp/agent/config/pi-verdict.json` |
|
|
68
71
|
| 凭据文件(S0 硬 deny) | `~/.pi/agent/auth.json` | `~/.omp/agent/auth.json` |
|
|
69
72
|
|
package/extensions/auto-mode.ts
CHANGED
|
@@ -217,11 +217,12 @@ const OWN_FILE_PATH: string | null = (() => {
|
|
|
217
217
|
* Resolve the agent directory the gate is anchored to (#35, dual-host):
|
|
218
218
|
* 1. PI_CODING_AGENT_DIR — explicit user override, always wins.
|
|
219
219
|
* 2. Self-anchoring from the extension's own installed path: a copy at
|
|
220
|
-
* <home>/<dot-dir>/agent/(plugins/node_modules/<pkg>/)?extensions/…
|
|
220
|
+
* <home>/<dot-dir>/(agent/)?(plugins/node_modules/<pkg>/)?extensions/…
|
|
221
221
|
* anchors to <home>/<dot-dir>/agent. Covers the pi forms
|
|
222
|
-
* (~/.pi/agent/extensions[/pkg]/…) and the omp npm
|
|
223
|
-
* (~/.omp/agent/plugins/node_modules/<pkg
|
|
224
|
-
*
|
|
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.
|
|
225
226
|
* Deliberately NO host-tree existence probing: on a dual-install machine
|
|
226
227
|
* running under pi, a present ~/.omp must not misroute the gate.
|
|
227
228
|
* 3. Fallback: today's default (~/.pi/agent) — dev checkouts and any
|
|
@@ -232,7 +233,7 @@ const OWN_FILE_PATH: string | null = (() => {
|
|
|
232
233
|
export function resolveAgentDir(ownFile: string | null, home: string, envAgentDir: string | undefined): string {
|
|
233
234
|
if (envAgentDir) return envAgentDir;
|
|
234
235
|
if (ownFile) {
|
|
235
|
-
const anchor = new RegExp(`^${escapeRegExp(home)}(/(\\.[^/]+)/agent/(?:plugins/node_modules/(?:@[^/]+/)?[^/]+/)?extensions/)`);
|
|
236
|
+
const anchor = new RegExp(`^${escapeRegExp(home)}(/(\\.[^/]+)/(?:agent/)?(?:plugins/node_modules/(?:@[^/]+/)?[^/]+/)?extensions/)`);
|
|
236
237
|
for (const f of [ownFile, tryRealpath(ownFile)]) {
|
|
237
238
|
const m = f.match(anchor);
|
|
238
239
|
if (m) return path.join(home, m[2], "agent");
|
|
@@ -506,10 +507,9 @@ function hitDenyPaths(toolName: string, input: Record<string, unknown>, cwd: str
|
|
|
506
507
|
// 门禁自身的完整性不受任何配置豁免:builtinDenyFloor:false 只关危险正则与路径
|
|
507
508
|
// 敏感度,关不掉本层;用户 allow 规则亦不可越过。保护对象:
|
|
508
509
|
// - <agentDir>/config/pi-verdict.json(用户规则 = 门禁的判定输入)
|
|
509
|
-
// - 本扩展的安装副本(pi
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
// checkouts are not in scope)
|
|
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)
|
|
513
513
|
// 语义:门禁内一切写入按定义均由 agent 发起 → 恒 deny(reason 指引手工编辑);
|
|
514
514
|
// 读放行(读门禁文件无害);用户经编辑器的修改不经门禁,不受影响。
|
|
515
515
|
// bash 侧:命令串正则覆盖字面量/~/\$HOME/\$PI_CODING_AGENT_DIR 变体,可被混淆
|
|
@@ -556,10 +556,10 @@ function escapeRegExp(s: string): string {
|
|
|
556
556
|
/**
|
|
557
557
|
* 构建受保护集合。
|
|
558
558
|
* ownFile:本模块文件路径(import.meta.url 解析;null = 不可解析,仅保护配置)。
|
|
559
|
-
* The installed copy is protected only when ownFile sits under
|
|
560
|
-
*
|
|
561
|
-
* (
|
|
562
|
-
*
|
|
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).
|
|
563
563
|
*/
|
|
564
564
|
export function buildProtectedSet(agentDir: string, ownFile: string | null): ProtectedSet {
|
|
565
565
|
const exact = new Set<string>();
|
|
@@ -613,15 +613,18 @@ export function buildProtectedSet(agentDir: string, ownFile: string | null): Pro
|
|
|
613
613
|
watchBases.push({ file: ownFile, kind: "extension" });
|
|
614
614
|
const seenWatch = new Set<string>([ownFile]);
|
|
615
615
|
let pkgRoot: string | null = null;
|
|
616
|
-
// Install roots, lexical + realpath forms (#35):
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
// root
|
|
620
|
-
//
|
|
621
|
-
// npm-dir
|
|
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
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)));
|
|
623
625
|
for (const seg of [["extensions"], ["plugins", "node_modules"]]) {
|
|
624
|
-
|
|
626
|
+
const bases = seg.length === 2 ? new Set([...agentBases, ...configRootBases]) : agentBases;
|
|
627
|
+
for (const base of bases) {
|
|
625
628
|
const root = path.join(base, ...seg);
|
|
626
629
|
extRoots.add(root);
|
|
627
630
|
extRoots.add(tryRealpath(root));
|
|
@@ -631,9 +634,11 @@ export function buildProtectedSet(agentDir: string, ownFile: string | null): Pro
|
|
|
631
634
|
for (const extRoot of extRoots) {
|
|
632
635
|
for (const own of ownForms) {
|
|
633
636
|
if (!own.startsWith(extRoot + path.sep)) continue;
|
|
634
|
-
const
|
|
635
|
-
const singleFile =
|
|
636
|
-
|
|
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));
|
|
637
642
|
for (const f of pathForms(target)) {
|
|
638
643
|
(singleFile ? exact : prefixes).add(f);
|
|
639
644
|
extTargets.add(f);
|