@yaosu/pi-path-guard 1.0.0 → 1.1.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
@@ -22,7 +22,7 @@ pi install /path/to/pi-path-guard
22
22
  # Try without installing (no settings change / 临时试用,不写入 settings)
23
23
  pi -e ./pi-path-guard
24
24
 
25
- # npm (after publishing / 发布后): scope package
25
+ # npm: scope package
26
26
  # pi install npm:@yaosu/pi-path-guard
27
27
  ```
28
28
 
@@ -33,32 +33,36 @@ After installing, run `/reload` or restart pi. 安装后 `/reload` 或重启 pi
33
33
  ### `/guard` command
34
34
 
35
35
  - `/guard` — interactive mode picker (title shows the full decision matrix; choices are bilingual) 交互式选择防护模式(标题展示完整判定矩阵,选项中英双语)
36
- - `/guard <strict|normal|loose|trusted>` — quick switch (trusted requires a warning confirmation) 快捷切换(trusted 需警告确认)
36
+ - `/guard <strict|normal|loose|trusted|naked>` — quick switch (trusted requires a warning; naked requires a double warning) 快捷切换(trusted 需警告确认;naked 需两级确认)
37
37
  - Invalid argument → falls back to the interactive picker 非法参数 → 兜底弹出交互选择
38
38
  - Every new session resets to `normal` 每次新会话自动回到 `normal`
39
+ - The active mode is shown in the footer status bar (`🛡 <mode>`, `🛡 NAKED` in warning color) 当前模式显示在底部状态栏(`🛡 <mode>`,naked 用警示色 `🛡 NAKED`)
39
40
 
40
41
  ### Guard mode matrix / 防护模式矩阵
41
42
 
42
- | Checkpoint / 判定点 | strict | normal | loose | trusted |
43
- | --- | --- | --- | --- | --- |
44
- | Protected paths (.env/.ssh/keys/credentials) / 受保护路径 | block | block | block | block |
45
- | Block group (mkfs/reboot/block-device writes/bulk delete) / Block 组危险命令 | block | block | block | block |
46
- | Confirm group (sudo/ssh/chmod 777 …) / Confirm 组 | block | confirm | confirm | confirm |
47
- | git destructive (reset --hard/clean -f …) / git 破坏性 | confirm | confirm | confirm | confirm |
48
- | In-project write/edit/new / 项目内写/改/新建 | confirm | pass | pass | pass |
49
- | In-project delete / 项目内删除 | confirm | confirm | pass | pass |
50
- | Outside write (new file) / 项目外写新文件 | confirm | confirm | pass | pass |
51
- | Outside overwrite existing / 项目外覆盖已存在 | block | block | confirm | pass |
52
- | Outside delete ordinary / 项目外删除普通文件 | block | block | confirm | pass |
53
- | `>` truncate existing file / 截断已有文件 | confirm | confirm | confirm | confirm |
54
- | cwd=HOME write / HOME 目录写 | confirm | confirm | pass | pass |
55
- | No UI (headless) / 无交互界面 | block* | block* | block* | block* |
43
+ | Checkpoint / 判定点 | strict | normal | loose | trusted | naked |
44
+ | --- | --- | --- | --- | --- | --- |
45
+ | Protected paths (.env/.ssh/keys/credentials) / 受保护路径 | block | block | block | block | pass |
46
+ | Block group (mkfs/reboot/block-device writes/bulk delete) / Block 组危险命令 | block | block | block | block | confirm |
47
+ | Confirm group (sudo/ssh/chmod 777 …) / Confirm 组 | block | confirm | confirm | confirm | pass |
48
+ | git destructive (reset --hard/clean -f …) / git 破坏性 | confirm | confirm | confirm | confirm | pass |
49
+ | In-project write/edit/new / 项目内写/改/新建 | confirm | pass | pass | pass | pass |
50
+ | In-project delete / 项目内删除 | confirm | confirm | pass | pass | pass |
51
+ | Outside write (new file) / 项目外写新文件 | confirm | confirm | pass | pass | pass |
52
+ | Outside overwrite existing / 项目外覆盖已存在 | block | block | confirm | pass | pass |
53
+ | Outside delete ordinary / 项目外删除普通文件 | block | block | confirm | pass | pass |
54
+ | `>` truncate existing file / 截断已有文件 | confirm | confirm | confirm | confirm | pass |
55
+ | cwd=HOME write / HOME 目录写 | confirm | confirm | pass | pass | pass |
56
+ | No UI (headless) / 无交互界面 | block* | block* | block* | block* | pass |
56
57
 
57
58
  *block = denied directly, no confirmation opportunity / 直接阻止,无确认机会;confirm = prompt / 弹窗询问;pass = allow / 放行;\*headless: items that would be confirmed are blocked instead / 无 UI 时需确认项一律阻止
58
59
 
60
+ > ⚠️ **naked mode / 裸奔模式**: passes almost everything — protected paths, the write/edit tool checks, git destructive, truncation, outside deletes/overwrites all pass even with no UI. Only system-destructive commands (mkfs/reboot/bulk-delete/block-device writes) are still **confirmed**. Switching requires a **double confirmation** (two prompts). Use only when you want minimal path-guard interference.
61
+ > ⚠️ **裸奔模式**:除系统级破坏命令外几乎全部放行——受保护路径、write/edit 工具检查、git 破坏性、截断、外部删除/覆盖均放行,无 UI 下也放行;但系统级破坏命令(mkfs/reboot/批量删除/写块设备)仍会**弹窗询问**。切换需要**两级确认**(两次弹窗)。仅当你需要最少的路径守护干扰时使用。
62
+
59
63
  ### Core capabilities / 核心能力
60
64
 
61
- - **Protected-path interception / 受保护路径拦截**: `.env` / `.ssh` / `.aws` / `.kube` / private keys (`*.pem`/`*.key`) / credentials / shell configs (`.bashrc` …) / `node_modules` / `dist` / `build` … blocked hard in every mode — 任何模式下硬性阻止
65
+ - **Protected-path interception / 受保护路径拦截**: `.env` / `.ssh` / `.aws` / `.kube` / private keys (`*.pem`/`*.key`) / credentials / shell configs (`.bashrc` …) / `node_modules` / `dist` / `build` … blocked hard in every mode (except naked) 任何模式下硬性阻止(naked 除外)
62
66
  - **Block group / Block 组危险命令**: `mkfs.*` / `mkswap` / `poweroff` / `reboot` / `shutdown` / `dd` to block devices / `> /dev/sdX` / `find -delete` / `find -exec rm` / `xargs rm`
63
67
  - **Confirm group / Confirm 组**: `sudo` / `doas` / `pkexec` / `chmod 777` / `ssh` / `scp` / `sftp` / `rsh` / `telnet` / `wget -O /dev/null`
64
68
  - **Overwrite detection / 覆盖检测**: `mv` / `cp` / `install` / `tee` / `ln -f` / `rsync --delete` on existing targets, classified by in/out project — 目标已存在时按内外策略处理
@@ -69,13 +73,13 @@ After installing, run `/reload` or restart pi. 安装后 `/reload` 或重启 pi
69
73
 
70
74
  ## Development / 开发与测试
71
75
 
72
- Automated tests (83 assertions) load the real extension with a mocked pi API, covering the 4 modes × protected paths / dangerous commands / truncation / git destructive matrix, plus `/guard` command interaction and trusted-mode confirmation flow:
76
+ Automated tests (99 assertions) load the real extension with a mocked pi API, covering the 5 modes × protected paths / dangerous commands / truncation / git destructive matrix, plus `/guard` command interaction, trusted-mode confirmation, naked-mode double confirmation, and the footer status indicator:
73
77
 
74
78
  ```bash
75
79
  cd tests && node --experimental-strip-types test-pathguard.ts
76
80
  ```
77
81
 
78
- 自动化测试(83 断言)模拟 pi API 加载真实扩展,覆盖 4 种模式 × 受保护路径 / 危险命令 / 截断 / git 破坏性等判定矩阵,以及 `/guard` 命令交互与 trusted 确认流程:
82
+ 自动化测试(99 断言)模拟 pi API 加载真实扩展,覆盖 5 种模式 × 受保护路径 / 危险命令 / 截断 / git 破坏性等判定矩阵,以及 `/guard` 命令交互、trusted 确认与 naked 两级确认、底部状态栏指示等流程:
79
83
 
80
84
  ```bash
81
85
  cd tests && node --experimental-strip-types test-pathguard.ts
@@ -37,6 +37,11 @@
37
37
  * - trusted most permissive: overwrites and ordinary-file deletes pass too
38
38
  * - Protected paths (credentials/config/keys) and the Block group (format/shutdown/bulk-delete/
39
39
  * block-device writes) are blocked directly in every mode, with no confirmation opportunity
40
+ *
41
+ * v3.1 adds (upgrade from v3): naked mode — passes almost everything (protected paths, write/edit
42
+ * checks, git destructive, truncate, outside deletes/overwrites); only system-destructive
43
+ * Block-group commands (mkfs/reboot/block-device writes/bulk delete) are still confirmed.
44
+ * Switching to naked requires a double confirmation (stronger than trusted's single warning).
40
45
  */
41
46
 
42
47
  import type {
@@ -47,6 +52,7 @@ import type {
47
52
  EditToolInput,
48
53
  WriteToolInput,
49
54
  ToolCallEventResult,
55
+ ExtensionUIContext,
50
56
  } from "@earendil-works/pi-coding-agent";
51
57
  import {
52
58
  resolve,
@@ -185,8 +191,8 @@ const HOME = homedir();
185
191
 
186
192
  // ─── Guard Modes ─────────────────────────────────────────────────────
187
193
 
188
- /** Guard mode: strict (full) / normal (default) / loose (relaxed) / trusted (most permissive) */
189
- type GuardMode = "strict" | "normal" | "loose" | "trusted";
194
+ /** Guard mode: strict (full) / normal (default) / loose (relaxed) / trusted (most permissive) / naked (no protection) */
195
+ type GuardMode = "strict" | "normal" | "loose" | "trusted" | "naked";
190
196
 
191
197
  /** Current session guard mode (switched via /guard; reset to normal on session_start) */
192
198
  let currentMode: GuardMode = "normal";
@@ -197,6 +203,7 @@ const GUARD_MODES: readonly GuardMode[] = [
197
203
  "normal",
198
204
  "loose",
199
205
  "trusted",
206
+ "naked",
200
207
  ];
201
208
 
202
209
  /** Whether a string is a valid guard mode (for /guard argument validation) */
@@ -214,24 +221,26 @@ const MODE_DESCRIPTIONS: Record<GuardMode, string> = {
214
221
  "Loose: pass new-file writes & deletes, confirm overwrites / 放宽:新建/删除免问,覆盖需确认",
215
222
  trusted:
216
223
  "Trusted: pass overwrites & ordinary-file deletes / 最宽松:覆盖/删除普通文件也免问",
224
+ naked:
225
+ "Naked: pass everything except system-destructive cmds (confirmed) / 裸奔:除系统级破坏命令外全部放行(破坏命令弹窗询问)",
217
226
  };
218
227
 
219
228
  /** Full decision matrix (shown as the /guard picker title, English only) */
220
229
  const MODE_MATRIX = [
221
230
  "Path Guard Mode Matrix (B=block / ?=confirm / .=pass)",
222
- " Checkpoint strict normal loose trusted",
223
- " Protected paths .env/.ssh/keys B B B B",
224
- " System-destructive mkfs/reboot B B B B",
225
- " Privilege/remote sudo/ssh/chmod777 B ? ? ?",
226
- " Git destructive reset --hard ? ? ? ?",
227
- " In-project write/edit/new ? . . .",
228
- " In-project delete ? ? . .",
229
- " Outside write (new file) ? ? . .",
230
- " Outside overwrite existing B B ? .",
231
- " Outside delete ordinary B B ? .",
232
- " Truncate existing > file ? ? ? ?",
233
- " HOME dir write ? ? . .",
234
- " No UI (headless) B B B B",
231
+ " Checkpoint strict normal loose trusted naked",
232
+ " Protected paths .env/.ssh/keys B B B B .",
233
+ " System-destructive mkfs/reboot B B B B ?",
234
+ " Privilege/remote sudo/ssh/chmod777 B ? ? ? .",
235
+ " Git destructive reset --hard ? ? ? ? .",
236
+ " In-project write/edit/new ? . . . .",
237
+ " In-project delete ? ? . . .",
238
+ " Outside write (new file) ? ? . . .",
239
+ " Outside overwrite existing B B ? . .",
240
+ " Outside delete ordinary B B ? . .",
241
+ " Truncate existing > file ? ? ? ? .",
242
+ " HOME dir write ? ? . . .",
243
+ " No UI (headless) B B B B .",
235
244
  ].join("\n");
236
245
 
237
246
  /** Guard verdict: { block, reason } to block / undefined to allow (askConfirm returns a Promise) */
@@ -242,29 +251,47 @@ type GuardVerdict =
242
251
 
243
252
  // ─── Entry ────────────────────────────────────────────────────────────
244
253
 
254
+ /** Set the current guard mode and mirror it into the TUI footer status bar. */
255
+ function setMode(mode: GuardMode, ui: ExtensionUIContext) {
256
+ currentMode = mode;
257
+ refreshModeStatus(ui);
258
+ }
259
+
260
+ /**
261
+ * Show the active guard mode in the footer status bar (persists across renders).
262
+ * naked is highlighted in warning color so the "bare" state is unmissable.
263
+ */
264
+ function refreshModeStatus(ui: ExtensionUIContext) {
265
+ const t = ui.theme;
266
+ const color = currentMode === "naked" ? "warning" : "accent";
267
+ const label = currentMode === "naked" ? "🛡 NAKED" : `🛡 ${currentMode}`;
268
+ ui.setStatus("path-guard", t.fg(color, label));
269
+ }
270
+
245
271
  export default function (pi: ExtensionAPI) {
246
272
  // Reset to normal on every new session (startup, /new, /resume all fire session_start)
247
- pi.on("session_start", () => {
248
- currentMode = "normal";
273
+ pi.on("session_start", (_event, ctx) => {
274
+ setMode("normal", ctx.ui);
249
275
  });
250
276
 
251
277
  // /guard slash command: view / switch guard mode
252
278
  pi.registerCommand("guard", {
253
279
  description:
254
- "Path Guard modes: /guard shows the current mode, /guard <strict|normal|loose|trusted> switches",
280
+ "Path Guard modes: /guard shows the current mode, /guard <strict|normal|loose|trusted|naked> switches",
255
281
  handler: async (args, ctx) => {
256
282
  const m = args?.trim().toLowerCase() ?? "";
257
283
 
258
- // Valid argument → switch directly (shortcut, no picker); trusted requires a warning confirmation
284
+ // Valid argument → switch directly (shortcut, no picker); trusted/naked require a warning confirmation
259
285
  if (isGuardMode(m)) {
260
- if (m === "trusted" && !(await confirmTrustedSwitch(ctx))) {
286
+ if (!(await confirmModeSwitch(m, ctx))) {
261
287
  ctx.ui.notify(
262
- "Cancelled: switching to trusted requires confirmation",
288
+ `Cancelled: switching to ${m} requires confirmation`,
263
289
  "info",
264
290
  );
265
291
  return;
266
292
  }
267
293
  currentMode = m;
294
+ refreshModeStatus(ctx.ui);
268
295
  ctx.ui.notify(
269
296
  `Path Guard switched to: ${m} (session-only; new sessions reset to normal)`,
270
297
  "info",
@@ -293,14 +320,15 @@ export default function (pi: ExtensionAPI) {
293
320
  }
294
321
  const picked = chosen.split(/\s+/)[0] as GuardMode;
295
322
  if (isGuardMode(picked)) {
296
- if (picked === "trusted" && !(await confirmTrustedSwitch(ctx))) {
323
+ if (!(await confirmModeSwitch(picked, ctx))) {
297
324
  ctx.ui.notify(
298
- "Cancelled: switching to trusted requires confirmation",
325
+ `Cancelled: switching to ${picked} requires confirmation`,
299
326
  "info",
300
327
  );
301
328
  return;
302
329
  }
303
330
  currentMode = picked;
331
+ refreshModeStatus(ctx.ui);
304
332
  ctx.ui.notify(
305
333
  `Path Guard switched to: ${picked} (session-only; new sessions reset to normal)`,
306
334
  "info",
@@ -330,6 +358,9 @@ function checkWriteEdit(
330
358
  const path = input.path;
331
359
  if (!path) return;
332
360
 
361
+ // naked disables ALL checks (incl. protected paths & the write/edit tools) → pass everything
362
+ if (currentMode === "naked") return;
363
+
333
364
  // Resolve the real cwd first (cwd may itself be a symlink), then the real target path,
334
365
  // preventing symlink escape to protected locations and symlink-cwd false positives
335
366
  const realCwd = resolveReal(ctx.cwd);
@@ -434,6 +465,14 @@ function classifySegment(
434
465
  // Recursion depth guard (bash -c / eval nested too deep to statically check → conservative confirm)
435
466
  if (depth > 4) return { kind: "confirm" };
436
467
 
468
+ // naked: pass everything EXCEPT system-destructive Block-group commands, which are confirmed
469
+ // (protected paths, write/edit, git destructive, truncate, etc. all pass in naked mode)
470
+ if (currentMode === "naked") {
471
+ return dangerousLevel(trimmed) === "block"
472
+ ? { kind: "confirm" }
473
+ : { kind: "pass" };
474
+ }
475
+
437
476
  // ① Redirect check:
438
477
  // - Write to a protected path (echo x > .env etc.) → block
439
478
  // - "> existing file" (truncate, not >> append, not a device) → confirm
@@ -1270,6 +1309,34 @@ async function confirmTrustedSwitch(
1270
1309
  );
1271
1310
  }
1272
1311
 
1312
+ /** Double confirmation before switching to naked: disables ALL protection (incl. protected paths, destructive commands, and write/edit checks) */
1313
+ async function confirmNakedSwitch(
1314
+ ctx: ExtensionCommandContext,
1315
+ ): Promise<boolean> {
1316
+ // No UI (headless) cannot confirm → conservatively refuse the switch
1317
+ if (!ctx.hasUI) return false;
1318
+ const first = await ctx.ui.confirm(
1319
+ "⚠️ Switch to NAKED mode?",
1320
+ "naked passes nearly everything: protected paths (.env/.ssh/keys), write/edit tool checks, git\ndestructive ops, truncation, and outside deletes/overwrites are no longer blocked or prompted.\nOnly system-destructive commands (mkfs/reboot/bulk-delete/block-device writes) are still\nconfirmed — everything else is allowed without a prompt.",
1321
+ );
1322
+ if (!first) return false;
1323
+ // Second, final confirmation — makes an accidental /guard naked far less likely
1324
+ return ctx.ui.confirm(
1325
+ "⚠️⚠️ FINAL confirmation — disable ALL protection?",
1326
+ "This is the final step. After this, path-guard passes nearly every operation with no blocking and\nno confirmation, including writes to protected paths and git destructive / truncate / outside\ndelete operations. Only system-destructive commands (mkfs/reboot/bulk-delete/block-device\nwrites) will still prompt for confirmation.\n\nOnly switch if you are certain you want minimal protection.",
1327
+ );
1328
+ }
1329
+
1330
+ /** Mode-switch confirmation: trusted → single warn; naked → double warn; others → no confirmation */
1331
+ async function confirmModeSwitch(
1332
+ mode: GuardMode,
1333
+ ctx: ExtensionCommandContext,
1334
+ ): Promise<boolean> {
1335
+ if (mode === "trusted") return confirmTrustedSwitch(ctx);
1336
+ if (mode === "naked") return confirmNakedSwitch(ctx);
1337
+ return true;
1338
+ }
1339
+
1273
1340
  async function askConfirm(
1274
1341
  ctx: ExtensionContext,
1275
1342
  message: string,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@yaosu/pi-path-guard",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
- "description": "Path Guard for pi — blocks destructive commands & path overwrites, protects .env/keys, with strict/normal/loose/trusted guard modes (/guard). pi 防误删/防误覆盖扩展,支持 4 种防护模式。",
5
+ "description": "Path Guard for pi — blocks destructive commands & path overwrites, protects .env/keys, with strict/normal/loose/trusted/naked guard modes (/guard). pi 防误删/防误覆盖扩展,支持 5 种防护模式。",
6
6
  "keywords": [
7
7
  "pi-package",
8
8
  "pi-extension",