@andares/pi-toolkits 0.7.1 → 0.7.3
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 +12 -11
- package/README.zh-CN.md +12 -11
- package/package.json +5 -5
- package/src/features/ask/bash-sandbox.test.ts +133 -2
- package/src/features/ask/bash-sandbox.ts +221 -17
- package/src/features/ask/constants.ts +16 -1
- package/src/features/ask/index.ts +39 -19
- package/src/features/ask/prompt.test.ts +45 -0
- package/src/features/ask/prompt.ts +51 -12
- package/src/features/ask/reasons.test.ts +35 -0
- package/src/features/ask/reasons.ts +68 -0
- package/src/features/autostart/index.ts +2 -1
- package/src/features/compact/index.ts +8 -4
- package/src/features/compact/prompt.ts +12 -4
- package/src/features/favorites/constants.ts +5 -4
- package/src/features/favorites/patch-cycle.ts +8 -7
- package/src/features/favorites/patch-selector.ts +4 -4
- package/src/features/stash/stash.ts +1 -1
- package/src/lib/model-config.ts +2 -2
- package/src/lib/model-picker.ts +2 -2
- package/src/lib/tools.ts +5 -3
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@andares/pi-toolkits)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
[](https://pnpm.io)
|
|
12
|
-
[](https://github.com/earendil-works/pi)
|
|
13
13
|
[](https://github.com/andares/pi-toolkits)
|
|
14
14
|
|
|
15
15
|
In a nutshell: six ready-to-use capabilities — **read-only consult mode** `ask`, **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 `>=0.
|
|
17
|
+
**Requirements**: pi `>=0.99.0 <2.0.0` (covering the four most recently released minor lines — 0.99.x / 1.0.x / 1.1.x / 1.2.x; 0.88–0.98 were never released). Every one of the 10 published versions inside that range (0.99.0 → 1.2.0) was compared against the host `dist`: the extension APIs we use carry no breaking differences (1.2.0 turns `before_agent_start` into a discriminated union and adds `reachesTools`; the fields we read are unchanged), and both runtime patch targets used by favorites — `ModelSelectorComponent` / `AgentSession.cycleModel` — are byte-identical, as are the `core/compaction/compaction.js` and `core/usage-totals.js` sources compact-model mirrors. `typecheck` plus the full test suite pass on each of 0.99.0, 0.99.1, 0.99.2, 1.0.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4, 1.1.0 and 1.2.0, and smoke-loading on real pi 1.2.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
|
|
|
@@ -36,7 +36,7 @@ pi -e ./src/index.ts
|
|
|
36
36
|
After installing, verify in any pi session:
|
|
37
37
|
|
|
38
38
|
- `/ask` → a grey `ask` status appears in the footer, `write`/`edit` are hard-disabled
|
|
39
|
-
- `/model` (or `ctrl+l`) → a favorites hint line appears at the top of the selector
|
|
39
|
+
- `/model` (or `ctrl+l`) → a favorites hint line appears at the top of the selector; `ctrl+A` on the selected model → it turns bold bright yellow and the favorites counter goes up (this step is also the real-machine keypress check — see Keybindings)
|
|
40
40
|
- press `ctrl+alt+y` in the input box → the current prompt is stashed and the input box is cleared
|
|
41
41
|
- after finishing a development task, type `/third-review` → switches to the review model and runs check + fix
|
|
42
42
|
- `/compact-model`, pick a cheap fast model → from then on both `/compact` and auto-compaction summarize with it (a grey `compact` status appears in the footer)
|
|
@@ -65,7 +65,7 @@ After installing, verify in any pi session:
|
|
|
65
65
|
|
|
66
66
|
| Feature | Entry point | One-line description |
|
|
67
67
|
| --- | --- | --- |
|
|
68
|
-
| 🛡️ **ask** read-only consult mode | `/ask` | enter read-only Q&A mode: `write`/`edit` hard-disabled, `bash` runs in a read-only sandbox |
|
|
68
|
+
| 🛡️ **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 |
|
|
69
69
|
| ⭐ **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
70
|
| 📥 **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
71
|
| 🔎 **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" |
|
|
@@ -82,12 +82,13 @@ Enter with `/ask` (press it again to exit). Meant for the case where you only wa
|
|
|
82
82
|
|
|
83
83
|
**While active**:
|
|
84
84
|
|
|
85
|
-
- **`write` / `edit` hard-disabled** — removed from the active tool set; pi rejects calls to inactive tools outright ("Tool not found"), so file modification becomes mechanically impossible
|
|
86
|
-
-
|
|
87
|
-
-
|
|
85
|
+
- **`write` / `edit` / `powershell` hard-disabled** — removed from the active tool set; pi rejects calls to inactive tools outright ("Tool not found"), so file modification becomes mechanically impossible
|
|
86
|
+
- **Read-only helper tools activated** — `grep` / `find` / `ls` (registered by pi but inactive by default) are switched on for the duration, so the model gets proper search tools instead of bash-only search; the entry snapshot is restored exactly on exit
|
|
87
|
+
- **`bash` kept, but sandboxed to read-only** — shell tokenization, per-segment validation plus write-intent detection. Allowed beyond the obvious readers: git archaeology (`blame`, `grep`, `rev-parse`, `describe`, `shortlog`, `reflog show`, `stash list`, `tag -l`, `cat-file`, ...), package metadata queries (`pnpm view`, `pip show/list`, `python -m json.tool`) and network reads through `curl -s <url>` / `wget -O - <url>`. Still blocked: redirections, `tee`, `dd`, `sed -i`, installs/builds, git mutations, command substitution, command runners (`find -exec`, `git grep -O`, `rg --pre`), and every write target (`curl -o`, `wget -O file`, `git diff --output=`, `tree -o`). Heuristic, not a security boundary
|
|
88
|
+
- **System prompt appendix names the alternatives** — the appended mode description states what is blocked *and what to use instead* (`read` for file contents, `curl -s` for URLs, `/ask` to exit). Rejected tool calls come back with a classified reason that points at the nearest allowed alternative, so the model does not retry blind variants
|
|
88
89
|
- **Grey `ask` footer status** — a permanent reminder that read-only mode is on
|
|
89
90
|
|
|
90
|
-
**Exit**: press `/ask` again; the tool-set snapshot taken on entry is fully restored.
|
|
91
|
+
**Exit**: press `/ask` again; the tool-set snapshot taken on entry is fully restored (read-only helpers included).
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
@@ -118,7 +119,7 @@ The `/favorites` command lists the current favorites and the cycling mode.
|
|
|
118
119
|
|
|
119
120
|
> **Implementation notes**: pi's `app.model.cycleForward` (`ctrl+p`) and `cycleBackward` are **reserved keybindings** that extension shortcuts cannot override; extension shortcuts also only fire while the editor is focused. This feature therefore rebinds nothing; instead it patches two built-in prototypes at runtime (version-guarded, idempotent under `/reload`): `ModelSelectorComponent` (hint line, `ctrl+A`/`ctrl+J`, bright-yellow rendering, favorites-only filter — the keys are consumed only inside the selector, so in the editor `ctrl+j` still inserts a newline) and `AgentSession.cycleModel` (favorites-only cycling). If a pi upgrade makes the patch inapplicable, it warns and degrades gracefully without affecting the other features.
|
|
120
121
|
>
|
|
121
|
-
> **Why `ctrl+A` and not `ctrl+F`**: since pi 1.0 the default `tuiMode` is `fullscreen` (0.
|
|
122
|
+
> **Why `ctrl+A` and not `ctrl+F`**: since pi 1.0 the default `tuiMode` is `fullscreen` (0.87–0.99 defaulted to `regular`), and on Windows/WSL the host resolves `tui.altScreen.search` to `ctrl+f`; `TuiAltScreen.handleViewportInput()` checks that binding **before** handing input to the focused overlay, so `ctrl+f` opened the transcript search instead of reaching the selector. An interim pick, `ctrl+alt+f`, was clean at the pi layer but turned out to be grabbed by a Windows screenshot tool — a key free in the host table is not necessarily free on the machine. The final pick takes a letter from "favorite" and audits each: `v` is Windows Terminal's paste, `o`/`r`/`t` are host app actions, `i` is byte-identical to Tab (the selector's scope toggle), `e` works but carries no mnemonic; `a` only shadows the search box's jump-to-line-start, reads as *add favorite*, and nothing upstream of the selector claims it. Full audit: `src/features/favorites/constants.ts` and `pnpm check:keybindings`.
|
|
122
123
|
|
|
123
124
|
---
|
|
124
125
|
|
|
@@ -195,7 +196,7 @@ pi's `/compact` and auto-compact summarize the context **with the current sessio
|
|
|
195
196
|
| auto-compact threshold reached | summarized by the compaction model |
|
|
196
197
|
| context overflow recovery | summarized by the compaction model, retry behavior unchanged |
|
|
197
198
|
|
|
198
|
-
**Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim —
|
|
199
|
+
**Parts that are identical to pi's built-in behavior after takeover** (the built-in implementation is mirrored verbatim — copied from 0.87.1, byte-identical across every published version of the supported window, 0.99.0 → 1.2.0 — and only the executing model changes; the conversation serializer itself is imported from the host, so it follows whatever the running host does, e.g. 1.2.0 stops feeding assistant thinking back into the summarization request):
|
|
199
200
|
|
|
200
201
|
- **Summary format** — the structured Goal / Progress / Key Decisions checkpoint; when a summary already exists, the iterative-update prompt is used so information carries over instead of being lost
|
|
201
202
|
- **Split behavior** — the `keepRecentTokens` retention policy, and `firstKeptEntryId` / `tokensBefore` passed back unchanged
|
|
@@ -262,7 +263,7 @@ No file = feature disabled, zero overhead, silently skipped.
|
|
|
262
263
|
|
|
263
264
|
**Key constraint: extension commands only**. Commands registered via `registerCommand` such as `/add-dir` (pi-add-dir) and `/cd` (pi-cd) work; built-in commands (`/compact`, `/reload`, `/resume`, …) are dispatched in the TUI layer and cannot be triggered from the extension layer; skill commands (`/skill:name`) are an expansion mechanism rather than an execution mechanism. The target extension you depend on (e.g. pi-add-dir) must be installed first.
|
|
264
265
|
|
|
265
|
-
> **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`;
|
|
266
|
+
> **Implementation notes**: execution uses `sendUserMessage(cmd, { expandPromptTemplates: true })` — that option routes `/cmd args` into command dispatch instead of the model prompt path (the wrapper defaults to `false`; `sendUserMessage` and `_tryExecuteExtensionCommand` are byte-identical across 0.99.0–1.2.0, see the host-compatibility discipline in AGENTS.md). Pre-validation uses `pi.getCommands()` (accepting only `source: "extension"`). Depends on `smol-toml` (zero-dependency, ~10KB, TOML 1.0).
|
|
266
267
|
|
|
267
268
|
---
|
|
268
269
|
|
package/README.zh-CN.md
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@andares/pi-toolkits)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
[](https://pnpm.io)
|
|
12
|
-
[](https://github.com/earendil-works/pi)
|
|
13
13
|
[](https://github.com/andares/pi-toolkits)
|
|
14
14
|
|
|
15
15
|
一句话介绍:目前包含六个开箱即用的功能——**只读咨询模式** `ask`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`、**会话启动自动化** `autostart`,后续持续追加。
|
|
16
16
|
|
|
17
|
-
**版本要求**:pi `>=0.
|
|
17
|
+
**版本要求**:pi `>=0.99.0 <2.0.0`(覆盖最近 4 个中版本线 0.99.x / 1.0.x / 1.1.x / 1.2.x —— 0.88–0.98 从未发布。区间内全部 10 个已发布版本(0.99.0 → 1.2.0)逐版本比对宿主 dist:用到的扩展 API 无破坏性差异(1.2.0 把 `before_agent_start` 改为判别联合类型、新增 `reachesTools`,我们读取的字段未变),favorites 两处运行时 patch 目标 `ModelSelectorComponent` / `AgentSession.cycleModel` 字节级一致,compact-model 镜像所依据的 `core/compaction/compaction.js` 与 `core/usage-totals.js` 同样字节级一致;并在 0.99.0、0.99.1、0.99.2、1.0.0、1.0.1、1.0.2、1.0.3、1.0.4、1.1.0、1.2.0 上逐一通过 typecheck + 全量测试,在真实 pi 1.2.0 上冒烟加载无警告)。注:pi ≥ 0.99 的宿主要求 `@earendil-works/pi-tui` 声明为 peerDependencies `"*"`(由宿主提供),本包已按要求声明。
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -36,7 +36,7 @@ pi -e ./src/index.ts
|
|
|
36
36
|
安装后在任意 pi 会话中验证:
|
|
37
37
|
|
|
38
38
|
- `/ask` → footer 出现灰色 `ask` 状态,`write`/`edit` 被硬禁用
|
|
39
|
-
- `/model`(或 `ctrl+l`)→
|
|
39
|
+
- `/model`(或 `ctrl+l`)→ 选择器顶部出现收藏提示行;对选中模型按 `ctrl+A` → 文字变**加粗亮黄**、收藏计数 +1(这一步同时就是真机按键检查,见「键位」约定)
|
|
40
40
|
- 输入框内按 `ctrl+alt+y` → 当前提示词被暂存并清空输入框
|
|
41
41
|
- 完成一个开发任务后输入 `/third-review` → 切换到评审模型执行查+修
|
|
42
42
|
- `/compact-model` 选一个便宜快模型 → 之后 `/compact` 与自动压缩的总结都由它执行(footer 出现灰色 `compact` 状态)
|
|
@@ -65,7 +65,7 @@ pi -e ./src/index.ts
|
|
|
65
65
|
|
|
66
66
|
| 功能 | 入口 | 一句话说明 |
|
|
67
67
|
| --- | --- | --- |
|
|
68
|
-
| 🛡️ **ask** 只读咨询模式 | `/ask` | 进入只读问答模式,`write`/`edit` 硬禁用,`bash`
|
|
68
|
+
| 🛡️ **ask** 只读咨询模式 | `/ask` | 进入只读问答模式,`write`/`edit`/`powershell` 硬禁用,`bash` 只读沙箱,临时启用 `grep`/`find`/`ls` |
|
|
69
69
|
| ⭐ **favorites** 模型收藏 | `/model` 内 `ctrl+A` / `ctrl+J` | 收藏常用模型(加粗亮黄标记),`ctrl+p` 仅在收藏间循环 |
|
|
70
70
|
| 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
|
|
71
71
|
| 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
|
|
@@ -82,12 +82,13 @@ pi -e ./src/index.ts
|
|
|
82
82
|
|
|
83
83
|
**进入后**:
|
|
84
84
|
|
|
85
|
-
- **`write` / `edit` 硬禁用** —— 从活跃工具集中移除,pi 运行时对未激活工具的调用直接拒绝("Tool not found"),文件修改在机制上不可能发生
|
|
86
|
-
-
|
|
87
|
-
-
|
|
85
|
+
- **`write` / `edit` / `powershell` 硬禁用** —— 从活跃工具集中移除,pi 运行时对未激活工具的调用直接拒绝("Tool not found"),文件修改在机制上不可能发生
|
|
86
|
+
- **临时启用只读检索工具** —— `grep` / `find` / `ls`(宿主已注册但默认不激活)在 ask 期间打开,让模型有正规搜索工具而不是只能走 bash;退出时按进入前快照完整恢复
|
|
87
|
+
- **`bash` 保留但只读沙箱** —— shell 分词 + 逐段校验 + 写盘意图检测。除常见读取命令外,放行 git 考古族(`blame`、`grep`、`rev-parse`、`describe`、`shortlog`、`reflog show`、`stash list`、`tag -l`、`cat-file` 等)、包元数据查询(`pnpm view`、`pip show/list`、`python -m json.tool`),以及网络读取 `curl -s <url>` / `wget -O - <url>`。仍拦截:重定向、`tee`、`dd`、`sed -i`、安装/构建、git 变更类命令、命令替换、会启动其它程序的选项(`find -exec`、`git grep -O`、`rg --pre`),以及一切写盘目标(`curl -o`、`wget -O file`、`git diff --output=`、`tree -o`)。启发式,非安全边界
|
|
88
|
+
- **系统提示词给出替代方案** —— 追加的模式说明不仅列限制,还写明被拦后用什么替代(读文件用 `read`、取网页用 `curl -s`、要改动则 `/ask` 退出);被拒的工具调用返回分类原因,直接指向最近的可用替代,模型不会盲目重试
|
|
88
89
|
- **灰色 `ask` footer 状态** —— 常驻提示当前处于只读模式
|
|
89
90
|
|
|
90
|
-
**退出**:再按 `/ask
|
|
91
|
+
**退出**:再按 `/ask`,完整恢复进入前的工具集快照(含临时启用的只读工具)。
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
@@ -118,7 +119,7 @@ pi -e ./src/index.ts
|
|
|
118
119
|
|
|
119
120
|
> **实现说明**:pi 的 `app.model.cycleForward`(`ctrl+p`)与 `cycleBackward` 是**保留键位**,扩展快捷键无法覆盖;且扩展快捷键只在编辑器聚焦时生效。因此本功能不重绑任何按键,而是在运行时 patch 两个内置原型(带版本守卫、`/reload` 幂等):`ModelSelectorComponent`(提示行、`ctrl+A`/`ctrl+J`、亮黄渲染、仅收藏过滤——按键仅在选择器内消费,编辑器里 `ctrl+j` 换行不受影响)与 `AgentSession.cycleModel`(收藏循环)。若 pi 升级导致 patch 无法应用,会 warn 并优雅降级,不影响其它功能。
|
|
120
121
|
>
|
|
121
|
-
> **为何是 `ctrl+A` 而不是 `ctrl+F`**:pi 自 1.0 起 `tuiMode` 默认 `fullscreen`(0.
|
|
122
|
+
> **为何是 `ctrl+A` 而不是 `ctrl+F`**:pi 自 1.0 起 `tuiMode` 默认 `fullscreen`(0.87–0.99 默认 `regular`),而在 Windows/WSL 上宿主把 `tui.altScreen.search` 解析为 `ctrl+f`;`TuiAltScreen.handleViewportInput()` 在**把输入交给聚焦覆盖层之前**先检查该绑定,于是 `ctrl+f` 打开了会话记录搜索、根本到不了选择器。曾过渡性地选过 `ctrl+alt+f`——pi 层完全空闲,却被 Windows 侧截图工具作为全局热键截走:**宿主表空闲 ≠ 机器层空闲**。最终从 "favorite" 一词里逐字母审计:`v` 是 Windows Terminal 的粘贴键,`o`/`r`/`t` 是宿主 app 层动作,`i` 与 Tab 字节恒等(选择器的 scope 切换),`e` 可用但无记忆点;`a` 仅遮蔽搜索框内的行首跳转、读作 *add favorite*,且选择器上游无任何消费者。完整依据见 `src/features/favorites/constants.ts` 与 `pnpm check:keybindings`。
|
|
122
123
|
|
|
123
124
|
---
|
|
124
125
|
|
|
@@ -195,7 +196,7 @@ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文
|
|
|
195
196
|
| auto-compact 阈值触发 | 压缩模型总结 |
|
|
196
197
|
| context overflow 恢复 | 压缩模型总结,重试行为不变 |
|
|
197
198
|
|
|
198
|
-
**接管后与 pi 内建行为完全一致的部分**(逐字同步 pi
|
|
199
|
+
**接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,复制自 0.87.1、在支持区间内全部已发布版本(0.99.0 → 1.2.0)字节级不变,只换执行模型;对话序列化函数(`serializeConversation`)是直接从宿主导入的,因此跟随宿主行为 —— 例如 1.2.0 起不再把 assistant thinking 回填进摘要请求):
|
|
199
200
|
|
|
200
201
|
- **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
|
|
201
202
|
- **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
|
|
@@ -262,7 +263,7 @@ run = [
|
|
|
262
263
|
|
|
263
264
|
**关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
|
|
264
265
|
|
|
265
|
-
> **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false
|
|
266
|
+
> **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,`sendUserMessage` 与 `_tryExecuteExtensionCommand` 在 0.99.0–1.2.0 全区间字节级一致,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
|
|
266
267
|
|
|
267
268
|
---
|
|
268
269
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
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",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"autostart"
|
|
29
29
|
],
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
31
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0 <2.0.0",
|
|
32
32
|
"@earendil-works/pi-tui": "*"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"smol-toml": "^1.9.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@earendil-works/pi-ai": "^1.0
|
|
40
|
-
"@earendil-works/pi-coding-agent": "^1.0
|
|
41
|
-
"@earendil-works/pi-tui": "^1.0
|
|
39
|
+
"@earendil-works/pi-ai": "^1.2.0",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "^1.2.0",
|
|
41
|
+
"@earendil-works/pi-tui": "^1.2.0",
|
|
42
42
|
"@types/node": "^26.6.2",
|
|
43
43
|
"typescript": "^5.6.0",
|
|
44
44
|
"vitest": "^4.1.10"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Boundary tests for the ask-mode read-only bash sandbox.
|
|
3
3
|
*/
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { isReadOnlyCommand } from "./bash-sandbox.js";
|
|
5
|
+
import { classifyBlockedCommand, isReadOnlyCommand } from "./bash-sandbox.js";
|
|
6
6
|
|
|
7
7
|
describe("isReadOnlyCommand — allowed (read-only)", () => {
|
|
8
8
|
const allowed = [
|
|
@@ -14,6 +14,10 @@ describe("isReadOnlyCommand — allowed (read-only)", () => {
|
|
|
14
14
|
"git status",
|
|
15
15
|
"git log --oneline -5",
|
|
16
16
|
"git diff --stat",
|
|
17
|
+
"git show HEAD",
|
|
18
|
+
"git branch -a",
|
|
19
|
+
"git remote -v",
|
|
20
|
+
"git show HEAD 2>/dev/null",
|
|
17
21
|
"grep -r foo src",
|
|
18
22
|
"find . -name '*.ts'",
|
|
19
23
|
"cd src && ls",
|
|
@@ -21,7 +25,12 @@ describe("isReadOnlyCommand — allowed (read-only)", () => {
|
|
|
21
25
|
"npm list --depth=0",
|
|
22
26
|
"curl -s https://example.com",
|
|
23
27
|
"curl -o - https://example.com", // stdout, allowed
|
|
28
|
+
"curl --output - https://example.com", // stdout, allowed
|
|
29
|
+
"curl --output=- https://example.com", // stdout, allowed
|
|
24
30
|
"wget -O - https://example.com", // stdout, allowed
|
|
31
|
+
"wget -qO- https://example.com", // stdout, allowed
|
|
32
|
+
"wget -O - --quiet https://example.com", // stdout, allowed
|
|
33
|
+
"wget --output-document=- https://example.com", // stdout, allowed
|
|
25
34
|
"echo 'hello && world'", // quoted operator is data, not control
|
|
26
35
|
];
|
|
27
36
|
|
|
@@ -30,6 +39,61 @@ describe("isReadOnlyCommand — allowed (read-only)", () => {
|
|
|
30
39
|
});
|
|
31
40
|
});
|
|
32
41
|
|
|
42
|
+
describe("isReadOnlyCommand — allowed (relaxed read-only families)", () => {
|
|
43
|
+
const allowed = [
|
|
44
|
+
// git archaeology
|
|
45
|
+
"git blame -L 10,20 src/index.ts",
|
|
46
|
+
"git grep -n registerAsk -- src",
|
|
47
|
+
"git rev-parse HEAD",
|
|
48
|
+
"git rev-list --count HEAD",
|
|
49
|
+
"git describe --tags --abbrev=0",
|
|
50
|
+
"git shortlog -sn",
|
|
51
|
+
"git reflog show",
|
|
52
|
+
"git reflog",
|
|
53
|
+
"git stash list",
|
|
54
|
+
"git stash show -p",
|
|
55
|
+
"git worktree list",
|
|
56
|
+
"git tag -l",
|
|
57
|
+
"git tag --list 'v*'",
|
|
58
|
+
"git cat-file -p HEAD",
|
|
59
|
+
"git merge-base HEAD~5 HEAD",
|
|
60
|
+
"git check-ignore -v node_modules",
|
|
61
|
+
"git config --list",
|
|
62
|
+
// destructive-substring false positives in arguments
|
|
63
|
+
"git log --grep=rm --oneline",
|
|
64
|
+
"git blame src/rm-utils.ts",
|
|
65
|
+
"git log --grep=rm --oneline && ls src",
|
|
66
|
+
// search arguments that merely *mention* a guarded flag are data
|
|
67
|
+
"grep -rn 'find -exec' src",
|
|
68
|
+
"git grep -n 'find -exec' -- .",
|
|
69
|
+
"git log --grep='curl -so x' --oneline",
|
|
70
|
+
"rg 'sed -i' .",
|
|
71
|
+
// package metadata
|
|
72
|
+
"pnpm view vite version",
|
|
73
|
+
"pnpm info react",
|
|
74
|
+
"pnpm search eslint",
|
|
75
|
+
"pip show requests",
|
|
76
|
+
"pip list",
|
|
77
|
+
"pip3 freeze",
|
|
78
|
+
"uv pip list",
|
|
79
|
+
"python3 -m json.tool package.json",
|
|
80
|
+
// guards must not over-block plain readers
|
|
81
|
+
"fd -e ts",
|
|
82
|
+
"sort package.json",
|
|
83
|
+
"curl -so- https://example.com", // clustered short form, stdout
|
|
84
|
+
"git grep --textconv -n foo", // read path, not the -O pager
|
|
85
|
+
"env", // bare env prints the environment
|
|
86
|
+
"env -i",
|
|
87
|
+
"env FOO=bar", // assignment-only form, no command
|
|
88
|
+
"file README.md",
|
|
89
|
+
"sed -n '1p' README.md",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
it.each(allowed)("allows: %s", (command) => {
|
|
93
|
+
expect(isReadOnlyCommand(command)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
33
97
|
describe("isReadOnlyCommand — blocked (mutating/destructive)", () => {
|
|
34
98
|
const blocked = [
|
|
35
99
|
"rm -rf /",
|
|
@@ -40,19 +104,67 @@ describe("isReadOnlyCommand — blocked (mutating/destructive)", () => {
|
|
|
40
104
|
"touch foo",
|
|
41
105
|
"npm install",
|
|
42
106
|
"npm run build",
|
|
107
|
+
"pnpm install",
|
|
108
|
+
"pnpm add left-pad",
|
|
109
|
+
"pip install requests",
|
|
43
110
|
"git commit -m x",
|
|
44
111
|
"git push",
|
|
45
112
|
"git checkout main",
|
|
113
|
+
"git reset --hard",
|
|
114
|
+
"git stash drop",
|
|
115
|
+
"git stash pop",
|
|
116
|
+
"git worktree remove x",
|
|
117
|
+
"git reflog expire --all",
|
|
118
|
+
"git tag v1",
|
|
119
|
+
"git tag -d v1",
|
|
120
|
+
"git branch new-branch",
|
|
121
|
+
"git branch -m old new",
|
|
122
|
+
"git branch -D old",
|
|
123
|
+
"git remote add origin git@example.com:x/y.git",
|
|
124
|
+
"git remote remove origin",
|
|
46
125
|
"cat x > out.txt",
|
|
47
126
|
"echo hi >> log",
|
|
48
127
|
"echo $(rm -rf /)", // command substitution
|
|
128
|
+
"git log --grep=$(rm -rf /)", // substitution hidden in an allowed verb
|
|
49
129
|
"ls | rm -rf /", // pipeline with destructive command
|
|
130
|
+
"git log --grep=rm --oneline && rm -rf /", // second segment is a write
|
|
131
|
+
"find . -name x -exec rm {} \\;", // find stays out of the read-only bypass
|
|
132
|
+
"find . -delete", // -delete is a write
|
|
133
|
+
"ls | find . -delete", // ... and the pre-check sees pipelines too
|
|
134
|
+
"find . -name '*.ts' -exec sh -c 'echo x > /tmp/y' \\;", // -exec runs commands
|
|
135
|
+
"ls | find . -name x -exec echo {} \\;", // exec intent outside the first segment
|
|
136
|
+
"fd . -x rm", // fd -x runs commands: never on the bypass list
|
|
137
|
+
"fd . -x touch y",
|
|
138
|
+
"fd --exec rm {}",
|
|
139
|
+
"sort -o out.txt file", // sort's output flag is a write
|
|
140
|
+
"sort file --output=out.txt",
|
|
141
|
+
"curl -so /tmp/x https://example.com", // clustered short form writes
|
|
142
|
+
"curl -o/tmp/x https://example.com", // attached short form writes
|
|
143
|
+
"curl -sLo /tmp/x https://example.com",
|
|
144
|
+
"curl --remote-name https://example.com",
|
|
145
|
+
"sed -n -i 's/a/b/' f", // -i sneaks past the `sed -n` allowlist
|
|
146
|
+
"tree -o /tmp/x",
|
|
147
|
+
"tree --output=/tmp/x",
|
|
148
|
+
"file -C -m magic",
|
|
149
|
+
"git grep -O 'rm -rf /' pattern", // -O runs a pager command
|
|
150
|
+
"git grep --open-files-in-pager=less pattern",
|
|
151
|
+
"git log --ext-diff -p", // external diff driver
|
|
152
|
+
"env curl -o out.txt https://example.com", // env forwards to a write
|
|
153
|
+
"env -i curl -o out.txt https://example.com",
|
|
154
|
+
"env FOO=1 curl -o out.txt https://example.com",
|
|
155
|
+
"ls && env curl -o out.txt https://example.com",
|
|
156
|
+
"env -S 'sh -c x'",
|
|
157
|
+
"env find . -delete",
|
|
158
|
+
"env sed -i 's/a/b/' f",
|
|
50
159
|
"curl -o file https://example.com",
|
|
51
160
|
"curl -O https://example.com/x",
|
|
52
161
|
"curl --output file https://example.com",
|
|
53
162
|
"wget -O file https://example.com",
|
|
54
163
|
"wget --output-document file https://example.com",
|
|
55
|
-
"wget
|
|
164
|
+
"wget https://example.com",
|
|
165
|
+
"git diff --output=out.txt", // real write path on a read-only verb
|
|
166
|
+
"git log --output out.txt",
|
|
167
|
+
"git show -o out.txt",
|
|
56
168
|
"dd if=/dev/zero of=/tmp/x bs=1 count=1",
|
|
57
169
|
"echo x | tee file",
|
|
58
170
|
"vim README.md",
|
|
@@ -64,3 +176,22 @@ describe("isReadOnlyCommand — blocked (mutating/destructive)", () => {
|
|
|
64
176
|
expect(isReadOnlyCommand(command)).toBe(false);
|
|
65
177
|
});
|
|
66
178
|
});
|
|
179
|
+
|
|
180
|
+
describe("classifyBlockedCommand", () => {
|
|
181
|
+
it.each([
|
|
182
|
+
["echo $(id)", "substitution"],
|
|
183
|
+
["cat a > b", "redirect"],
|
|
184
|
+
["git grep -O x", "exec"],
|
|
185
|
+
["env curl -o out.txt https://example.com", "exec"],
|
|
186
|
+
["find . -exec echo {} ;", "exec"],
|
|
187
|
+
["rm -rf /", "write"],
|
|
188
|
+
["find . -delete", "write"],
|
|
189
|
+
["curl -O https://example.com", "write"],
|
|
190
|
+
["wget https://example.com", "download"],
|
|
191
|
+
["git commit -m x", "git"],
|
|
192
|
+
["pnpm install", "install"],
|
|
193
|
+
["vim README.md", "unlisted"],
|
|
194
|
+
] as const)("classifies %s as %s", (command, kind) => {
|
|
195
|
+
expect(classifyBlockedCommand(command)).toBe(kind);
|
|
196
|
+
});
|
|
197
|
+
});
|