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 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 a few hundred lines of code
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** — a long read-only pipeline may auto-allow while a short destructive one still denies; the classifier judges what an action *does*, not how long it is.
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
+ ![Demo: protected-path ask declined](docs/demo.gif)
43
+
42
44
  ![Automode Status](docs/images/status.png)
43
45
  ![Ask Permission](docs/images/asked.png)
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/agent/plugins/node_modules/pi-verdict/` |
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
+ ![演示:受保护路径 ask 被拒绝](docs/demo.gif)
44
+
42
45
  ![Automode Status](docs/images/status.png)
43
46
  ![Ask Permission](docs/images/asked.png)
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/agent/plugins/node_modules/pi-verdict/` |
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
 
@@ -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 form
223
- * (~/.omp/agent/plugins/node_modules/<pkg>/extensions/…); XDG-style
224
- * ~/.config/pi trees match too because the anchor accepts any dot-dir.
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 installs under <agentDir>/extensions/, omp under
510
- // <agentDir>/plugins/node_modules/<pkg>/; self-anchored via import.meta.url,
511
- // covering the single-file and npm package-dir install forms; dev
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
- * <agentDir>/extensions/ (pi) or <agentDir>/plugins/node_modules/<pkg>/
561
- * (omp npm form, #35). Dev checkouts (source inside the cwd) are NOT
562
- * protected — in-project development writes are legitimate daily work (ADR-0001).
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): pi installs under
617
- // <agentDir>/extensions/, omp installs npm plugins under
618
- // <agentDir>/plugins/node_modules/. First path segment under the matched
619
- // root is the install target (single file → exact, package dir → prefix),
620
- // so the omp form gets whole-package-dir protection exactly like the pi
621
- // npm-dir form (#26).
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
- for (const base of new Set([agentDir, tryRealpath(agentDir)])) {
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 rel = path.relative(extRoot, own);
635
- const singleFile = !rel.includes(path.sep);
636
- const target = singleFile ? own : path.join(extRoot, rel.split(path.sep)[0]);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-verdict",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A minimal permission gate for Pi in the style of Claude Code's auto mode",
5
5
  "author": "Jesset (https://github.com/jesset)",
6
6
  "type": "module",