edgehero/pi-dispatch
GitHub: edgehero/pi-dispatch
pi-dispatch 为 pi 编码代理提供了缺失的运维调度层,包含持久化队列、容器沙箱、消费上限和实时管理面板,支持 CLI、cron 和 GitHub 事件三种触发方式。
Stars: 4 | Forks: 1
# pi-dispatch
**将 [pi](https://github.com/earendil-works/pi) 编码代理作为一种服务运行 —— 可按需触发,基于
cron 定时任务,或由 GitHub issue 或 pull request 触发 —— 运行在你可控的容器中,并配备持久化队列、
消费上限和实时管理面板。**


pi 没有作业队列、没有并发控制、没有消费限制,而且 —— 正如其 README 所述 —— 没有权限
系统。**pi-dispatch 恰好就是那个缺失的运维层,仅此而已。**
- **容器即边界。** 每个作业都在 `--cap-drop=ALL`、非 root 用户、临时性的环境下运行,且指令以
只读方式挂载 —— 这就是 pi 缺失的权限系统,由 Docker 强制执行。
- **在容器启动前消费即受到限制** —— 基于每个作业的 turn 预算和每日上限,在消耗任何
token 之前进行检查。
- **镜像由你塑造。** 将项目的工具链融入 [`image/Dockerfile`](image/Dockerfile);
它内置了 **Playwright + Chromium**,因此一个流程可以构建前端、对其进行截图,并
根据渲染结果进行迭代 —— 这便是其胜过固定托管例程或 `/loop` 的优势所在。
- **三种触发器,同一种作业。** 无论是 CLI 命令、cron 计划,还是 GitHub issue/PR —— 同样的作业、同样的沙箱、
同样的面板。cron 是无人值守的模式:在你自己的硬件上、在你控制的镜像中执行重复性工作。
- **由你的项目来驾驭它** —— 使用来自你已提交文件中的 pi 原生 `.pi/skills` 和 persona,并凌驾于
代理无法移除的、不可变的安全底线之上。
## 快速开始(本地文件夹)
你需要 **Docker** 和 **Node ≥ 22.19**,以及一个提供商的 API key(例如 Anthropic)。
```
# 1. 构建 job image(一次)
docker build -f image/Dockerfile -t pi-job:latest .
# 2. 启动 Valkey(持久化 job queue)
docker compose -f deploy/docker-compose.yml up -d
# 3. 配置
cp .env.example .env # then set ANTHROPIC_API_KEY (or your provider's key)
npm ci
# 4. 在一个 terminal 中运行 worker
npx pi-dispatch worker # (or: npm --workspace worker start)
# 5. 从另一个 terminal 加入 job 队列
npx pi-dispatch run ./my-project --task "add type hints to utils.py" --flow tidy
```
worker 会接收作业,将你的文件夹挂载到容器中,然后 pi 会**就地**对其进行编辑。除非
你传递了 `--force` 参数,否则它会拒绝在存在未提交内容的 git 工作树上运行,因为这是无法撤销的 ——
请将其指向你可以恢复的文件夹,并提前做好提交。
## 运行机制及其保护原理
本地路径的端到端流程 —— 每个触发器都会流经相同的队列、容器和预算:
```
flowchart LR
CLI["pi-dispatch run ./folder --task ..."] -->|enqueue| Q[("Valkey + BullMQ
the wait-list, AOF")]
Q --> B{"under the daily cap
and turn budget?"}
B -->|no| STOP["refused before any spend"]
B -->|yes| C["docker run --rm: one ephemeral container
--cap-drop=ALL, non-root, no-new-privileges
/job read-only, /workspace = your folder"]
C --> PI["pi + Playwright + git + gh
guardrails + your .pi/"]
PI -->|"edits in place"| F[("your folder")]
```
容器边界、在容器启动*之前*设定的消费限制、且没有任何降级操作 —— 这就是该路径
所强制执行的。在依赖它之前,请阅读 [`SECURITY.md`](SECURITY.md):它明确说明了
哪些受到保护,哪些没有。
## 作为服务运行
`pi-dispatch worker` 是一个长期运行的进程 —— 你可以在终端中运行它,或者将其交给操作系统的服务
管理器,以便它在开机时启动并在崩溃时重启。[`deploy/`](deploy/) 中的配置单元是**针对单主机的
模板,而非即插即用方案**:每个都包含你需要根据自己机器填写的 `
` 路径。systemd
单元的*结构*已通过 `systemd-analyze` 检查;launchd 和 nssm 单元则是完整的示例。这三者都在
**主机**上运行 worker —— 它驱动 `docker` CLI 且本身并未被容器化 —— 因此
它们需要与 [`deploy/docker-compose.yml`](deploy/docker-compose.yml) 中启用了 AOF 的 Valkey 一起
运行,这正是让队列**及暂停状态**在重启后依然存活的秘诀。
**无需停止即可控制正在运行的 worker** —— 这些命令与 Valkey 通信,因此无论
worker 是在终端中还是在服务管理器下运行,它们都能起作用:
- `pi-dispatch pause` —— 停止接收新作业。**持久化**:暂停状态保存在队列中,并在
worker 重启后依然有效,因此处于暂停状态的 worker 在重启后依然会保持暂停。作业仍可入队;它们只需等待。
- `pi-dispatch resume` —— 再次开始接收作业。
- `pi-dispatch status` —— 打印 `{ pausedState, waiting, active, paused, delayed, failed }`。`pausedState`
是开关;`paused` 是在暂停期间堆积的作业积压**计数**(它们会进入
`paused` 列表,而不是 `waiting`)。
### Linux (systemd)
编辑 [`deploy/worker.service`](deploy/worker.service):根据你的代码仓库克隆路径设置 `WorkingDirectory`、`EnvironmentFile`、`User`
以及 `node` 的路径。然后安装并启动它:
```
sudo cp deploy/worker.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now worker
```
`systemctl stop worker` 会发送 **SIGTERM** —— worker 将停止接收作业,并让正在运行的
容器在退出前完成任务。
### macOS (launchd)
编辑 [`deploy/com.pi-dispatch.worker.plist`](deploy/com.pi-dispatch.worker.plist) 及其包装脚本
[`deploy/worker-env-wrapper.sh`](deploy/worker-env-wrapper.sh):设置仓库根目录和日志路径(launchd
没有 `EnvironmentFile`,因此包装脚本会在运行时加载 `.env`)。然后引导它:
```
launchctl bootstrap gui/$(id -u) deploy/com.pi-dispatch.worker.plist
```
`launchctl bootout gui/$(id -u)/com.pi-dispatch.worker` 会发送 **SIGTERM**,实现同样的优雅清理。
### Windows (nssm)
将 `nssm.exe` 加入 PATH ([nssm.cc](https://nssm.cc)),在
[`deploy/nssm-install.cmd`](deploy/nssm-install.cmd) 中设置 `SERVICE` / `REPO` / `LOGDIR`,
然后运行它并启动服务:
```
deploy\nssm-install.cmd
nssm start pi-dispatch-worker
```
### 计划重启前的清理
计划重启不应中断任何正在进行的作业。暂停,等待队列变为空闲状态,重启,然后
恢复:
```
pi-dispatch pause # stop taking new jobs (durable)
pi-dispatch status # repeat until "active": 0 — nothing in flight
sudo systemctl restart worker # (or the launchctl / nssm equivalent)
pi-dispatch resume # take jobs again
```
因为暂停是持久化的,即使重启速度超过了你的 `resume`,worker 也会以暂停状态返回,因此
在间隙中不会漏掉任何东西。
**Windows 注意事项**:使用 nssm 的 **console-stop** (`nssm stop`) 停止服务,这会传递一个
worker 可以处理并优雅清理的信号。任务计划程序是一个较弱的备选方案 —— 它会通过
强制终止来停止任务,使 worker 没有机会进行清理;在运行中被杀死的作业会留下一个孤立的容器,
该容器将由 worker 的 **boot reaper** 在下次启动时清除,而不是被优雅地清理。
## 管理(pi 扩展)
管理界面 —— 即本 README 顶部展示的仪表盘和命令记录 —— 是位于 [`admin/`](admin/) 中的一个 **pi
扩展**,它会加载到*你自己的*交互式 pi 会话中 —— 无需守护进程、无需 Web
应用,**完全不需要网络端口**。你可以在此检出目录中使用 `pi -e admin/src/index.ts` 加载它,将该路径添加
到 `~/.pi/agent/settings.json` 的 `"extensions"` 数组中,或者直接在此检出目录中运行 pi:一旦你信任了该项目,
仓库内的 `.pi/extensions` 垫片就会自动加载。
直接输入 `/dispatch` 即可打开实时仪表盘覆盖层 —— 每秒刷新一次快照,按 `p`/`r` 可就地暂停/恢复
队列,`↑`/`↓` 可在触发器和运行列表之间移动,按 `Enter` 可深入查看任意一项。**触发器支持
就地编辑**:在触发器上按 `Enter` 可显示其信任模型,`e` 可编辑其流程,`x` 可删除,`a`
可添加(带有引导且优先考虑友好性),`s` 可编辑限制 —— 每次写入都由操作员键入、经过验证且是原子性的,并且
会被 worker/receiver **实时重新加载**(无需重启)。在运行记录上按 `Enter` 会打开其不含 PII 的完整记录:

它还添加了**在本地运行且无需模型参与**的 `/dispatch` 命令:
- `status` —— 队列计数、暂停状态、预算;`budget` —— 今日相对于每日上限的消费情况
- `pause` / `resume` —— 队列的开启/关闭开关
- `runs` / `logs` —— 最近的运行记录,以及某次运行的原始日志
- `triggers` —— 已配置的触发器(也可在覆盖层中编辑:`a`/`e`/`x`,实时应用)
- `run [task]` —— 将针对本地文件夹的流程加入队列(由操作员键入;脏树保护依然适用)
- `settings` / `set ` / `unset ` —— 运行时覆盖层
`/dispatch pause|resume|status` 是基于与 `pi-dispatch pause|resume|status` 相同的**持久化开关**(见上文**控制正在运行的 worker**)的第二种界面,
而不是一种新机制;`runs`
和 `logs` 读取的是与下方**运行历史**中**相同**的 `logs/.json` / `.log` 文件。该扩展
仅读取队列计数、运行记录和设置覆盖层 —— 这些都不包含凭证信息。
### 通过你的 AI 操作 pi-dispatch
该扩展支持 AI 操作,因此你的助手可以驱动它 —— 但**每次更改都会要求你先进行确认**。
模型可调用的工具包括:读取操作(`status`、`runs`、`triggers`);开启/关闭(`pause`/`resume`,无需
确认 —— 可逆且资金安全);受限的 `dispatch_run` 入队;以及**需确认门控的写入操作**
`dispatch_set`(更改限制)和 `dispatch_trigger_add`/`_edit`/`_delete`。写入工具只有在
**你确认了显示确切“修改前→修改后”的对话框后**才会应用更改,并且**在不存在交互式操作员时
会拒绝执行 —— 什么也不写**(因此,被提示词注入的会话无法提高你的上限或添加付费
触发器;模型只能发出调用,只有你的按键才能批准它)。内置的 `operate-pi-dispatch` 技能
会告知模型如何使用这些门控:明确陈述更改,并接受拒绝。`CONST-BUDGET-BEFORE-TOKENS`
和 `CONST-TRIGGER-AUTHOR-GATE` 保持不变 —— 确认即代表人类批准。
`dispatch_run` 是唯一一个**非资金安全**的模型可调用工具:与其他工具不同,它会将一个
**付费的**代理运行加入队列,该运行会**不可撤销地**就地编辑文件夹 —— 并且与受确认门控的写入不同,它
没有操作员确认。它受到**六个**独立限制的约束(限制了影响范围,而非完全禁止):
文件夹白名单 `PI_DISPATCH_RUN_ROOTS`(realpath + 包含关系);在 HEAD 处读取已提交的、针对特定流程的
`ai-trigger: allow` 主动 opt-in(默认为**拒绝**);脏树拒绝(无 force 选项);
工具上没有调节消费的参数;每小时速率限制;以及每日上限。不要认为它是资金安全
或可逆的 —— 它既不是前者也不是后者。原始的 `.log` 是不受信任的容器输出,仅在覆盖层
查看器中渲染,永远不会进入模型上下文。设置会保存在 `settings.json` 覆盖层
(`PI_SETTINGS_FILE`;键包括 `model`、`provider`、`maxTurns`、`dailyCap`、`concurrency`)中,并按
作业生效 —— `concurrency` 在下次提取时生效。支持的 pi 版本被固定为 `0.80.7`;加载时的
能力探测是全有或全无的,在任何其他版本上都会坚决拒绝。
只有当流程的 `.pi/skills//SKILL.md` frontmatter 设置了
`ai-trigger: allow`(默认**拒绝**)时,该流程才可被 AI 触发;未指定此类主动 opt-in 流程的 AI 触发请求将被拒绝。
## 运行历史
worker 会在 `PI_LOGS_DIR` 下保存持久的、针对每个作业的记录(默认:你操作系统的临时目录,
`.../pi-dispatch/logs)。每个作业都会写入一个仅包含 ID 的状态记录 `logs/.json` —— 仅保留稳定的 ID
(投递 GUID、`repo#number`),绝不包含 issue 或评论文本。设置 `PI_CAPTURE_JOB_LOGS=1` 以**同时**
将容器的原始 stdout/stderr 捕获到 `logs/.log`;这是**opt-in 且默认关闭的**,
因为该原始流可能包含 issue 和评论文本(PII)。这两个文件都保留在主机端,
永远不会被挂载到作业容器中,并且已被 gitignore 忽略。启动时的扫描会清理任何早于
`PI_LOG_RETENTION_DAYS`(默认为 30;`0` 表示永久保留)的文件。
## 触发器:cron、labels、comments、pull requests
每一个常设触发器 —— 无论是 cron 计划还是 GitHub 触发器 —— 都位于统一的
**`triggers.json`** 中,这是一个由 `{ on, run }` 对组成的列表,由 worker (cron) 和 receiver
(GitHub) 共同读取。使用 `PI_TRIGGERS_FILE` 将两个服务都指向它;worker 将其视为可选(未设置 =
关闭 cron),而 receiver 则要求必须设置。
```
{ "triggers": [
{ "on": { "type": "cron", "id": "nightly", "pattern": "0 3 * * *" },
"run": { "kind": "local", "folder": "/srv/site", "flow": "tidy", "task": "run the nightly tidy" } },
{ "on": { "type": "label", "any": ["pi:frontend"] }, "run": { "kind": "github", "flow": "frontend-fix" } },
{ "on": { "type": "comment", "phrase": "@pi" }, "run": { "kind": "github", "flow": "fix" } },
{ "on": { "type": "pull_request", "action": ["labeled"], "any": ["pi:review"] }, "run": { "kind": "github", "flow": "review" } }
] }
```
`on × run` 矩阵是信任边界,在加载时会进行严格的失败即报错验证:`cron` 触发器必须运行
`local`(它没有 webhook 投递、issue/PR 编号或正文),而每个 webhook 触发器都运行 `github`。
### 安排重复性作业
cron 触发器会根据 cron 模式,通过某个流程运行本地文件夹 —— `pattern` 是一个 5 或 6 字段的 cron
表达式;`provider`、`model` 和 `maxTurns` 在 `run` 中是可选的,若未设置则回退至 worker 的
默认值。cron 的 `folder` 是一个**主机路径** —— worker 在主机上运行
([`DES-WORKER-ON-HOST`](specs/design.md))并将该文件夹挂载到作业容器中,因此它必须能被
worker 的用户读取。
```
cp triggers.example.json triggers.json # then edit the cron entry's "folder" to a REAL absolute path
# 在 .env 中:PI_TRIGGERS_FILE=/absolute/path/to/triggers.json
npx pi-dispatch worker
```
逐字复制 `triggers.example.json` 会导致 worker **拒绝启动**并抛出
`configError: folder does not exist`,直到 cron 触发器的 `folder` 指向一个真实的路径 —— 这是
故意的失败即报错,因此损坏的触发器绝不会静默失效。
## GitHub 自动化
pi-dispatch 也可以由 GitHub 触发 —— 给 issue 打上 label,容器就会在全新的克隆上处理它、
打开 PR 并进行回复。仓库的 **webhook** 驱动此过程(设置一个 `WEBHOOK_SECRET`),并且 worker
默认通过 `GITHUB_AUTH_SOURCE` 向 GitHub 进行身份验证:`gh`(使用 `gh auth token`)或一个仓库范围受限的
细粒度 **PAT**。GitHub **App 是可选的** —— 它能实现更严格的 token 范围限制,并且是多租户场景
所必需的。哪些 labels、评论短语以及 pull_request 动作会触发哪个流程,都在
上方统一的 **`triggers.json`** 中配置;receiver **要求**设置 `PI_TRIGGERS_FILE`。
```
flowchart LR
GH["GitHub repo
issue labeled, @pi comment, or PR"] -->|"webhook, HMAC-signed"| R
subgraph EDGE["receiver/ — public edge, binds 0.0.0.0"]
R["verify raw-body HMAC (401 on mismatch)
filter: label allowlist, author gate, bot-loop"]
end
R -->|"enqueueGitHubJob (jobId = gh-<delivery>)"| Q[("Valkey + BullMQ
pi-jobs, AOF, 31d+ retention")]
subgraph HOST["worker/ — host process"]
W["mint scoped token, refuse an unprotected branch,
hardened clone at the default-branch SHA, run container"]
end
Q --> W
W -->|"docker run --rm"| C["job container: the agent commits,
pushes --force-with-lease, gh pr create, comments"]
C -->|"GITHUB_TOKEN via env only, never merges"| GH
```
- 只有协作者的 label 或 `@pi` 评论才能启动作业(label *即是*审批步骤)。
- 除了带有 label 的 issue 外,pi-dispatch 还能处理 **pull requests**:给 PR 打上 label、在 PR 上评论,或者在 PR
打开或更新时自动触发 —— 自动(`opened`/`synchronize`/`reopened`)路径取决于 PR
作者是否为协作者,因此来自陌生人的 fork PR 永远不会自动触发。PR 触发器
仅运行配置好的流程;该流程(一个 repo 技能)通过 `gh` 进行审查、评论或推送到 PR。
- 代理会获得一个**仓库范围受限、短期的 token** —— 坦白说:该 token *可以*执行合并,因为
GitHub 将推送和合并在同一个 `contents: write` 范围内进行限制。**默认分支上的分支保护才是
真正的控制手段**,因此 worker 会**拒绝**不受保护的仓库。具体细节请参阅 `SECURITY.md`。
每次投递在任何内容加入队列之前都会经历相同的门控 —— 签名会在解析正文*之前*对原始字节进行验证,并且
`sender.id` 机器人循环防护会在作者检查之前触发(因此
receiver 自身的评论,以及代理自身推送到 PR 头部的操作,都永远无法重新触发作业):
```
flowchart TD
D["POST delivery"] --> V{"HMAC over the
raw body valid?"}
V -->|no| E401["401 — reject, enqueue nothing"]
V -->|yes| S{"sender.id ==
our own id?"}
S -->|"yes"| D204a["204 — drop (bot-loop guard)"]
S -->|no| A{"allowlisted label, collaborator @pi,
or collaborator-authored PR?"}
A -->|no| D204b["204 — drop"]
A -->|yes| EN{"enqueue to Valkey"}
EN -->|ok| A202["202 — queued
(duplicate delivery = no-op, deduped by GUID)"]
EN -->|"Valkey down"| E503["503 — GitHub redelivers,
deduped by GUID"]
```
## 横向对比
**对比 Claude Code GitHub Action。** 在 GitHub 自动化方面,通常会首选该 Action ——
[`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action)(MIT 协议,约 8.4k 星)已
正式发布(GA),并且只需 10% 的精力就能实现基于 label 触发的 issue 自动化。pi-dispatch 针对的是更细分的场景:
**你运行 pi,在你自己的硬件上,并且你需要真正的队列、容器边界,以及 —— 该 Action 做不到的 —— 针对本地文件夹运行流程**,而不仅仅是 GitHub 仓库,且无需消耗托管运行器的使用时长。
**对比 Claude Code 例程和 `/loop`。** 例程会根据 cron 计划运行重复的代理任务(在云端
托管);`/loop` 则在你的会话中按一定间隔重复提示。对于通用的重复性工作,它们更简单 —— 无需托管 —— 并且通常是
正确的选择。pi-dispatch 的 cron 触发器是相同的理念,但侧重点不同:运行发生在**你构建的容器镜像中**,在**你的**
硬件上,受限于**你的**队列和消费上限。当任务需要托管例程无法提供的环境时 —— 比如项目特定的
工具链和系统库,或者内置的 **Playwright +
Chromium**,让计划任务能够构建前端、截图并进行迭代直到渲染正确,然后将前后的对比图附加到 PR 中 —— 这便是它的优势所在。经验法则:如果重复性任务是“运行一个提示”,请使用
例程;如果任务是“按照计划,在我控制的镜像中运行该项目的真实构建/测试/视觉循环”,那么就用本工具。
## 状态
本地文件夹路径(镜像、worker、`pi-dispatch run` / `worker`)、GitHub 路径(receiver → 队列 →
克隆 → 针对ssue 和 pull request 的 PR)以及针对本地文件夹的计划 (cron) 触发器均已构建完成并可正常
工作;worker 可以在终端中运行,或者作为 Linux、macOS 或 Windows 上的 OS 服务运行(参见**作为服务运行**)。管理界面以 pi 扩展的形式提供(参见**管理(pi 扩展)**)。设计细节在
[`specs/`](specs/) 中有详细说明 —— 请从 [`specs/constitution.md`](specs/constitution.md) 了解不可妥协的原则,
从 [`specs/design.md`](specs/design.md) 了解相关决策以及被否决的方案。
## 许可证
MIT。详见 [LICENSE](LICENSE)。基于 Mario Zechner 开发的 [pi](https://github.com/earendil-works/pi) 构建,是他完成了其中真正困难的部分。标签:AI编程助手, DevOps工具, MITM代理, NIDS, 任务调度, 容器化, 特征检测, 网络调试, 自动化, 自定义脚本, 自托管, 请求拦截