@edgehero/pi-dispatch-admin 0.3.0 → 0.5.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 CHANGED
@@ -1,82 +1,89 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/banner.png?v=0.5.0" alt="pi-dispatch — run the pi coding agent as a self-hosted service" width="880">
2
+ <img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/banner.png?v=0.5.0" alt="pi-dispatch: run the pi coding agent as a self-hosted service" width="880">
3
3
  </p>
4
4
 
5
- # pi-dispatch — run the pi coding agent as a self-hosted service
5
+ # pi-dispatch: run the pi coding agent as a self-hosted service
6
6
 
7
- **[pi](https://github.com/earendil-works/pi) is a superb coding agent — but it has no job queue, no spend limit, and, by its own README, no permission system.** pi-dispatch is the operational layer that adds exactly those, so you can run pi **unattended and safely**: on a cron schedule, on your repo's GitHub issues and PRs, or straight from the CLI — every job in an isolated container, with spend bounded *before* a single token is spent.
7
+ **[pi](https://github.com/earendil-works/pi) is a superb coding agent, but it has no job queue, no spend limit, and, by its own README, no permission system.** pi-dispatch adds exactly those, so you can run pi unattended and safely: on a cron schedule, on your repo's issues and PRs, or straight from the CLI. Every job runs in an isolated container, with spend bounded before a single token is spent.
8
8
 
9
- > This npm package — **`@edgehero/pi-dispatch-admin`** — is the **operator console** (a pi extension). The queue, worker, container image, and GitHub receiver that make up the service live in the [main repo](https://github.com/edgehero/pi-dispatch); install this to **drive** a deployment, and get the whole thing there.
9
+ > This npm package, **`@edgehero/pi-dispatch-admin`**, is the **operator console** (a pi extension). The service itself is `@edgehero/pi-dispatch` (worker + CLI) and `@edgehero/pi-dispatch-receiver` (the webhook edge); the [main repo](https://github.com/edgehero/pi-dispatch) has the container image, docs, and SECURITY.md.
10
10
 
11
11
  ## How it works
12
12
 
13
- Every trigger produces the same job, through the same path — one queue, one container, one budget:
13
+ Every trigger produces the same job, through the same path: one queue, one container, one budget.
14
14
 
15
15
  ```
16
- CLI · cron · GitHub issue / PR
16
+ CLI · cron · label / comment / PR on GitHub, GitLab, Forgejo or Azure DevOps
17
17
  │ enqueue
18
18
  ▼
19
- Valkey + BullMQ durable queue — survives reboots, absorbs bursts
19
+ Valkey + BullMQ durable queue: survives reboots, absorbs bursts
20
20
  │
21
21
  ▼
22
- under the daily cap + ── if not: refused here, before any spend
22
+ under the daily cap + if not: refused here, before any spend
23
23
  per-job turn budget?
24
24
  │ yes
25
25
  ▼
26
- docker run --rm one ephemeral container per job:
27
- --cap-drop=ALL · non-root --no-new-privileges · /job mounted read-only
26
+ docker run --rm one ephemeral container per job: --cap-drop=ALL,
27
+ --security-opt no-new-privileges, memory/CPU/pids
28
+ limits, /job mounted read-only
28
29
  │
29
30
  ▼
30
- pi + your .pi/skills edits your code in place, opens a PR, comments back
31
+ pi + your .pi/skills edits your code in place, opens a PR or MR, comments back
31
32
  ```
32
33
 
33
- - **The container is the boundary.** pi ships no permission system, so every job runs `--cap-drop=ALL`, non-root, ephemeral, with its instructions mounted read-only. That *is* the missing permission system — enforced by Docker, not hoped for.
34
- - **Spend is bounded before tokens.** A per-job turn budget plus daily / weekly / monthly caps, checked *before* a container starts. A runaway or a junk trigger costs you a refusal, not a surprise bill.
35
- - **Three triggers, one job.** A CLI command, a cron schedule, or a GitHub issue/PR — same queue, same box, same budget. Cron is the unattended one: recurring work on your own hardware, in an image you control — one per deployment, or one per trigger.
36
- - **Your image, your tools.** Bake a project's toolchain into the Dockerfile; it ships **Playwright + Chromium**, so a flow can build a frontend, screenshot it, and iterate on the rendered result.
34
+ The container is the boundary (pi's missing permission system, enforced by Docker). Those isolation flags are built by the worker's own `docker run` argv, so nothing an image contains can weaken them; the non-root user is a property of the **image**, which is why an image has to meet the conformance checklist in [`docs/job-image.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/job-image.md). Spend is checked before a container starts, so a runaway or a junk trigger costs a refusal, not a surprise bill. The job image is yours to shape, per deployment or per trigger, and it ships Playwright and Chromium so a flow can build a frontend, screenshot it, and iterate on the render.
37
35
 
38
- ## The console — `/dispatch`
36
+ ## Triggers: what starts a job
39
37
 
40
- This extension puts a live TUI over the whole deployment in one command:
38
+ Every trigger is one `{ on, run }` entry in a single `triggers.json`, read live by the worker (cron) and the receiver (forge webhooks), and editable from the console. **`on` is what fires it; `run` is the skill it runs.**
39
+
40
+ | `on.type` | Fires on | What narrows it | What the agent gets as its task |
41
+ |---|---|---|---|
42
+ | `cron` | your schedule | nothing: a schedule is its own condition | the `task` written in the file |
43
+ | `label` | a label on an **issue** (or an Azure work item), never a pull request | a label predicate: `any`, `all`, or `none` (which can suppress a fire, never cause one) | the issue title and body |
44
+ | `comment` | a comment containing your phrase, for example `@pi` | the phrase, and one comment trigger per forge | the comment body, plus the issue title and body |
45
+ | `pull_request` | a PR or MR event | `action`, in your forge's own words, plus the same label predicate | the PR title and body |
46
+
47
+ Four forges: GitHub, GitLab, Forgejo (and Gitea), Azure DevOps. **Who may fire a trigger is your forge's decision, not this service's**: on GitHub the label *is* the approval, because only collaborators can apply one, while GitLab, Forgejo and Azure resolve the actor's permission through their APIs. Each forge's action vocabulary is validated when the file loads, so a word from the wrong forge is refused instead of silently never matching.
48
+
49
+ **Flows, and workflows.** `run.flow` names a skill committed to the target repo at `.pi/skills/<flow>/SKILL.md`, read from the **default branch**, so the repo owns the prompt and merging it is the repo's consent. A skill may call other skills, which is already a workflow. For typed multi-stage ones, a pi extension such as `@juicesharp/rpiv-workflow` can be staged into the deployment: pinned to an exact version, installed on your host (never at job time, since jobs run offline), loaded in every container, and declinable per trigger.
50
+
51
+ **How a workflow gets triggered, in one line:** `label / comment / PR / cron` fires **one job in one container**, that job runs `run.flow`, and the flow drives whatever stages follow. Four consequences worth knowing before you build on it. A trigger names a **flow, never a workflow**, so which stages run is decided inside the job by that skill. A job is **not an interactive session** (the container hands pi one assembled prompt and reads the exit line), so a workflow extension's slash command has nobody to type it: in a job the flow's instructions drive the workflow, or a small extension you also staged calls its API from a lifecycle hook. One trigger is **one job, one budget slot and one turn budget**, so ten stages share the same `PI_MAX_TURNS` and per-job token budget, and exhausting either ends the job as a policy refusal that is never retried. And whether the workflow's own state survives depends on the trigger kind: a cron or CLI job has your folder mounted read-write so state persists between runs, while a forge job gets a fresh clone that is discarded with the container. Full reference: [`docs/workflows.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/workflows.md).
52
+
53
+ ## The console: `/dispatch`
54
+
55
+ One command puts a live TUI over the whole deployment:
41
56
 
42
57
  <p align="center">
43
58
  <img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/dispatch-dashboard.png?v=0.5.0" alt="The /dispatch panel: status, spend meters, triggers, runs, and settings" width="820">
44
59
  </p>
45
60
 
46
- - **Status & spend.** Queue and worker state, day/week/month **spend meters** + a daily token counter, and a run-history table with per-job token & cost.
47
- - **Costs, analyzed honestly.** A verdict-first **COSTS view** (`c`): spend per flow/model/day over the retention window, what each declared subscription actually saves against API rates, amortized $/run, and a keyboard **what-if** that re-prices a flow's recorded token profile under another model. Every dollar carries its class — a plan-covered run never renders as $0.00, an estimate is always marked `~ est.`, and a quota no vendor discloses never grows an invented burn-down. Also plain: `/dispatch costs [7d|30d|mtd]` and a `dispatch_costs` tool whose JSON marks every value the same way.
61
+ - **Status and spend.** Queue and worker state, day/week/month spend meters, a daily token counter, and a run-history table with per-job tokens and cost.
62
+ - **Costs, analyzed honestly.** The COSTS view (`c`) shows spend per flow, model and day, what each declared subscription actually saves against API rates, and a what-if that re-prices a flow under another model. Every dollar carries its class: a plan-covered run never renders as $0.00, and an estimate is always marked as one.
48
63
 
49
64
  <p align="center">
50
- <img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/costs-view.png?v=0.5.0" alt="The COSTS view: per-plan verdicts against API rates, a daily spend sparkline, per-flow spend with API-equivalents, and subscription amortization" width="820">
65
+ <img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/costs-view.png?v=0.5.0" alt="The COSTS view: per-plan verdicts against API rates, a daily spend sparkline, per-flow spend with API equivalents, and subscription amortization" width="820">
51
66
  </p>
52
- - **Triggers, editable live.** cron / label / comment / pull_request, with colored drill-ins showing *what fires* each one, *what it runs*, and its *trust model* — added, edited, and deleted without a restart.
53
- - **You can see which triggers run third-party code.** A trigger loads the pi packages you pinned into your global overlay **unless** it carries `run.packages: false`, so the ones that do get a **`[packages]`** badge in the trigger list, and their **trust model** names the staged `name@version` set plus the one-line consequence — third-party code, on adversarial input, with open network egress. Declining is deliberately *not* a panel action: it stays an edit to the reviewed `triggers.json`, which neither the console nor a model-callable tool will make for you.
54
- - **You can see which image each trigger runs.** A trigger may name its own container image with `run.image` (absent = the deployment default), and the trigger list shows the tag while the drill-in states it either way — because which image a job runs *is* which code it runs. Like `packages`, it is display-only: naming an image stays an edit to the reviewed `triggers.json`, which neither the console nor a model-callable tool will make for you.
55
- - **Quiet hours.** Scheduled pause windows per folder or repo: defer a scope's runs **between certain times** — recurring daily, weekday- and date-bounded, timezone-aware — and resume automatically. Deferred, never dropped, at zero budget cost.
56
- - **AI-operable, with a human gate.** Model-callable tools let an agent change limits and manage triggers and pause windows — but **every write pops an operator confirmation the model can't answer**, and is refused when no operator is present. The bundled `operate-pi-dispatch` **skill** teaches the agent to use those gates.
67
+
68
+ - **Triggers, editable live.** cron, label, comment and pull_request triggers with colored drill-ins showing what fires each one, what it runs, and its trust model. Added, edited and deleted without a restart. Triggers that run third-party code or a custom image are badged; opting in or out of either stays an edit to the reviewed `triggers.json`, which neither the console nor a model-callable tool will make for you.
69
+ - **Quiet hours.** Scheduled pause windows per folder or repo: defer runs between certain times, timezone-aware, and resume automatically. Deferred, never dropped, at zero budget cost.
70
+ - **AI-operable, with a human gate.** Model-callable tools let an agent change limits and manage triggers and pause windows, and every **config** write pops an operator confirmation the model cannot answer, refusing outright when no operator is present. Two tools sit outside that gate on purpose: `dispatch_pause` and `dispatch_resume` write durable queue state but are reversible and spend nothing, so they carry no confirm. One more sits outside it and is **not** money-safe: `dispatch_run` enqueues a **paid** run that edits a local folder in place with no undo, bounded instead by six independent limits (the `PI_DISPATCH_RUN_ROOTS` folder allowlist, a committed per-flow `ai-trigger: allow` opt-in read at a pre-agent SHA, a dirty-tree refusal with no force option, no spend knobs on the tool, a per-hour rate limit, and the worker's daily cap). Read [`SECURITY.md`](https://github.com/edgehero/pi-dispatch/blob/main/SECURITY.md) on that one before you enable it. The bundled `operate-pi-dispatch` skill teaches the agent those gates.
57
71
  - **Logs stay put.** Raw container output renders only in the overlay viewer, never into model context.
58
72
 
59
- ## Install the console
73
+ ## Install
60
74
 
61
75
  ```bash
62
- pi install npm:@edgehero/pi-dispatch-admin
76
+ pi install npm:@edgehero/pi-dispatch-admin # then, in pi: /dispatch
63
77
  ```
64
78
 
65
- Then run pi and open `/dispatch`. Point it at your deployment with `VALKEY_URL` / `PI_LOGS_DIR` / `PI_SETTINGS_FILE` / `PI_TRIGGERS_FILE` / `PI_PAUSE_WINDOWS_FILE` / `PI_SUBSCRIPTIONS_FILE` / `PI_GLOBAL_PI_DIR` — the same values your worker uses. It reads that deployment's queue and run history; on its own it will just say "queue unreachable."
66
-
67
- ## Get the whole thing
79
+ **This is the default way to set up pi-dispatch.** With nothing configured, `/dispatch` takes you straight into guided setup, in this order: an opening choice, a deployment folder, a Docker check with per-OS pointers if it is missing, a consented npm install of the pinned runtime (Docker is checked **first** on purpose, so no bandwidth is spent on a host where `up` cannot work anyway), `pi-dispatch up` running its own prompts in your terminal, the deployment pointer that lets `/dispatch` find this deployment from any directory, a notice naming the file your provider key belongs in, an optional worker service, optional GitHub App credentials (`setup github`, the one step that mints a private key), an optional trigger edge (receiver service, docker compose profile, or the polling command), and an optional first **cron** trigger for the repo you are sitting in. Every step shows what it will do, asks first, and can be declined; nothing is written into your repo and no credential passes through a dialog. A deployment whose queue is merely down keeps the unreachable banner instead: setup appears when there is nothing, never over an outage.
68
80
 
69
- The service this console drives is a sibling install away — no clone needed:
81
+ Already have a deployment? The panel finds it through the deployment pointer setup writes, or through the same env vars your worker uses (`VALKEY_URL`, `PI_LOGS_DIR`, `PI_SETTINGS_FILE`, `PI_TRIGGERS_FILE`, `PI_PAUSE_WINDOWS_FILE`, `PI_SUBSCRIPTIONS_FILE`). Your env always wins.
70
82
 
71
- ```bash
72
- mkdir my-dispatch && cd my-dispatch
73
- npx @edgehero/pi-dispatch up # consented setup pass: job image, Valkey, config scaffold, preflight
74
- npx @edgehero/pi-dispatch worker # drain the queue (its own terminal, or `service install`)
75
- ```
83
+ `dispatch_run` is inert until you set one more variable yourself: `PI_DISPATCH_RUN_ROOTS` defaults to empty, and an empty allowlist refuses every folder. The deployment pointer deliberately cannot set it (the pointer carries paths, never capability grants), so widening that allowlist is always your own env edit.
76
84
 
77
- (`@edgehero/pi-dispatch` is the worker + CLI; `@edgehero/pi-dispatch-receiver` is the webhook edge for
78
- forge triggers.) The container image, docs, specs, and SECURITY.md live in the main repo. **Start there:**
85
+ ## Get the whole thing
79
86
 
80
87
  ### → **https://github.com/edgehero/pi-dispatch**
81
88
 
82
- MIT, self-hosted. Read [`SECURITY.md`](https://github.com/edgehero/pi-dispatch/blob/main/SECURITY.md) before you rely on it — it states plainly what is and isn't defended (short version: the trust model is a GitHub Action's — whoever can merge to your default branch can instruct the agent).
89
+ MIT, self-hosted. Read [`SECURITY.md`](https://github.com/edgehero/pi-dispatch/blob/main/SECURITY.md) before you rely on it; it states plainly what is and is not defended. Short version: the trust model is a GitHub Action's, so whoever can merge to your default branch can instruct the agent.