pi-sleep-guard2.0.0
A Pi extension that blocks statically known long Bash sleeps and guides event-driven CI waits.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
sleep-guard
Pi 扩展:在 Bash 和 bg_run 命令执行前检查能静态确定时长的 sleep 调用。单次等待超过 120 秒时,阻止整次调用,并把简短、可执行的纠正原则作为工具错误返回给模型。
它不自动改写命令,不执行替代命令,也不轮询后台任务。对 GitHub Actions,提示使用 gh run watch <run-id> --exit-status:构建结束就立即返回,退出码区分成功/失败;需要完成时自动弹出,就用支持完成通知的后台任务运行这条 watch 命令。
拦截规则
- 阈值:严格大于 120 秒才拦截;
120、120.0、2m放行。 - 识别:普通
sleep、/bin/sleep、/usr/bin/sleep,以及command、exec、sudo、env、nohup、nice、time、timeout的静态包装;支持s、m、h、d单位、静态小数和静态多参数求和。infinity/inf视为超过上限。 - 递归分析:
bash/sh/zsh/dash/ksh -c '...'的静态字符串、eval '...'和ssh host '...'的最后一个静态命令字符串都会再按 shell 命令分析,包括被sudo、env、nohup、nice、time、timeout包装的情况;递归最多 4 层。 timeout:如果静态超时不超过 120 秒,调用最多等这么久,因此不拦;超过 120 秒时继续检查其子命令。- 所有位置:分号、管道、条件、循环、case、子 shell 和命令替换里的静态长 sleep 都会被检查;不执行控制流求值,所以即使分支运行时不会走到,静态长 sleep 也会被拦。
- 后台也拦截:
sleep 121 &、(sleep 121; ...) &不会被自动豁免;bg_run的command参数同样检查,避免用后台固定 sleep 绕过守卫。超时看门狗如果包含静态长 sleep,也可能被拦截;这是有意的严格规则。 - 不误伤文本:普通引号字符串、注释、带引号 heredoc 正文中的
sleep 121不是命令,不拦截;无引号 heredoc 中实际执行的$(sleep 121)会拦截。 - 不分析函数定义体;
f() { sleep 121; }本身不拦,调用f也看不到这个 sleep。 - 不确定时不拦截:
sleep $DELAY、$((...))、sleep 1h2m、sleep --help、解析失败或超长输入都返回unknown,不会仅凭猜测阻止 Bash。 - 解析器不可用时不会阻断 Bash;交互模式会提示一次,无 UI 模式写入 stderr,避免静默声称守卫生效。
这些都是静态分析边界,不是安全沙箱。脚本文件内的 sleep、docker exec、kubectl exec 等二次命令、python -c 'time.sleep(...)' / Node/其他语言内部的 sleep、make/npm 脚本内部的 sleep、变量或算术表达式、函数调用、多个短 sleep 组成的循环,以及 watch、tail -f、journalctl -f 等长期阻塞命令,不在本版保证范围内。
场景与推荐等待方式
| 场景 | 推荐方式 | 守卫覆盖 |
|---|---|---|
| Python、编译、测试、本地命令 | 前台直接运行,Bash 等进程退出并返回输出/退出码 | 不拦命令;拦命令里的静态长 sleep |
| 本地任务不阻塞 agent | bg_run 运行真实命令,完成时通知 |
拦 bg_run 里的固定 sleep |
| GitHub Actions | gh run watch <id> --exit-status;需要通知时后台运行它 |
提示具体命令 |
| 其他 CI(GitLab、Jenkins、Buildkite 等) | 使用原生 watch/API;没有就用有截止时间的短轮询 | 通用提示,不自动识别服务 |
| Docker / Kubernetes | docker wait、docker logs -f、kubectl wait、kubectl rollout status |
不递归识别这些包装 |
| SSH / 远程机器 | ssh host 'command' 前台等待,或远程命令完成通知 |
最后一个静态命令字符串会递归分析;交互式 ssh 不分析 |
| 端口、数据库就绪 | curl --retry、pg_isready 等,或短轮询 + 总超时 |
短 sleep 放行,长 sleep 拦截 |
| 长驻服务 | 后台启动服务,再做有截止时间的就绪检查 | 不等待服务进程退出 |
| 脚本/语言内部 sleep | 需要脚本自己支持完成信号或看门狗 | 静态分析看不到 |
模型收到的提示
被拦截时,工具调用不会执行,模型会收到:
[sleep-guard] 本次 Bash 调用未执行:检测到 `sleep 121`,单次固定等待 121 秒,超过 120 秒上限。
请优先使用任务完成通知;其次使用原生 watch/wait。
如果等待的是自己启动的 Python、编译、测试或其他命令:直接在前台运行,Bash 会在进程退出时返回真实输出和退出码;
需要同时做别的事,就用支持完成通知的后台任务运行同一条命令。后台任务不会让固定 sleep 提前返回。
必须轮询时,先检查状态再短暂等待,并设置总截止时间及单次检查超时。
成功立即结束;失败返回非零状态;超时和取消应明确报告并及时结束。
不要仅把长 sleep 拆成没有状态检查的多个短 sleep。
如果命令中包含 GitHub Actions 相关操作,提示会追加具体做法:
等待 GitHub Actions 时不要用固定 sleep 或 bg_run sleep。先取 run id,例如:
gh run list --limit 1 --json databaseId,status,conclusion,headBranch,workflowName
然后执行:
gh run watch <run-id> --exit-status
它会等到结束并立即返回,退出码区分成功/失败;需要完成时自动弹出,就用支持完成通知的后台任务运行这条 watch 命令。
本地 Python、编译、测试命令本身不需要 sleep:直接前台运行,Bash 会等待进程退出并返回输出与退出码。需要并行做别的事时,用支持完成通知的后台任务运行这条命令;不要用后台任务包住固定 sleep。
失败和超时没有统一的可复制模板:等待逻辑必须按实际任务返回正确退出码,单纯把长 sleep 拆成多个短 sleep 不算修复。
依赖与加载
- Pi
1.3.0扩展 API:tool_call、{ block: true, reason };同时处理内置bash和自定义bg_run事件。 web-tree-sitter@0.27.1、tree-sitter-bash@0.25.1(使用包内官方tree-sitter-bash.wasm,不在安装时编译本机扩展)。- 依赖已锁定在
package-lock.json。tree-sitter是可选 peer,未安装;本扩展走 WASM。
源码放在 ~/.pi/local-extensions/sleep-guard/,全局入口是一个固定包装文件:
~/.pi/agent/extensions/sleep-guard.ts
-> ../../local-extensions/sleep-guard/index.ts
Pi 启动时会自动发现并加载 ~/.pi/agent/extensions/ 下的扩展,因此不需要再传 --extension。已用 Pi 的 discoverAndLoadExtensions() 验证:全局目录能发现 sleep-guard.ts,并且它注册了 tool_call 处理器。
如果之前用 pi --extension 启动过本项目,请直接重启 Pi,不要同时保留命令行入口和全局入口,否则会注册两份守卫。正常情况下不要再传 --extension。
修改源码后,在 Pi 中执行 /reload 或重启即可;回滚只需删除全局入口文件,不会删除本地源码和测试:
rm ~/.pi/agent/extensions/sleep-guard.ts
如果移动了本地源码目录,需要同步更新这个包装文件的相对导入路径。
测试
cd ~/.pi/local-extensions/sleep-guard
npm test
测试包含:
tests/analyze.test.mjs:纯静态分析用例,覆盖阈值、精确小数、包装命令、引号、命令替换、heredoc、后台、函数、动态参数、畸形输入和大输入。失败会让进程以非零状态退出。tests/extension.test.mjs:通过 Pi 自己的createExtensionRuntime()/loadExtensions()加载index.ts,测试 handler 返回值和通知;不调用模型、不执行测试字符串中的命令、不启动外部任务。
若 Pi 安装路径不同:
PI_PACKAGE=/path/to/@earendil-works/pi-coding-agent npm run test:pi
解析器验证记录(2026-10-17):tree-sitter-bash WASM ABI 15 可由 web-tree-sitter@0.27.1 加载;静态分析测试 15/15、Pi 加载器集成测试 8/8 通过。尚未进行真实模型会话 smoke test。
发布到 npm 和 pi.dev/packages
Pi 包画廊会收录 npm 上带 "keywords": ["pi-package"] 的包;pi 清单声明扩展入口。当前包名 pi-sleep-guard 在 npm 上可用(查询于 2026-10-17),包内已包含 Bash grammar WASM,不依赖本机编译。
发布前:
cd ~/.pi/local-extensions/sleep-guard
npm test
npm pack --dry-run
npm login
确认 tarball 后发布:
npm publish
发布后用户可安装:
pi install npm:pi-sleep-guard@2.0.0
注意:
tree-sitter-bash仅作为开发依赖用于更新assets/tree-sitter-bash.wasm;运行时只需要web-tree-sitter和已打包的 WASM。其 MIT 许可证在assets/tree-sitter-bash.LICENSE。@earendil-works/pi-coding-agent是 Pi 提供的宿主包,已放在peerDependencies,不打包。- 发布新版本前修改
version,并重新运行测试和npm pack --dry-run。 - 本仓库不会自动执行
npm publish;需要 npm 账号、登录和发布确认。