@edgehero/pi-dispatch-admin 1.10.1 → 2.0.0
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 +165 -45
- package/dist/index.mjs +164975 -24947
- package/package.json +5 -3
- package/skills/operate-pi-dispatch/SKILL.md +28 -5
package/README.md
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/banner.png?v=0.
|
|
2
|
+
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/banner.png?v=2.0.0" alt="pi-dispatch: run the pi coding agent as a self-hosted service" width="880">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# pi-dispatch: run the pi coding agent as a self-hosted service
|
|
6
6
|
|
|
7
|
-
**Let a coding agent work on your repositories while you are not watching, without surprise bills and
|
|
7
|
+
**Let a coding agent work on your repositories while you are not watching, without surprise bills and
|
|
8
|
+
without giving it the keys to your machine.**
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
[pi](https://github.com/earendil-works/pi) is an open source coding agent that you normally run in a
|
|
11
|
+
terminal. It has no job queue, no spend limit and, by its own README, no permission system. pi-dispatch
|
|
12
|
+
adds exactly those, so pi can run unattended: on a cron schedule, on your repo's issues and PRs, or from
|
|
13
|
+
the command line. Every job runs in its own container. Spend is checked before a single token is spent,
|
|
14
|
+
for the whole deployment and per repo or folder. Everything the agent did, and what it cost, is recorded,
|
|
15
|
+
graphed and priced.
|
|
10
16
|
|
|
17
|
+
On a forge you stay in control at both ends. A job starts only when someone with write access to the repo
|
|
18
|
+
asks for one, with a label, an `@pi` comment or a review. What comes back is a pull request that you
|
|
19
|
+
review and merge yourself, because pi-dispatch never merges anything. It is not another agent to compare
|
|
20
|
+
with the one you use. It is the queue, the budget and the box around the pi you already run.
|
|
21
|
+
|
|
22
|
+
> This npm package, **`@edgehero/pi-dispatch-admin`**, is the **operator console**, a pi extension. The
|
|
23
|
+
> service itself is [`@edgehero/pi-dispatch`](https://www.npmjs.com/package/@edgehero/pi-dispatch)
|
|
24
|
+
> (worker and CLI) and [`@edgehero/pi-dispatch-receiver`](https://www.npmjs.com/package/@edgehero/pi-dispatch-receiver)
|
|
25
|
+
> (the webhook edge). The [main repo](https://github.com/edgehero/pi-dispatch) has the job image, the
|
|
26
|
+
> docs (Docker, Podman, egress, every forge) and SECURITY.md.
|
|
11
27
|
|
|
12
28
|
## How it works
|
|
13
29
|
|
|
@@ -20,66 +36,141 @@ Every trigger produces the same job, through the same path: one queue, one conta
|
|
|
20
36
|
Valkey + BullMQ durable queue: survives reboots, absorbs bursts
|
|
21
37
|
│
|
|
22
38
|
▼
|
|
23
|
-
under the
|
|
24
|
-
|
|
39
|
+
under the day, week and if not: refused here, before any spend
|
|
40
|
+
month caps, the scoped
|
|
41
|
+
limits and the turn budget?
|
|
25
42
|
│ yes
|
|
26
43
|
▼
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
limits, /job mounted read-only
|
|
44
|
+
one container per job Docker or Podman, removed after the job: all
|
|
45
|
+
capabilities dropped, no-new-privileges,
|
|
46
|
+
memory/CPU/pids limits, /job mounted read-only
|
|
30
47
|
│
|
|
31
48
|
▼
|
|
32
49
|
pi + your .pi/skills edits your code in place, opens a PR or MR, comments back
|
|
33
50
|
```
|
|
34
51
|
|
|
35
|
-
The container is the boundary
|
|
52
|
+
The container is the boundary, and it is pi's missing permission system. The worker builds the isolation
|
|
53
|
+
flags itself, so nothing an image contains can weaken them. The non root user comes from the **image** on
|
|
54
|
+
Docker Desktop (and for a worker running as uid 1001), and from the worker's own uid on a native Linux
|
|
55
|
+
daemon. That is why an image has to meet the checklist in
|
|
56
|
+
[`docs/job-image.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/job-image.md). What each
|
|
57
|
+
container runtime guarantees is declared in
|
|
58
|
+
[`docs/backends.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/backends.md), and
|
|
59
|
+
`pi-dispatch doctor --live` reads it back from real containers.
|
|
60
|
+
|
|
61
|
+
Spend is checked before a container starts, so a runaway or a junk trigger costs a refusal, not a bill.
|
|
62
|
+
The job image ships Playwright and Chromium, so a flow can build a frontend, screenshot it and iterate.
|
|
63
|
+
|
|
64
|
+
**What the agent can see.** Two credentials go into each job as environment values, the provider key and
|
|
65
|
+
the forge token, and **the agent can read both**. The guardrails tell it never to print or send them, but
|
|
66
|
+
that is prompt text, not enforcement. Set a spend limit at your provider. On GitHub, the default `gh` auth
|
|
67
|
+
source forwards **your whole login, full scope and never expiring**, and `pi-dispatch doctor` warns about
|
|
68
|
+
it; the GitHub App gives each job a one hour token for one repo instead. Network egress is denied by
|
|
69
|
+
default and goes through an allowlist proxy, and the allowed hosts are yours to list
|
|
70
|
+
([`docs/egress.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/egress.md)).
|
|
36
71
|
|
|
37
72
|
## Triggers: what starts a job
|
|
38
73
|
|
|
39
|
-
Every trigger is one `{ on, run }` entry in a single `triggers.json
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| `
|
|
44
|
-
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
Every trigger is one `{ on, run }` entry in a single `triggers.json`. The worker reads it live for cron,
|
|
75
|
+
the receiver reads it live for forge webhooks, and the console can edit it. **`on` is what fires it;
|
|
76
|
+
`run` is the skill it runs.**
|
|
77
|
+
|
|
78
|
+
| `on.type` | Fires on | What the agent gets as its task |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `cron` | your schedule | the `task` written in the file |
|
|
81
|
+
| `label` | a label on an **issue** (or an Azure work item), never a pull request | the issue title and body |
|
|
82
|
+
| `comment` | a comment containing your phrase, such as `@pi` | the comment, plus the issue title and body |
|
|
83
|
+
| `pull_request` | a PR or MR event, including its close | the PR title and body |
|
|
84
|
+
| `issue` | an issue closing (`once: true` makes it a one-shot) | the issue title and body |
|
|
85
|
+
|
|
86
|
+
Four forges: GitHub, GitLab, Forgejo (and Gitea), Azure DevOps. **Who may fire a trigger is your forge's
|
|
87
|
+
decision, not this service's.** On GitHub the label *is* the approval, because only collaborators can add
|
|
88
|
+
one. GitLab, Forgejo and Azure check the actor's permission through their APIs. A close trigger checks the
|
|
89
|
+
person who **closed** the item (Azure has no close trigger yet, and refuses one at load). Each forge's
|
|
90
|
+
action words are checked when the file loads, so a word from the wrong forge is refused instead of never
|
|
91
|
+
matching. The full reference is
|
|
92
|
+
[`docs/triggers.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/triggers.md).
|
|
93
|
+
|
|
94
|
+
**Flows and workflows.** `run.flow` names a skill committed to the target repo at
|
|
95
|
+
`.pi/skills/<flow>/SKILL.md`, read from the **default branch**. The repo owns the prompt, and merging it
|
|
96
|
+
is the repo's consent. A skill may call other skills, which is already a workflow. For typed multi-stage
|
|
97
|
+
workflows, stage a pi extension such as `@juicesharp/rpiv-workflow` into the deployment. It is pinned to an
|
|
98
|
+
exact version, installed on your host (never at job time, since jobs run offline), loaded in every
|
|
99
|
+
container and declinable per trigger. Anything you installed with `pi install` is staged automatically at
|
|
100
|
+
your host's version.
|
|
101
|
+
|
|
102
|
+
`run.command` is the second entry point. A trigger may name a registered command that a staged extension
|
|
103
|
+
provides (`"command": "wf run nightly"`). The job's whole prompt is that line, handled by the extension
|
|
104
|
+
with no model turn in between. It picks which vetted command runs, never what code runs, and it is never
|
|
105
|
+
AI triggerable: job chaining refuses a request naming one, and `dispatch_run` cannot express one.
|
|
106
|
+
|
|
107
|
+
Four things to know before you build on it:
|
|
108
|
+
|
|
109
|
+
- A trigger names a **flow or a command, never a workflow**. That entry point decides which stages run.
|
|
110
|
+
- A job is **not an interactive session**. The container hands pi one prompt and reads the exit line.
|
|
111
|
+
- One trigger is **one job, one budget slot and one turn budget**. Ten stages share the same
|
|
112
|
+
`PI_MAX_TURNS` and token budget, and running out ends the job as a policy refusal that is never retried.
|
|
113
|
+
- **State survives only on local jobs.** A cron or CLI job has your folder mounted read-write, so state
|
|
114
|
+
persists. A forge job gets a fresh clone that is thrown away with the container
|
|
115
|
+
([`docs/workflows.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/workflows.md)).
|
|
54
116
|
|
|
55
117
|
## The console: `/dispatch`
|
|
56
118
|
|
|
57
|
-
One command puts a live
|
|
119
|
+
One command puts a live terminal view over the whole deployment:
|
|
58
120
|
|
|
59
121
|
<p align="center">
|
|
60
|
-
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/dispatch-dashboard.png?v=
|
|
122
|
+
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/dispatch-dashboard.png?v=2.0.0" alt="The /dispatch panel: status, spend meters, triggers, pause windows, scoped limits, held and failed jobs, runs, and settings" width="820">
|
|
61
123
|
</p>
|
|
62
124
|
|
|
63
|
-
- **Status and spend.** Queue and worker state, day
|
|
64
|
-
|
|
125
|
+
- **Status and spend.** Queue and worker state, day, week and month spend meters, a daily token counter,
|
|
126
|
+
and the run history with tokens and cost per job. `Enter` on a run opens its full record, `x` on a
|
|
127
|
+
running job cancels it after asking, and `b` reopens a finished run's workspace.
|
|
128
|
+
- **Insights, the one analytics page.** Press `i` (or type `/dispatch insights`) and one self contained
|
|
129
|
+
page opens in your browser. It shows the budget dials, plan verdicts against API rates, daily,
|
|
130
|
+
cumulative and per flow spend charts, breakdowns by flow, trigger, model and repo, and the trigger and
|
|
131
|
+
flow topology with spend on each trigger. A plan covered run never shows as $0.00, and an estimate is
|
|
132
|
+
always marked as one ([`docs/insights.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/insights.md)).
|
|
65
133
|
|
|
66
134
|
<p align="center">
|
|
67
|
-
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/insights-view.png?v=
|
|
135
|
+
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/insights-view.png?v=2.0.0" alt="The insights page: KPI tiles, budget dials, a plan verdict, spend charts, the four breakdowns, and the topology with spend badges" width="820">
|
|
68
136
|
</p>
|
|
69
137
|
|
|
70
|
-
- **Triggers, editable live.**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- **
|
|
138
|
+
- **Triggers, editable live.** Add, edit and delete triggers without a restart. Drill-ins show what
|
|
139
|
+
fires each one, what it runs and who may fire it. A one-shot shows armed or spent. A spent entry
|
|
140
|
+
stays in the list, matches nothing, and is re-armed when you delete its `on.disarmed` mark from the file.
|
|
141
|
+
Triggers that run third party code or a custom image carry a badge. Turning either on or off stays an
|
|
142
|
+
edit to the reviewed `triggers.json`, which neither the console nor a model callable tool makes for you.
|
|
143
|
+
- **Quiet hours.** Pause windows per folder or repo, timezone aware. A paused job waits, never drops, and
|
|
144
|
+
costs nothing.
|
|
145
|
+
- **Scoped limits.** Job caps per repo or folder (day, week, month), refused before any spend, plus a
|
|
146
|
+
ceiling on how many run at once, enforced by making jobs wait. Local jobs also have a fixed guard with
|
|
147
|
+
no switch: one job per folder at a time (it lives in the worker process, and one worker per container
|
|
148
|
+
daemon is the supported shape), because two agents editing one working tree race each other with no
|
|
149
|
+
gate and no undo.
|
|
150
|
+
- **Held and failed jobs.** A trigger with `run.waitFor` holds its job in the queue, unstarted and
|
|
151
|
+
unbilled, until a time passes or your check script exits 0. The panel shows a **held** section while
|
|
152
|
+
anything waits (the target, the condition and how long), and a **failed** section for jobs the queue
|
|
153
|
+
marked failed after their last attempt. `dispatch_waits` lists held jobs and `dispatch_wait_cancel` stops one behind your confirm
|
|
154
|
+
([`docs/wait-for.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/wait-for.md)).
|
|
155
|
+
- **AI operable, with a human gate.** Model callable tools can change limits (global and scoped) and
|
|
156
|
+
manage triggers and pause windows. Every **config** change asks the operator in a prompt the model
|
|
157
|
+
cannot answer, and is refused when no operator is present. `dispatch_pause` and `dispatch_resume` skip
|
|
158
|
+
that confirm on purpose: they are reversible and spend nothing.
|
|
159
|
+
- **One tool is not money safe.** `dispatch_run` queues a **paid** run that edits a local folder in place
|
|
160
|
+
with no undo, without a confirm. Six separate limits bound it instead: the `PI_DISPATCH_RUN_ROOTS` folder
|
|
161
|
+
allowlist, a committed per flow `ai-trigger: allow` opt in (read at a commit fixed before the agent
|
|
162
|
+
starts), a dirty tree refusal with no force option, no spend settings on the tool, a per hour rate limit
|
|
163
|
+
and the worker's daily cap. It cannot run commands, and a chained job asking for a `command` is refused.
|
|
164
|
+
Read [`SECURITY.md`](https://github.com/edgehero/pi-dispatch/blob/main/SECURITY.md) before you enable it.
|
|
165
|
+
The bundled `operate-pi-dispatch` skill teaches the agent these gates.
|
|
166
|
+
- **Logs stay put.** Raw container output shows only in the panel's viewer, never in model context.
|
|
76
167
|
|
|
77
168
|
## More than one machine
|
|
78
169
|
|
|
79
|
-
The console reads the whole deployment, not one host. The status line names the workers
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
170
|
+
The console reads the whole deployment, not one host. The status line names the workers once they have
|
|
171
|
+
names. The run detail names the machine that ran a job. The pause switch stops every queue, and the
|
|
172
|
+
scheduler view spans hosts. On a single host none of that shows, because there is nothing to tell apart.
|
|
173
|
+
See [`docs/multi-host.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/multi-host.md).
|
|
83
174
|
|
|
84
175
|
## Install
|
|
85
176
|
|
|
@@ -87,14 +178,43 @@ there is nothing to distinguish. See [`docs/multi-host.md`](../docs/multi-host.m
|
|
|
87
178
|
pi install npm:@edgehero/pi-dispatch-admin # then, in pi: /dispatch
|
|
88
179
|
```
|
|
89
180
|
|
|
90
|
-
**This is the default way to set up pi-dispatch.** With nothing configured, `/dispatch`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
181
|
+
**This is the default way to set up pi-dispatch.** With nothing configured, `/dispatch` starts a guided
|
|
182
|
+
setup, in this order:
|
|
183
|
+
|
|
184
|
+
1. an opening choice and a deployment folder;
|
|
185
|
+
2. a container runtime check (Docker, or rootless Podman on Linux), with pointers per OS if it is missing.
|
|
186
|
+
It runs **first** on purpose, so no bandwidth is spent where `up` cannot work;
|
|
187
|
+
3. a consented npm install of the pinned runtime;
|
|
188
|
+
4. `pi-dispatch up`, running its own prompts in your terminal (Valkey with its password, the egress proxy,
|
|
189
|
+
the config files);
|
|
190
|
+
5. the deployment pointer, so `/dispatch` finds this deployment from any directory;
|
|
191
|
+
6. a notice naming the file your provider key belongs in;
|
|
192
|
+
7. optional: the worker as a service, GitHub App credentials (`setup github`, the one step that mints a
|
|
193
|
+
private key), a trigger edge (receiver service, docker compose profile or the polling command), and a
|
|
194
|
+
first **cron** trigger for the repo you are in.
|
|
195
|
+
|
|
196
|
+
Every step says what it will do, asks first and can be declined. Nothing is written into your repo, and no
|
|
197
|
+
credential passes through a dialog. A deployment whose queue is only down keeps the unreachable banner:
|
|
198
|
+
setup appears when there is nothing, never over an outage.
|
|
199
|
+
|
|
200
|
+
**Already have a deployment?** The panel finds it through the deployment pointer, or through the same env
|
|
201
|
+
vars your worker uses (`VALKEY_URL`, `PI_LOGS_DIR`, `PI_SETTINGS_FILE`, `PI_TRIGGERS_FILE`,
|
|
202
|
+
`PI_PAUSE_WINDOWS_FILE`, `PI_SUBSCRIPTIONS_FILE`). Your env always wins. What your env does not set comes
|
|
203
|
+
from the deployment's own `.env`, by the same rule `pi-dispatch doctor` uses: only the pointer's
|
|
204
|
+
deployment folder (never a `.env` in the folder pi started in, which any repository could ship), and only
|
|
205
|
+
when that file is yours and nobody else can write it. A key your env sets differently is named once as a
|
|
206
|
+
warning, never with its value. The switches that grant a capability (`PI_BACKENDS`, `PI_EGRESS`,
|
|
207
|
+
`DOCKER_HOST`, `PI_DISPATCH_RUN_ROOTS`) are never read from a file.
|
|
208
|
+
|
|
209
|
+
`dispatch_run` does nothing until you set one more variable yourself. `PI_DISPATCH_RUN_ROOTS` is empty by
|
|
210
|
+
default, and an empty allowlist refuses every folder. The deployment pointer cannot set it (it carries
|
|
211
|
+
paths, never capability grants), so widening that allowlist is always your own env edit. No allowlist
|
|
212
|
+
reaches commands either: a `run.command` fires from the reviewed triggers file only.
|
|
95
213
|
|
|
96
214
|
## Get the whole thing
|
|
97
215
|
|
|
98
216
|
### → **https://github.com/edgehero/pi-dispatch**
|
|
99
217
|
|
|
100
|
-
MIT, self
|
|
218
|
+
MIT, self hosted. Read [`SECURITY.md`](https://github.com/edgehero/pi-dispatch/blob/main/SECURITY.md)
|
|
219
|
+
before you rely on it: it states plainly what is and is not defended. Short version: the trust model is a
|
|
220
|
+
GitHub Action's. Whoever can merge to your default branch can instruct the agent.
|