edgehero/pi-dispatch

GitHub: edgehero/pi-dispatch

pi-dispatch 为 pi 编码代理提供了缺失的运维调度层,包含持久化队列、容器沙箱、消费上限和实时管理面板,支持 CLI、cron 和 GitHub 事件三种触发方式。

Stars: 4 | Forks: 1

pi-dispatch — run the pi coding agent as a self-hosted service

# pi-dispatch **将 [pi](https://github.com/earendil-works/pi) 编码代理作为一种服务运行 —— 可按需触发,基于 cron 定时任务,或由 GitHub issue 或 pull request 触发 —— 运行在你可控的容器中,并配备持久化队列、 消费上限和实时管理面板。** ![/dispatch 仪表盘覆盖层 —— 主题色:实时队列状态、日/周/月消费计量器 + 每日 token 计数器、统一触发器面板(cron、label、comment、pull_request —— 可选且可编辑)以及交互式运行列表,同处于一个带框的 TUI 中](https://static.pigsec.cn/wp-content/uploads/repos/cas/89/8950ade925044fd07c5990cb2942a49f8cdda7dd6249af1757b1d6e49f4d5603.svg) ![/dispatch status、runs 和 triggers 的记录 —— 队列计数、带有每个任务 token 和成本核算的运行历史表,以及统一的 {on,run} 触发器列表](https://static.pigsec.cn/wp-content/uploads/repos/cas/92/923cbf44a929a363783c9e4c963ee8e1399e2d6b92cf39876fed2866086cec45.svg) 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 的完整记录: ![RUN_DETAIL 深入查看 —— 某次运行无 PII 记录的彩色事后分析:结果、目标、包含持续时间的时间信息、turns/退出/预算槽位、token 和成本,以及命名了衍生子进程的链路信息](https://static.pigsec.cn/wp-content/uploads/repos/cas/52/529beb9b8acf1b630c70256a9deca6bf7ef7757fa225ddf0c0660cd47096d85f.svg) 它还添加了**在本地运行且无需模型参与**的 `/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, 任务调度, 容器化, 特征检测, 网络调试, 自动化, 自定义脚本, 自托管, 请求拦截