@andares/pi-toolkits 0.8.0 → 0.9.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 +72 -5
- package/README.zh-CN.md +71 -5
- package/package.json +7 -3
- package/src/features/ask/index.test.ts +118 -0
- package/src/features/ask/index.ts +31 -13
- package/src/features/autostart/autostart.test.ts +113 -2
- package/src/features/autostart/autostart.ts +28 -2
- package/src/features/autostart/index.ts +138 -49
- package/src/features/permission/constants.test.ts +60 -0
- package/src/features/permission/constants.ts +158 -0
- package/src/features/permission/index.test.ts +340 -0
- package/src/features/permission/index.ts +333 -0
- package/src/features/permission/modes.test.ts +66 -0
- package/src/features/permission/modes.ts +82 -0
- package/src/features/permission/panel.ts +118 -0
- package/src/features/permission/paths.test.ts +111 -0
- package/src/features/permission/paths.ts +126 -0
- package/src/features/permission/policy.test.ts +237 -0
- package/src/features/permission/policy.ts +338 -0
- package/src/features/permission/prompt.ts +146 -0
- package/src/features/permission/session-rules.ts +99 -0
- package/src/features/permission/units.test.ts +205 -0
- package/src/features/permission/units.ts +995 -0
- package/src/index.ts +15 -2
- package/src/lib/gates.test.ts +171 -0
- package/src/lib/gates.ts +153 -0
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
[](https://github.com/earendil-works/pi)
|
|
13
13
|
[](https://github.com/andares/pi-toolkits)
|
|
14
14
|
|
|
15
|
-
In a nutshell:
|
|
15
|
+
In a nutshell: seven ready-to-use capabilities — **read-only consult mode** `ask`, **session-scoped boundary permission** `ask-perm`, **model favorites** `favorites`, **prompt stash** `stash`, **third-party code review** `third-review`, **dedicated compaction model** `compact-model` and **session startup automation** `autostart` — with more to come.
|
|
16
16
|
|
|
17
|
-
**Requirements**: pi `>=1.0.0 <1.12.0` — the four most recently released minor lines (1.0.x / 1.1.x / 1.2.x / 1.3.x) are supported best-effort, and the upper bound encodes the forward probe: on every host release we check up to **8 minor lines** beyond the newest one (now 1.4 … 1.11) and never claim past that cap. **Measured natural compatibility: 1.3.0** (the newest release at the time of writing — nothing newer exists to probe yet). A single command, `pnpm check:compat`, derives that version set from the registry and runs, per version, in a sandbox under the system temp dir with the whole `@earendil-works/*` family pinned to the version under test: install → `tsc --noEmit` → full test suite → keybinding audit → hash-compare of the host `dist` files this package patches or mirrors. Results over 1.0.0 → 1.3.0 (all 8 published versions): typecheck,
|
|
17
|
+
**Requirements**: pi `>=1.0.0 <1.12.0` — the four most recently released minor lines (1.0.x / 1.1.x / 1.2.x / 1.3.x) are supported best-effort, and the upper bound encodes the forward probe: on every host release we check up to **8 minor lines** beyond the newest one (now 1.4 … 1.11) and never claim past that cap. **Measured natural compatibility: 1.3.0** (the newest release at the time of writing — nothing newer exists to probe yet). A single command, `pnpm check:compat`, derives that version set from the registry and runs, per version, in a sandbox under the system temp dir with the whole `@earendil-works/*` family pinned to the version under test: install → `tsc --noEmit` → full test suite → keybinding audit → hash-compare of the host `dist` files this package patches or mirrors. Results over 1.0.0 → 1.3.0 (all 8 published versions): typecheck, 397 tests and the keybinding audit pass on every one; the tracked host files drift only in `core/agent-session.js` (1.3.0 changed the tool-restore path; `cycleModel` untouched), `core/keybindings.js` and `core/compaction/utils.js` (both changed in 1.2.0, table semantics for our keys unchanged), and `extensions/types.d.ts` (additive) — `ModelSelectorComponent`, `core/compaction/compaction.js` and `core/usage-totals.js` are byte-identical across the whole window. Smoke-loading on real pi 1.3.0 produces no warnings. Note: pi ≥ 0.99 hosts require `@earendil-works/pi-tui` to be declared in `peerDependencies` as `"*"` (provided by the host); this package declares it as required.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -48,6 +48,7 @@ After installing, verify in any pi session:
|
|
|
48
48
|
- [Installation](#install)
|
|
49
49
|
- [Features at a glance](#features)
|
|
50
50
|
- [🛡️ ask — read-only consult mode](#ask)
|
|
51
|
+
- [🔐 permission — session-scoped boundary checks](#permission)
|
|
51
52
|
- [⭐ favorites — model favorites](#favorites)
|
|
52
53
|
- [📥 stash — prompt stash](#stash)
|
|
53
54
|
- [🔎 review — third-party code review](#review)
|
|
@@ -66,6 +67,7 @@ After installing, verify in any pi session:
|
|
|
66
67
|
| Feature | Entry point | One-line description |
|
|
67
68
|
| --- | --- | --- |
|
|
68
69
|
| 🛡️ **ask** read-only consult mode | `/ask` | enter read-only Q&A mode: `write`/`edit`/`powershell` hard-disabled, `bash` runs in a read-only sandbox, `grep`/`find`/`ls` activated |
|
|
70
|
+
| 🔐 **permission** session-scoped boundary checks | `/ask-perm` | four in/out × read/write modes, off by default and never persisted; outside writes ask, credentials are denied, destructive commands always ask; composes with `/ask` |
|
|
69
71
|
| ⭐ **favorites** model favorites | `ctrl+A` / `ctrl+J` inside `/model` | favorite the models you use most (marked in bold bright yellow), `ctrl+p` cycles only among favorites |
|
|
70
72
|
| 📥 **stash** prompt stash | `ctrl+alt+y` | a stash slot for one prompt: press to swap, press twice to stash and clear the input box |
|
|
71
73
|
| 🔎 **review** third-party code review | `/third-review` | summon the configured review model to check + fix the task just finished, focusing on "is this round's requirement implemented / is anything missing / is anything wrong" |
|
|
@@ -92,6 +94,55 @@ Enter with `/ask` (press it again to exit). Meant for the case where you only wa
|
|
|
92
94
|
|
|
93
95
|
---
|
|
94
96
|
|
|
97
|
+
## 🔐 permission — session-scoped boundary checks
|
|
98
|
+
|
|
99
|
+
<a name="permission"></a>
|
|
100
|
+
|
|
101
|
+
`/ask-perm` turns on boundary checks for the **current session only**. Every piece of state — the on/off flag, the four modes, the session grants — lives in the extension's memory. Nothing is written to disk, the feature is off by default, and it leaves no status behind when disabled. Toggling it on can never silently follow you into the next session.
|
|
102
|
+
|
|
103
|
+
**Enable**
|
|
104
|
+
|
|
105
|
+
- `/ask-perm` — opens the mode panel. The first row applies on Enter; the four rows cycle with space. Cursor starts at the top, so `/ask-perm` + Enter enables the defaults.
|
|
106
|
+
- `/ask-perm allow allow allow ask` — positional form (`in-read in-write out-read out-write`), enables immediately without a dialog. Missing arguments fall back to the defaults. This is the form autostart uses.
|
|
107
|
+
- `/ask-perm` again — turns it off.
|
|
108
|
+
|
|
109
|
+
**Defaults**: in-project read/write allowed, outside read allowed, outside write asks.
|
|
110
|
+
|
|
111
|
+
| Scope | reads | writes / exec |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| inside the project (working directory + `/add-dir` directories) | `in read` | `in write` |
|
|
114
|
+
| outside the project | `out read` | `out write` |
|
|
115
|
+
|
|
116
|
+
`exec` is folded into `write`: any command that changes state counts as a write.
|
|
117
|
+
|
|
118
|
+
**What is inspected**
|
|
119
|
+
|
|
120
|
+
- **bash** is decomposed into simple commands — `&&`, `||`, `;`, pipes, `$(…)`, backticks, `bash -c`, `eval`, the common indirection wrappers (`sudo`, `env`, `xargs`, `find -exec`, …), heredocs — and judged per command. `cd` folds into the commands after it; the strictest result wins, so `cd /tmp && rm x` cannot ride on the first segment.
|
|
121
|
+
- **path-bearing tools** (`read` / `write` / `edit` / `grep` / `find` / `ls`) are judged by their own path; every other tool is judged by the working directory.
|
|
122
|
+
- **unresolvable input** — a non-literal `cd` or `git -C`, an unknown shell variable in a path, an unterminated heredoc or substitution — is asked about regardless of mode (fail closed), never guessed.
|
|
123
|
+
- **`git -C <dir>`** is resolved like a local `cd`: relative path arguments (and the repository) are judged against `<dir>`, not the working directory.
|
|
124
|
+
|
|
125
|
+
**Answers**: `a` allow once · `s` allow this directory for the session · `d` deny · `f` deny this directory for the session. Session grants are kept in memory as `(read|write, directory)`, matched by containment, and cleared when the mode is turned off or the session ends. A session grant outranks the destructive table — once you have allowed an operation for the session, repeating it does not ask again. A command the parser cannot resolve is granted by its exact text instead, so repeating the same opaque command does not ask again.
|
|
126
|
+
|
|
127
|
+
**Fixed safety tables** (independent of the four modes):
|
|
128
|
+
|
|
129
|
+
- **credentials are denied in every mode** — `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.pi/agent/auth*.json`, `.env` files (templates excepted). Without this, `out read = allow` would hand out `~/.ssh/id_rsa`.
|
|
130
|
+
- **destructive commands always ask** — `rm` of `/`, the home directory, or the working directory; `git reset --hard`; `git clean -f`; `git push --force`; package `publish`; `dd` to a raw device; `mkfs*`; shutdown/reboot; `chmod -R 777`; anything through `sudo`. Scope alone cannot express "destructive".
|
|
131
|
+
|
|
132
|
+
**Composition with `/ask`**: the two features are independent gates on one bus. Each answers only its own question — ask: "does this mutate?", permission: "does this cross the boundary?" — and either can block. Ask mode still denies credential reads, and permission still applies while the read-only sandbox is active. Neither feature needs to know the other exists.
|
|
133
|
+
|
|
134
|
+
**Enable it for every session** (the state itself is never persisted — this is the opt-in):
|
|
135
|
+
|
|
136
|
+
```toml
|
|
137
|
+
# <project>/.pi/autostart.toml, or <agent-dir>/autostart.toml for all projects
|
|
138
|
+
[command]
|
|
139
|
+
run = ["/ask-perm allow allow allow ask"]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> **Implementation notes**: state is reset on `session_start` and cleared on `session_shutdown`; the footer shows `perm i:A/A o:A/ask` while on (`A` allow, `a` ask, `D` deny). Subagent sessions start with their own state and do not inherit the parent's mode — when child sessions must be gated too, enable it through the project's autostart file. POSIX hosts (Linux / macOS / WSL) are the tested target; native Windows Git-Bash path spellings are approximated, not verified. bash decomposition uses `shell-quote`; unknown variables expand to a sentinel that makes the unit ask instead of silently becoming an empty path. Added directories are read from pi-add-dir's session entry (`add-dir:state`). Heuristic, not a sandbox: project-internal destructive commands are covered by the fixed table, not by a sandbox. One known blind spot: a *read* command's bare filename operand (`cat notes.txt`) is not collected as a target, so with `out read = ask` only absolute-path-shaped read operands are questioned — reads never write, so the four modes' write side is unaffected.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
95
146
|
## ⭐ favorites — model favorites
|
|
96
147
|
|
|
97
148
|
<a name="favorites"></a>
|
|
@@ -247,15 +298,27 @@ run = [
|
|
|
247
298
|
|
|
248
299
|
No file = feature disabled, zero overhead, silently skipped.
|
|
249
300
|
|
|
301
|
+
**Two sources, executed in order, never merged**:
|
|
302
|
+
|
|
303
|
+
| Source | Path | Trust gate |
|
|
304
|
+
| --- | --- | --- |
|
|
305
|
+
| global (operator defaults) | `<agent-dir>/autostart.toml` (default `~/.pi/agent/autostart.toml`) | none — the file belongs to the operator |
|
|
306
|
+
| project | `<project root>/.pi/autostart.toml` | requires project trust |
|
|
307
|
+
|
|
308
|
+
The global list runs first, so a project can override an earlier setting (for example a later `/ask-perm` with different arguments). Use the global file only when you want the same commands in every project; per-project files remain the recommended default.
|
|
309
|
+
|
|
250
310
|
**When it runs**:
|
|
251
311
|
|
|
252
312
|
| session_start reason | Replayed | Why |
|
|
253
313
|
| --- | --- | --- |
|
|
254
|
-
| `startup` / `new
|
|
255
|
-
| `
|
|
256
|
-
| `
|
|
314
|
+
| `startup` / `new`, empty session | ✅ | a new session, replay as declared |
|
|
315
|
+
| `startup` on a session that already has history | ❌ | that is a process started on an existing session (`pi -c` / `-r` / `--session` / `--fork`), and its branch already carries the state a replay would rebuild |
|
|
316
|
+
| `fork` (in-process) | ✅ | the branch point may predate the config's appearance |
|
|
317
|
+
| `resume` (in-process `/resume`) | ❌ | the restored branch already carries the mount state from its time, replaying would conflict |
|
|
257
318
|
| `reload` | ❌ | extensions are rebuilt, replaying would duplicate side effects |
|
|
258
319
|
|
|
320
|
+
> The `startup` split is not cosmetic: pi reports `startup` for **every** process start, including one that opens an existing session. `resume` is emitted only for in-process session switches, never for `pi -c`. Lines 1–2 of the table are therefore decided by the session's content (does it already hold a message?), not by the reason alone.
|
|
321
|
+
|
|
259
322
|
**Safety gates** (in order):
|
|
260
323
|
|
|
261
324
|
1. **project trust** — no command runs automatically in an untrusted directory (a warning is shown); a config parse failure likewise prevents execution (an error is shown)
|
|
@@ -288,6 +351,7 @@ pnpm check:compat # verify the declared pi range: last 4 minor lines + 8-
|
|
|
288
351
|
src/
|
|
289
352
|
├── index.ts # entry point: aggregates feature modules
|
|
290
353
|
├── lib/ # shared infrastructure (tool-set snapshot/restore,
|
|
354
|
+
│ # gate bus composing tool_call policies,
|
|
291
355
|
│ # model-key resolution shared by review & compact)
|
|
292
356
|
└── features/ # feature modules — one directory per feature
|
|
293
357
|
├── ask/ # ask mode: command + state machine, bash sandbox,
|
|
@@ -299,6 +363,9 @@ src/
|
|
|
299
363
|
│ # tests
|
|
300
364
|
├── favorites/ # model favorites: selector + cycle patches,
|
|
301
365
|
│ # persisted store, tests
|
|
366
|
+
├── permission/ # session-scoped boundary permission: modes, panel,
|
|
367
|
+
│ # bash units, policy, session grants, prompt,
|
|
368
|
+
│ # tests
|
|
302
369
|
├── review/ # third-party review: input/command triggers,
|
|
303
370
|
│ # review-model config store, prompt, tests
|
|
304
371
|
└── stash/ # prompt stash: hotkey state machine + tests
|
package/README.zh-CN.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
[](https://github.com/earendil-works/pi)
|
|
13
13
|
[](https://github.com/andares/pi-toolkits)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
一句话介绍:目前包含七个开箱即用的功能——**只读咨询模式** `ask`、**会话级边界权限** `ask-perm`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`、**会话启动自动化** `autostart`,后续持续追加。
|
|
16
16
|
|
|
17
|
-
**版本要求**:pi `>=1.0.0 <1.12.0` —— 最近 4 个中版本线(1.0.x / 1.1.x / 1.2.x / 1.3.x)尽力支持,上界编码「向后探针」的硬上限:宿主每发一版,我们最多向后查 **8 条中版本线**(当前 1.4 … 1.11),再晚不宣称。**实测自然兼容最高 1.3.0**(写此文时的最新版本,更新的还没发布、无物可探)。一条命令 `pnpm check:compat` 从 registry 推导目标集合,逐版本在系统临时目录沙箱中(把整个 `@earendil-works/*` 家族钉在待测版本)跑:安装 → `tsc --noEmit` → 全量测试 → 键位审计 → 对 patch/mirror 的宿主 `dist` 文件做哈希比对。1.0.0 → 1.3.0 全部 8 个已发布版本:typecheck、
|
|
17
|
+
**版本要求**:pi `>=1.0.0 <1.12.0` —— 最近 4 个中版本线(1.0.x / 1.1.x / 1.2.x / 1.3.x)尽力支持,上界编码「向后探针」的硬上限:宿主每发一版,我们最多向后查 **8 条中版本线**(当前 1.4 … 1.11),再晚不宣称。**实测自然兼容最高 1.3.0**(写此文时的最新版本,更新的还没发布、无物可探)。一条命令 `pnpm check:compat` 从 registry 推导目标集合,逐版本在系统临时目录沙箱中(把整个 `@earendil-works/*` 家族钉在待测版本)跑:安装 → `tsc --noEmit` → 全量测试 → 键位审计 → 对 patch/mirror 的宿主 `dist` 文件做哈希比对。1.0.0 → 1.3.0 全部 8 个已发布版本:typecheck、397 个测试、键位审计全绿;被跟踪的宿主文件仅在 `core/agent-session.js`(1.3.0 改了 tool-restore 路径,`cycleModel` 未动)、`core/keybindings.js` 与 `core/compaction/utils.js`(二者都是 1.2.0 变的,我们按键的解析结果不变)、`extensions/types.d.ts`(纯增量)上有差异 —— `ModelSelectorComponent`、`core/compaction/compaction.js` 与 `core/usage-totals.js` 在窗口内字节级不变。真实 pi 1.3.0 冒烟加载无警告。注:pi ≥ 0.99 的宿主要求 `@earendil-works/pi-tui` 声明为 peerDependencies `"*"`(由宿主提供),本包已按要求声明。
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -48,6 +48,7 @@ pi -e ./src/index.ts
|
|
|
48
48
|
- [安装](#install)
|
|
49
49
|
- [功能总览](#features)
|
|
50
50
|
- [🛡️ ask — 只读咨询模式](#ask)
|
|
51
|
+
- [🔐 permission — 会话级边界权限](#permission)
|
|
51
52
|
- [⭐ favorites — 模型收藏](#favorites)
|
|
52
53
|
- [📥 stash — 提示词暂存](#stash)
|
|
53
54
|
- [🔎 review — 第三方代码评审](#review)
|
|
@@ -66,6 +67,7 @@ pi -e ./src/index.ts
|
|
|
66
67
|
| 功能 | 入口 | 一句话说明 |
|
|
67
68
|
| --- | --- | --- |
|
|
68
69
|
| 🛡️ **ask** 只读咨询模式 | `/ask` | 进入只读问答模式,`write`/`edit`/`powershell` 硬禁用,`bash` 只读沙箱,临时启用 `grep`/`find`/`ls` |
|
|
70
|
+
| 🔐 **permission** 会话级边界权限 | `/ask-perm` | 项目内/外 × 读/写四格模式,默认关闭且从不落盘;项目外写询问、凭据路径硬拒、破坏性命令必问;与 `/ask` 并行组合 |
|
|
69
71
|
| ⭐ **favorites** 模型收藏 | `/model` 内 `ctrl+A` / `ctrl+J` | 收藏常用模型(加粗亮黄标记),`ctrl+p` 仅在收藏间循环 |
|
|
70
72
|
| 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
|
|
71
73
|
| 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
|
|
@@ -92,6 +94,55 @@ pi -e ./src/index.ts
|
|
|
92
94
|
|
|
93
95
|
---
|
|
94
96
|
|
|
97
|
+
## 🔐 permission — 会话级边界权限
|
|
98
|
+
|
|
99
|
+
<a name="permission"></a>
|
|
100
|
+
|
|
101
|
+
`/ask-perm` 只在**当前会话**开启边界检查。开关状态、四项模式、会话授权全部只存在扩展内存里:不落盘、默认关闭、关闭后不留任何状态。开启一次不会悄悄跟到下一个会话。
|
|
102
|
+
|
|
103
|
+
**开启**
|
|
104
|
+
|
|
105
|
+
- `/ask-perm` —— 打开模式面板。顶行按回车即应用,四行用空格循环、↑↓ 移动;光标默认在最上面,所以「输入指令 + 回车」就是按默认值开启
|
|
106
|
+
- `/ask-perm allow allow allow ask` —— 位置参数形式(`in-read in-write out-read out-write`),不弹窗直接开启;没给全的参数回落到默认值。这也是 autostart 使用的形式
|
|
107
|
+
- 再输入一次 `/ask-perm` —— 关闭
|
|
108
|
+
|
|
109
|
+
**默认值**:项目内读/写全放行,项目外读放行,项目外写询问。
|
|
110
|
+
|
|
111
|
+
| 作用域 | 读 | 写 / 执行 |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| 项目内(工作目录 + `/add-dir` 加入的目录) | `in read` | `in write` |
|
|
114
|
+
| 项目外 | `out read` | `out write` |
|
|
115
|
+
|
|
116
|
+
`exec` 折进 `write`:任何产生变更的执行都算写。
|
|
117
|
+
|
|
118
|
+
**检查方式**
|
|
119
|
+
|
|
120
|
+
- **bash** 先拆成简单命令 —— `&&`、`||`、`;`、管道、`$(…)`、反引号、`bash -c`、`eval`、常见间接包装(`sudo`、`env`、`xargs`、`find -exec`…)、heredoc —— 再逐条判定;`cd` 向前折叠,取最严格的结果,所以 `cd /tmp && rm x` 不会靠第一段蒙混过关
|
|
121
|
+
- **带路径的工具**(`read` / `write` / `edit` / `grep` / `find` / `ls`)按自己的路径判定;其它工具按工作目录判定
|
|
122
|
+
- **无法解析的输入** —— 非字面量 `cd` 或 `git -C`、路径里的未知 shell 变量、未闭合的 heredoc/命令替换 —— 一律询问,不看模式(fail closed),绝不猜测
|
|
123
|
+
- **`git -C <dir>`** 按局部 `cd` 处理:相对路径参数(和仓库本身)按 `<dir>` 判定,而不是工作目录
|
|
124
|
+
|
|
125
|
+
**回答**:`a` 本次允许 · `s` 本会话允许该目录 · `d` 拒绝 · `f` 本会话拒绝该目录。会话授权在内存中按 `(读|写, 目录)` 保存、按包含关系匹配,关闭模式或会话结束时清空;**会话授权优先于破坏性表**——你按了 `s` 之后,重复同一操作不会再弹;解析不了完整路径的命令则按**完整命令文本**授权,重复同一条命令不会再问。
|
|
126
|
+
|
|
127
|
+
**固定安全表**(与四格模式无关):
|
|
128
|
+
|
|
129
|
+
- **凭据路径任何模式都拒绝** —— `~/.ssh`、`~/.gnupg`、`~/.aws`、`~/.pi/agent/auth*.json`、`.env` 文件(模板除外)。没有这一条,默认的「项目外读放行」就等于把 `~/.ssh/id_rsa` 交出去
|
|
130
|
+
- **破坏性命令永远询问** —— 对 `/`、家目录或工作目录的 `rm`;`git reset --hard`;`git clean -f`;`git push --force`;包 `publish`;`dd` 写裸设备;`mkfs*`;关机/重启;`chmod -R 777`;任何经 `sudo` 的命令。作用域本身表达不了「破坏性」
|
|
131
|
+
|
|
132
|
+
**与 `/ask` 的组合**:两者是同一总线上的独立闸门,各自只回答自己的问题 —— ask 回答「会不会产生变更」,permission 回答「有没有越过边界」,任一拦截都生效。所以 ask 模式下凭据仍然读不到,permission 在只读沙箱下也照常工作;两个功能互不需要知道对方存在。
|
|
133
|
+
|
|
134
|
+
**每个会话自动开启**(状态本身不落盘,这就是持久化的替身):
|
|
135
|
+
|
|
136
|
+
```toml
|
|
137
|
+
# <项目>/.pi/autostart.toml;想对所有项目生效则用 <agent-dir>/autostart.toml
|
|
138
|
+
[command]
|
|
139
|
+
run = ["/ask-perm allow allow allow ask"]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> **实现说明**:状态在 `session_start` 重置、`session_shutdown` 清空;开启时 footer 显示 `perm i:A/A o:A/ask`(`A` allow、`a` ask、`D` deny)。子代理会话有自己的状态、不继承父会话的模式;需要子会话也受控时,通过项目 autostart 再开启一次。实测目标是 POSIX 宿主(Linux/macOS/WSL);原生 Windows Git-Bash 的路径拼写只做了近似,未验证。bash 拆解使用 `shell-quote`;未知变量会展开成哨兵而不是空字符串,使该命令转为询问而不是被悄悄当成空路径。add-dir 目录从 pi-add-dir 的会话条目 `add-dir:state` 读取。启发式,不是沙箱:项目内的破坏性操作由固定表兜底,而不是靠沙箱。已知盲区一处:**读命令的裸文件名参数**(`cat notes.txt`)不被收集为目标,所以把 `out read` 调成 `ask` 时只有绝对路径形状的读目标会被问——读本身不会写,四格模式的写侧不受影响。
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
95
146
|
## ⭐ favorites — 模型收藏
|
|
96
147
|
|
|
97
148
|
<a name="favorites"></a>
|
|
@@ -247,15 +298,27 @@ run = [
|
|
|
247
298
|
|
|
248
299
|
无文件 = 功能未启用,零开销、静默跳过。
|
|
249
300
|
|
|
301
|
+
**两个来源,按序执行,不做合并**:
|
|
302
|
+
|
|
303
|
+
| 来源 | 路径 | project trust |
|
|
304
|
+
| --- | --- | --- |
|
|
305
|
+
| 全局(操作者默认) | `<agent-dir>/autostart.toml`(默认 `~/.pi/agent/autostart.toml`) | 不需要 —— 文件属于操作者本人 |
|
|
306
|
+
| 项目 | `<项目根>/.pi/autostart.toml` | 需要 |
|
|
307
|
+
|
|
308
|
+
全局列表先执行,项目列表后执行,因此项目可以覆盖此前的设置(例如后面再执行一条不同参数的 `/ask-perm`)。只有希望所有项目都用同一套命令时才用全局文件;仍然建议以项目级配置为主。
|
|
309
|
+
|
|
250
310
|
**执行时机**:
|
|
251
311
|
|
|
252
312
|
| session_start reason | 是否重放 | 原因 |
|
|
253
313
|
| --- | --- | --- |
|
|
254
|
-
| `startup` / `new
|
|
255
|
-
| `
|
|
256
|
-
| `
|
|
314
|
+
| `startup` / `new`,空会话 | ✅ | 新会话,按声明重放 |
|
|
315
|
+
| `startup`,但会话已有历史 | ❌ | 这是「进程启动即带着旧会话」(`pi -c` / `-r` / `--session` / `--fork`),其分支已携带重放本该重建的状态 |
|
|
316
|
+
| `fork`(进程内) | ✅ | 分支点可能早于配置出现 |
|
|
317
|
+
| `resume`(进程内 `/resume`) | ❌ | 恢复的分支已携带当时的挂载状态,重放反而会冲突 |
|
|
257
318
|
| `reload` | ❌ | 扩展重建,重放会重复副作用 |
|
|
258
319
|
|
|
320
|
+
> `startup` 这一行为什么要拆开:pi 对**每一次进程启动**都报 `startup`,包括「启动即打开旧会话」的情形;`resume` 只在进程内切换会话时发出,`pi -c` 永远不会报它。所以前两行的判定依据是**会话内容**(是否已有消息),不能只看 reason。
|
|
321
|
+
|
|
259
322
|
**安全门**(按序):
|
|
260
323
|
|
|
261
324
|
1. **project trust** —— 未信任目录不自动执行任何命令(warning 提示);配置解析失败同样不执行(error 提示)
|
|
@@ -288,6 +351,7 @@ pnpm check:compat # 校验声明的 pi 区间:最近 4 条中版本线 +
|
|
|
288
351
|
src/
|
|
289
352
|
├── index.ts # 入口:聚合各功能模块
|
|
290
353
|
├── lib/ # 共享基础设施(工具集快照/恢复、
|
|
354
|
+
│ # 组合 tool_call 策略的闸门总线、
|
|
291
355
|
│ # review 与 compact 共用的模型 key 解析)
|
|
292
356
|
└── features/ # 功能模块 —— 一个功能一个目录
|
|
293
357
|
├── ask/ # ask 模式:命令 + 状态机、bash 沙箱、
|
|
@@ -298,6 +362,8 @@ src/
|
|
|
298
362
|
│ # /compact-model、配置存储、prompt、测试
|
|
299
363
|
├── favorites/ # 模型收藏:选择器与循环 patch、
|
|
300
364
|
│ # 持久化存储、测试
|
|
365
|
+
├── permission/ # 会话级边界权限:模式、面板、bash 拆解、
|
|
366
|
+
│ # 策略、会话授权、确认对话框、测试
|
|
301
367
|
├── review/ # 第三方评审:输入/指令触发、
|
|
302
368
|
│ # 评审模型配置存储、提示词、测试
|
|
303
369
|
└── stash/ # 提示词暂存:热键状态机 + 测试
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), model favorites, prompt stash, third-party code review, and a dedicated compaction model",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, and a dedicated compaction model",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
"compaction",
|
|
26
26
|
"compact-model",
|
|
27
27
|
"toolkit",
|
|
28
|
-
"autostart"
|
|
28
|
+
"autostart",
|
|
29
|
+
"permission",
|
|
30
|
+
"approval",
|
|
31
|
+
"boundary"
|
|
29
32
|
],
|
|
30
33
|
"peerDependencies": {
|
|
31
34
|
"@earendil-works/pi-coding-agent": ">=1.0.0 <1.12.0",
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
38
|
"@dreki-gg/pi-command-sandbox": "^0.4.3",
|
|
39
|
+
"shell-quote": "^1.12.0",
|
|
36
40
|
"smol-toml": "^1.9.0"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask mode over the gate bus.
|
|
3
|
+
*
|
|
4
|
+
* The feature's command/state behaviour predates the bus; what this covers is
|
|
5
|
+
* the migration itself: while ask mode is on, its gate blocks the tools it
|
|
6
|
+
* confiscates and non-read-only bash, and it stops speaking once the mode is
|
|
7
|
+
* off.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import type {
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
ExtensionContext,
|
|
13
|
+
ToolCallEvent,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { GateHub, ToolGate } from "../../lib/gates.js";
|
|
16
|
+
import { registerAsk } from "./index.js";
|
|
17
|
+
|
|
18
|
+
function event(toolName: string, input: Record<string, unknown> = {}): ToolCallEvent {
|
|
19
|
+
return {
|
|
20
|
+
type: "tool_call",
|
|
21
|
+
toolCallId: "call-1",
|
|
22
|
+
toolName,
|
|
23
|
+
input,
|
|
24
|
+
} as unknown as ToolCallEvent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeHarness() {
|
|
28
|
+
const captured: ToolGate[] = [];
|
|
29
|
+
const hub = {
|
|
30
|
+
register(gate: ToolGate) {
|
|
31
|
+
captured.push(gate);
|
|
32
|
+
},
|
|
33
|
+
handle: async () => undefined,
|
|
34
|
+
install: () => {},
|
|
35
|
+
} as unknown as GateHub;
|
|
36
|
+
|
|
37
|
+
let activeTools = ["read", "write", "edit", "bash"];
|
|
38
|
+
let commandHandler: ((args: string, ctx: ExtensionContext) => unknown) | undefined;
|
|
39
|
+
const pi = {
|
|
40
|
+
on: () => {},
|
|
41
|
+
registerCommand(
|
|
42
|
+
name: string,
|
|
43
|
+
options: { handler: (args: string, ctx: ExtensionContext) => unknown },
|
|
44
|
+
) {
|
|
45
|
+
if (name === "ask") commandHandler = options.handler;
|
|
46
|
+
},
|
|
47
|
+
getActiveTools: () => [...activeTools],
|
|
48
|
+
getAllTools: () => [
|
|
49
|
+
{ name: "grep" },
|
|
50
|
+
{ name: "find" },
|
|
51
|
+
{ name: "ls" },
|
|
52
|
+
],
|
|
53
|
+
setActiveTools: (names: string[]) => {
|
|
54
|
+
activeTools = [...names];
|
|
55
|
+
},
|
|
56
|
+
} as unknown as ExtensionAPI;
|
|
57
|
+
|
|
58
|
+
const statuses: Array<[string, string | undefined]> = [];
|
|
59
|
+
const notes: string[] = [];
|
|
60
|
+
const ctx = {
|
|
61
|
+
hasUI: true,
|
|
62
|
+
mode: "tui",
|
|
63
|
+
ui: {
|
|
64
|
+
notify: (message: string) => notes.push(message),
|
|
65
|
+
setStatus: (key: string, value: string | undefined) => statuses.push([key, value]),
|
|
66
|
+
theme: { fg: (_color: string, text: string) => text },
|
|
67
|
+
},
|
|
68
|
+
} as unknown as ExtensionContext;
|
|
69
|
+
|
|
70
|
+
registerAsk(pi, hub);
|
|
71
|
+
const gate = captured[0];
|
|
72
|
+
if (!gate || !commandHandler) throw new Error("ask did not register");
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
gate,
|
|
76
|
+
ctx,
|
|
77
|
+
activeTools: () => activeTools,
|
|
78
|
+
toggle: async () => {
|
|
79
|
+
await commandHandler?.("", ctx);
|
|
80
|
+
},
|
|
81
|
+
evaluate: async (toolName: string, input: Record<string, unknown> = {}) =>
|
|
82
|
+
gate.evaluate(event(toolName, input), ctx),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe("ask gate", () => {
|
|
87
|
+
it("has no opinion while ask mode is off", async () => {
|
|
88
|
+
const h = makeHarness();
|
|
89
|
+
expect(await h.evaluate("write", { path: "x" })).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("blocks confiscated tools and non-read-only bash while on", async () => {
|
|
93
|
+
const h = makeHarness();
|
|
94
|
+
await h.toggle();
|
|
95
|
+
expect(await h.evaluate("write", { path: "x" })).toMatchObject({
|
|
96
|
+
kind: "block",
|
|
97
|
+
});
|
|
98
|
+
expect(await h.evaluate("bash", { command: "rm -rf x" })).toMatchObject({
|
|
99
|
+
kind: "block",
|
|
100
|
+
});
|
|
101
|
+
expect(await h.evaluate("bash", { command: "ls -la" })).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("stops blocking after exiting", async () => {
|
|
105
|
+
const h = makeHarness();
|
|
106
|
+
await h.toggle();
|
|
107
|
+
await h.toggle();
|
|
108
|
+
expect(await h.evaluate("write", { path: "x" })).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("restores the tool snapshot on exit", async () => {
|
|
112
|
+
const h = makeHarness();
|
|
113
|
+
await h.toggle();
|
|
114
|
+
expect(h.activeTools()).not.toContain("write");
|
|
115
|
+
await h.toggle();
|
|
116
|
+
expect(h.activeTools()).toEqual(["read", "write", "edit", "bash"]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* - Read-only helper tools (grep/find/ls) are activated for the duration so
|
|
9
9
|
* the model gets proper search tools; the entry snapshot restores exactly.
|
|
10
10
|
* - bash stays available but is gated by a read-only sandbox
|
|
11
|
-
* (see bash-sandbox.ts)
|
|
11
|
+
* (see bash-sandbox.ts) through the shared gate bus (lib/gates.ts), so
|
|
12
|
+
* another feature's gate (`permission`) runs alongside it rather than
|
|
13
|
+
* replacing it: each feature owns its own concern and the call is blocked
|
|
14
|
+
* when either objects.
|
|
12
15
|
* - Mode banner appended to the system prompt via before_agent_start
|
|
13
16
|
* chained systemPrompt (see prompt.ts), listing blocked -> alternative.
|
|
14
17
|
*/
|
|
@@ -16,6 +19,7 @@ import type {
|
|
|
16
19
|
ExtensionAPI,
|
|
17
20
|
ExtensionContext,
|
|
18
21
|
} from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import type { GateHub } from "../../lib/gates.js";
|
|
19
23
|
import {
|
|
20
24
|
restoreActiveTools,
|
|
21
25
|
snapshotActiveTools,
|
|
@@ -31,7 +35,7 @@ import { isReadOnlyCommand } from "./bash-sandbox.js";
|
|
|
31
35
|
import { appendAskBanner } from "./prompt.js";
|
|
32
36
|
import { blockedBashReason, blockedToolReason } from "./reasons.js";
|
|
33
37
|
|
|
34
|
-
export function registerAsk(pi: ExtensionAPI): void {
|
|
38
|
+
export function registerAsk(pi: ExtensionAPI, gates: GateHub): void {
|
|
35
39
|
let enabled = false;
|
|
36
40
|
/** Active tools before ask mode was entered (for exact restore on exit). */
|
|
37
41
|
let snapshot: string[] = [];
|
|
@@ -102,20 +106,34 @@ export function registerAsk(pi: ExtensionAPI): void {
|
|
|
102
106
|
// - bash is kept available but only read-only commands pass the sandbox.
|
|
103
107
|
// The rejection `reason` reaches the model as the tool result, so it names
|
|
104
108
|
// the nearest allowed alternative (see reasons.ts).
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
//
|
|
110
|
+
// Registered through the gate bus so the permission gate composes with it:
|
|
111
|
+
// ask answers "does this mutate?", permission answers "does this cross the
|
|
112
|
+
// project boundary?". A denied outside read still applies in ask mode, and
|
|
113
|
+
// a blocked write never reaches permission's dialog because a block
|
|
114
|
+
// short-circuits the bus.
|
|
115
|
+
gates.register({
|
|
116
|
+
name: "ask",
|
|
117
|
+
order: 10,
|
|
118
|
+
evaluate(event) {
|
|
119
|
+
if (!enabled) return undefined;
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
if (ASK_BLOCKED_TOOLS.has(event.toolName)) {
|
|
122
|
+
return {
|
|
123
|
+
kind: "block",
|
|
124
|
+
reason: blockedToolReason(event.toolName),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
111
127
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
if (event.toolName === "bash") {
|
|
129
|
+
const command =
|
|
130
|
+
typeof event.input.command === "string" ? event.input.command : "";
|
|
131
|
+
if (!isReadOnlyCommand(command)) {
|
|
132
|
+
return { kind: "block", reason: blockedBashReason(command) };
|
|
133
|
+
}
|
|
117
134
|
}
|
|
118
|
-
|
|
135
|
+
return undefined;
|
|
136
|
+
},
|
|
119
137
|
});
|
|
120
138
|
|
|
121
139
|
// Mode banner: chained append to the current system prompt while enabled.
|