@yaosu/pi-path-guard 1.0.0 → 1.1.0
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 +21 -18
- package/extensions/path-guard.ts +68 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,32 +33,35 @@ 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
|
|
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
39
|
|
|
40
40
|
### Guard mode matrix / 防护模式矩阵
|
|
41
41
|
|
|
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* |
|
|
42
|
+
| Checkpoint / 判定点 | strict | normal | loose | trusted | naked |
|
|
43
|
+
| --- | --- | --- | --- | --- | --- |
|
|
44
|
+
| Protected paths (.env/.ssh/keys/credentials) / 受保护路径 | block | block | block | block | pass |
|
|
45
|
+
| Block group (mkfs/reboot/block-device writes/bulk delete) / Block 组危险命令 | block | block | block | block | confirm |
|
|
46
|
+
| Confirm group (sudo/ssh/chmod 777 …) / Confirm 组 | block | confirm | confirm | confirm | pass |
|
|
47
|
+
| git destructive (reset --hard/clean -f …) / git 破坏性 | confirm | confirm | confirm | confirm | pass |
|
|
48
|
+
| In-project write/edit/new / 项目内写/改/新建 | confirm | pass | pass | pass | pass |
|
|
49
|
+
| In-project delete / 项目内删除 | confirm | confirm | pass | pass | pass |
|
|
50
|
+
| Outside write (new file) / 项目外写新文件 | confirm | confirm | pass | pass | pass |
|
|
51
|
+
| Outside overwrite existing / 项目外覆盖已存在 | block | block | confirm | pass | pass |
|
|
52
|
+
| Outside delete ordinary / 项目外删除普通文件 | block | block | confirm | pass | pass |
|
|
53
|
+
| `>` truncate existing file / 截断已有文件 | confirm | confirm | confirm | confirm | pass |
|
|
54
|
+
| cwd=HOME write / HOME 目录写 | confirm | confirm | pass | pass | pass |
|
|
55
|
+
| No UI (headless) / 无交互界面 | block* | block* | block* | block* | pass |
|
|
56
56
|
|
|
57
57
|
*block = denied directly, no confirmation opportunity / 直接阻止,无确认机会;confirm = prompt / 弹窗询问;pass = allow / 放行;\*headless: items that would be confirmed are blocked instead / 无 UI 时需确认项一律阻止
|
|
58
58
|
|
|
59
|
+
> ⚠️ **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.
|
|
60
|
+
> ⚠️ **裸奔模式**:除系统级破坏命令外几乎全部放行——受保护路径、write/edit 工具检查、git 破坏性、截断、外部删除/覆盖均放行,无 UI 下也放行;但系统级破坏命令(mkfs/reboot/批量删除/写块设备)仍会**弹窗询问**。切换需要**两级确认**(两次弹窗)。仅当你需要最少的路径守护干扰时使用。
|
|
61
|
+
|
|
59
62
|
### Core capabilities / 核心能力
|
|
60
63
|
|
|
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 —
|
|
64
|
+
- **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
65
|
- **Block group / Block 组危险命令**: `mkfs.*` / `mkswap` / `poweroff` / `reboot` / `shutdown` / `dd` to block devices / `> /dev/sdX` / `find -delete` / `find -exec rm` / `xargs rm`
|
|
63
66
|
- **Confirm group / Confirm 组**: `sudo` / `doas` / `pkexec` / `chmod 777` / `ssh` / `scp` / `sftp` / `rsh` / `telnet` / `wget -O /dev/null`
|
|
64
67
|
- **Overwrite detection / 覆盖检测**: `mv` / `cp` / `install` / `tee` / `ln -f` / `rsync --delete` on existing targets, classified by in/out project — 目标已存在时按内外策略处理
|
|
@@ -69,13 +72,13 @@ After installing, run `/reload` or restart pi. 安装后 `/reload` 或重启 pi
|
|
|
69
72
|
|
|
70
73
|
## Development / 开发与测试
|
|
71
74
|
|
|
72
|
-
Automated tests (
|
|
75
|
+
Automated tests (96 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, and naked-mode double confirmation:
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
78
|
cd tests && node --experimental-strip-types test-pathguard.ts
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
自动化测试(
|
|
81
|
+
自动化测试(96 断言)模拟 pi API 加载真实扩展,覆盖 5 种模式 × 受保护路径 / 危险命令 / 截断 / git 破坏性等判定矩阵,以及 `/guard` 命令交互、trusted 确认与 naked 两级确认流程:
|
|
79
82
|
|
|
80
83
|
```bash
|
|
81
84
|
cd tests && node --experimental-strip-types test-pathguard.ts
|
package/extensions/path-guard.ts
CHANGED
|
@@ -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 {
|
|
@@ -185,8 +190,8 @@ const HOME = homedir();
|
|
|
185
190
|
|
|
186
191
|
// ─── Guard Modes ─────────────────────────────────────────────────────
|
|
187
192
|
|
|
188
|
-
/** Guard mode: strict (full) / normal (default) / loose (relaxed) / trusted (most permissive) */
|
|
189
|
-
type GuardMode = "strict" | "normal" | "loose" | "trusted";
|
|
193
|
+
/** Guard mode: strict (full) / normal (default) / loose (relaxed) / trusted (most permissive) / naked (no protection) */
|
|
194
|
+
type GuardMode = "strict" | "normal" | "loose" | "trusted" | "naked";
|
|
190
195
|
|
|
191
196
|
/** Current session guard mode (switched via /guard; reset to normal on session_start) */
|
|
192
197
|
let currentMode: GuardMode = "normal";
|
|
@@ -197,6 +202,7 @@ const GUARD_MODES: readonly GuardMode[] = [
|
|
|
197
202
|
"normal",
|
|
198
203
|
"loose",
|
|
199
204
|
"trusted",
|
|
205
|
+
"naked",
|
|
200
206
|
];
|
|
201
207
|
|
|
202
208
|
/** Whether a string is a valid guard mode (for /guard argument validation) */
|
|
@@ -214,24 +220,26 @@ const MODE_DESCRIPTIONS: Record<GuardMode, string> = {
|
|
|
214
220
|
"Loose: pass new-file writes & deletes, confirm overwrites / 放宽:新建/删除免问,覆盖需确认",
|
|
215
221
|
trusted:
|
|
216
222
|
"Trusted: pass overwrites & ordinary-file deletes / 最宽松:覆盖/删除普通文件也免问",
|
|
223
|
+
naked:
|
|
224
|
+
"Naked: pass everything except system-destructive cmds (confirmed) / 裸奔:除系统级破坏命令外全部放行(破坏命令弹窗询问)",
|
|
217
225
|
};
|
|
218
226
|
|
|
219
227
|
/** Full decision matrix (shown as the /guard picker title, English only) */
|
|
220
228
|
const MODE_MATRIX = [
|
|
221
229
|
"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",
|
|
230
|
+
" Checkpoint strict normal loose trusted naked",
|
|
231
|
+
" Protected paths .env/.ssh/keys B B B B .",
|
|
232
|
+
" System-destructive mkfs/reboot B B B B ?",
|
|
233
|
+
" Privilege/remote sudo/ssh/chmod777 B ? ? ? .",
|
|
234
|
+
" Git destructive reset --hard ? ? ? ? .",
|
|
235
|
+
" In-project write/edit/new ? . . . .",
|
|
236
|
+
" In-project delete ? ? . . .",
|
|
237
|
+
" Outside write (new file) ? ? . . .",
|
|
238
|
+
" Outside overwrite existing B B ? . .",
|
|
239
|
+
" Outside delete ordinary B B ? . .",
|
|
240
|
+
" Truncate existing > file ? ? ? ? .",
|
|
241
|
+
" HOME dir write ? ? . . .",
|
|
242
|
+
" No UI (headless) B B B B .",
|
|
235
243
|
].join("\n");
|
|
236
244
|
|
|
237
245
|
/** Guard verdict: { block, reason } to block / undefined to allow (askConfirm returns a Promise) */
|
|
@@ -251,15 +259,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
259
|
// /guard slash command: view / switch guard mode
|
|
252
260
|
pi.registerCommand("guard", {
|
|
253
261
|
description:
|
|
254
|
-
"Path Guard modes: /guard shows the current mode, /guard <strict|normal|loose|trusted> switches",
|
|
262
|
+
"Path Guard modes: /guard shows the current mode, /guard <strict|normal|loose|trusted|naked> switches",
|
|
255
263
|
handler: async (args, ctx) => {
|
|
256
264
|
const m = args?.trim().toLowerCase() ?? "";
|
|
257
265
|
|
|
258
|
-
// Valid argument → switch directly (shortcut, no picker); trusted
|
|
266
|
+
// Valid argument → switch directly (shortcut, no picker); trusted/naked require a warning confirmation
|
|
259
267
|
if (isGuardMode(m)) {
|
|
260
|
-
if (
|
|
268
|
+
if (!(await confirmModeSwitch(m, ctx))) {
|
|
261
269
|
ctx.ui.notify(
|
|
262
|
-
|
|
270
|
+
`Cancelled: switching to ${m} requires confirmation`,
|
|
263
271
|
"info",
|
|
264
272
|
);
|
|
265
273
|
return;
|
|
@@ -293,9 +301,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
301
|
}
|
|
294
302
|
const picked = chosen.split(/\s+/)[0] as GuardMode;
|
|
295
303
|
if (isGuardMode(picked)) {
|
|
296
|
-
if (
|
|
304
|
+
if (!(await confirmModeSwitch(picked, ctx))) {
|
|
297
305
|
ctx.ui.notify(
|
|
298
|
-
|
|
306
|
+
`Cancelled: switching to ${picked} requires confirmation`,
|
|
299
307
|
"info",
|
|
300
308
|
);
|
|
301
309
|
return;
|
|
@@ -330,6 +338,9 @@ function checkWriteEdit(
|
|
|
330
338
|
const path = input.path;
|
|
331
339
|
if (!path) return;
|
|
332
340
|
|
|
341
|
+
// naked disables ALL checks (incl. protected paths & the write/edit tools) → pass everything
|
|
342
|
+
if (currentMode === "naked") return;
|
|
343
|
+
|
|
333
344
|
// Resolve the real cwd first (cwd may itself be a symlink), then the real target path,
|
|
334
345
|
// preventing symlink escape to protected locations and symlink-cwd false positives
|
|
335
346
|
const realCwd = resolveReal(ctx.cwd);
|
|
@@ -434,6 +445,14 @@ function classifySegment(
|
|
|
434
445
|
// Recursion depth guard (bash -c / eval nested too deep to statically check → conservative confirm)
|
|
435
446
|
if (depth > 4) return { kind: "confirm" };
|
|
436
447
|
|
|
448
|
+
// naked: pass everything EXCEPT system-destructive Block-group commands, which are confirmed
|
|
449
|
+
// (protected paths, write/edit, git destructive, truncate, etc. all pass in naked mode)
|
|
450
|
+
if (currentMode === "naked") {
|
|
451
|
+
return dangerousLevel(trimmed) === "block"
|
|
452
|
+
? { kind: "confirm" }
|
|
453
|
+
: { kind: "pass" };
|
|
454
|
+
}
|
|
455
|
+
|
|
437
456
|
// ① Redirect check:
|
|
438
457
|
// - Write to a protected path (echo x > .env etc.) → block
|
|
439
458
|
// - "> existing file" (truncate, not >> append, not a device) → confirm
|
|
@@ -1270,6 +1289,34 @@ async function confirmTrustedSwitch(
|
|
|
1270
1289
|
);
|
|
1271
1290
|
}
|
|
1272
1291
|
|
|
1292
|
+
/** Double confirmation before switching to naked: disables ALL protection (incl. protected paths, destructive commands, and write/edit checks) */
|
|
1293
|
+
async function confirmNakedSwitch(
|
|
1294
|
+
ctx: ExtensionCommandContext,
|
|
1295
|
+
): Promise<boolean> {
|
|
1296
|
+
// No UI (headless) cannot confirm → conservatively refuse the switch
|
|
1297
|
+
if (!ctx.hasUI) return false;
|
|
1298
|
+
const first = await ctx.ui.confirm(
|
|
1299
|
+
"⚠️ Switch to NAKED mode?",
|
|
1300
|
+
"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.",
|
|
1301
|
+
);
|
|
1302
|
+
if (!first) return false;
|
|
1303
|
+
// Second, final confirmation — makes an accidental /guard naked far less likely
|
|
1304
|
+
return ctx.ui.confirm(
|
|
1305
|
+
"⚠️⚠️ FINAL confirmation — disable ALL protection?",
|
|
1306
|
+
"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.",
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** Mode-switch confirmation: trusted → single warn; naked → double warn; others → no confirmation */
|
|
1311
|
+
async function confirmModeSwitch(
|
|
1312
|
+
mode: GuardMode,
|
|
1313
|
+
ctx: ExtensionCommandContext,
|
|
1314
|
+
): Promise<boolean> {
|
|
1315
|
+
if (mode === "trusted") return confirmTrustedSwitch(ctx);
|
|
1316
|
+
if (mode === "naked") return confirmNakedSwitch(ctx);
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1273
1320
|
async function askConfirm(
|
|
1274
1321
|
ctx: ExtensionContext,
|
|
1275
1322
|
message: string,
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaosu/pi-path-guard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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 防误删/防误覆盖扩展,支持
|
|
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",
|