You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thirteen commits since v0.8.0: issue #60 in four PRs (whole-directory skill
materialisation, the import-pi symlink repair, `run.skillsDir`,
`run.instructions`), issue #66 (`pull_request_review` ingest), issue #102
(import-pi discovers the host's own pi packages), #103, #104 and #105.
THE RECEIVER'S WORKER RANGE MOVES WITH THE WORKER, and it is the reason this is
not a one-line version bump. `receiver` declared `"@edgehero/pi-dispatch":
"^0.1.0"`, and on a 0.x version caret means `>=0.1.0 <0.2.0`. Bumping the worker
to 0.2.0 alone would have resolved an installed receiver against worker 0.1.2 --
and the receiver imports `@edgehero/pi-dispatch/triggers` and runs the SHARED
validator, whose old copy drops unknown `run.*` fields by reconstruction. So
`run.skillsDir` and `run.instructions` would have been silently absent on the
webhook path, with no error anywhere, on a release whose headline is those two
fields.
The existing test asserted only the range's SHAPE (`/^\^\d+\.\d+\.\d+$/`), which
is why this passed for as long as the minor never moved. A new anti-drift test
asserts the range actually SATISFIES the in-repo worker version, hand-rolling
caret's rule rather than taking a `semver` dev dep for six lines. Mutation-checked:
restoring `^0.1.0` turns it red while the old shape test stays green.
Two other literals had to follow, both caught by the suite rather than by reading:
the wizard's RUNTIME_VERSION/RECEIVER_VERSION pins, and two spelled-out
`@0.1.1` tokens in the wizard tests that exist precisely to be belt-and-braces
against the constant.
UPGRADE ACTION, and it is the one that fails silently: REBUILD OR REPULL THE JOB
IMAGE. The runner changed (the loader's third skill path, PROTECTED_SKILL_ROOTS,
run-job's protected roots). A 0.2.0 worker paired with an older job image copies
a trigger's skills to /job/trigger-skills and the old runner never looks there,
so the flow runs without the skills it was written against and exits 0. No
`service install --force` is needed this cycle: deploy/ is unchanged apart from
an example file.
Lockfile edited by hand, six lines. `npm install --package-lock-only` under this
npm rewrites unrelated normalization (`./dist/cli.js` -> `dist/cli.js`, dropped
`libc` arrays), which does not belong in a release commit.
Signed-off-by: Rob Boerman <robboerman@live.nl>
Copy file name to clipboardExpand all lines: SECURITY.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,6 +54,7 @@ Jobs are a **trigger × target** matrix, and the triggers do not share a threat
54
54
| A serviced repo's `.pi/` on the **default branch**|**Maintainer-level**| It is read into the system prompt, from a pinned SHA, on purpose — same trust as `.github/workflows/`. Only someone who can merge can change it. |
55
55
| A serviced repo's contents on **any other branch**|**None**| A fork PR can contain anything. Never read for instructions. |
56
56
| A local folder's `.pi/`|**Whatever can write that folder**| No merge gate, no reviewer, no history |
57
+
| A trigger's `run.skillsDir` and `run.instructions`|**Operator — the same trust as the triggers file**| Both are instructions, and both come from the reviewed `triggers.json` on the worker host rather than from any payload. Nothing reachable from a webhook, an issue or comment body, or `dispatch_run` can set either, and no panel key or AI tool writes them. The skills are copied per job into `/job` (adding no mount) and are layered UNDER the repo's own `.pi/`, so a serviced repo still wins a name collision; the instruction text lands in the user prompt above the issue text and never in the system prompt |
57
58
| The job image (`PI_JOB_IMAGE`, or a trigger's `run.image`) |**Operator — the same trust as baking it**| It *is* the code every job executes: the pi version, the runner and its exit codes, the guardrail floor, the loader's discovery posture and the non-root user all come from it. Nothing here verifies an image this project did not build. The isolation flags are applied by the worker's argv and hold for **any** image; the **contents** do not. |
58
59
| The job container |**None** — it is the untrusted side | It runs the agent |
59
60
| A job container's `/outbox` request file |**None** — agent-authored | An agent-initiated signal channel back to the host; validated host-side before anything is enqueued. **Local jobs only** — a github job has no `/outbox` mount at all |
Copy file name to clipboardExpand all lines: admin/package.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
{
2
2
"name": "@edgehero/pi-dispatch-admin",
3
-
"version": "0.5.0",
3
+
"version": "0.6.0",
4
4
"description": "Operator console (a pi extension) + skill for pi-dispatch: run the pi coding agent as a self-hosted service. A /dispatch TUI for the queue, spend caps, run history, editable GitHub, GitLab, Forgejo and Azure DevOps triggers (cron/label/comment/PR/MR/work item), and scheduled pause windows — plus AI-operable, human-confirmed controls. Runs against a live pi-dispatch deployment.",
Copy file name to clipboardExpand all lines: receiver/package.json
+2-2Lines changed: 2 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
{
2
2
"name": "@edgehero/pi-dispatch-receiver",
3
-
"version": "0.1.1",
3
+
"version": "0.2.0",
4
4
"type": "module",
5
5
"description": "Webhook receiver for pi-dispatch: the always-on edge that verifies GitHub, GitLab, Forgejo and Azure DevOps deliveries and enqueues (at most) one job per event for the worker.",
// Overlay extensions load unless the operator opted OUT (PI_GLOBAL_ALLOW_EXTENSIONS=0) AND the dir
60
63
// is present. Operator-staged pi packages (REQ-GLOBAL-PI-OVERLAY) come LAST and do NOT ride that
61
64
// option — the spread is unconditional, because the worker already applied the per-trigger
@@ -2220,6 +2223,7 @@ recorded repair is re-running `/dispatch setup` (or editing the pointer by hand)
2220
2223
2221
2224
| Date | Change |
2222
2225
|---|---|
2226
+
| 2026-08-09 | Follow-up audit after issue #60. **INT-SDK-SESSION-OPTIONS AMENDED**, a correction rather than an addition: the option block still showed the TWO-path `additionalSkillPaths` literal, while the prose beside it had already been updated to three protected roots. A contract block that disagrees with its own note is worse than either being wrong alone, since a reader checking the code against the spec would have found the spec confirming the old shape. Now shows repo, injected, overlay. No behaviour changed; the literal had been stale since the injected tier landed hours earlier. |
2223
2227
| 2026-08-09 | Issue #60 (Gap 3: `run.instructions`). **INT-TRIGGERS-FILE-CONTRACT AMENDED**: a new optional field on the three webhook types, refused on cron with a message naming `run.task`, capped at 2000 characters and refused rather than truncated. Surrounding whitespace is deliberately NOT refused here, unlike `run.image`, and the divergence is recorded: that rule exists because whitespace changes what an image REFERENCE means, and it does not change what prose means. **INT-CONTAINER-JOB-INPUTS AMENDED**: `prompt.md` may now carry an operator standing-instruction block in the envelope above the data region; it reaches no other file. **INT-WEBHOOK-PAYLOAD-SUBSET UNCHANGED, checked**: the field is operator config and is not a webhook body field, so the subset is untouched and the value never appears in `event.json`. |
2224
2228
| 2026-08-09 | Issue #60 (Gap 2: `run.skillsDir`, a per-trigger operator skills directory). **INT-TRIGGERS-FILE-CONTRACT AMENDED**: a new optional field on all four run kinds, with the validation SPLIT written out because both halves are load-bearing. Existence is not checked in the shared validator because BOTH services parse this file and the receiver may run on another host (the `run.folder` precedent); absoluteness is not checked there either, and that one is subtler, because `path.isAbsolute` is OS-dependent, so a shared check would let a Windows worker and a Linux receiver disagree about the same reviewed file. The worker enforces both where the answer is knowable: at boot for cron, and pre-spend per job for every kind. Also records that the value never reaches `/job/event.json` (it rides at JOB level, never inside `trigger`, which is what the subset is built from), and that injected skills are trigger-reachable and never AI-reachable. **INT-CONTAINER-JOB-INPUTS AMENDED**: `/job/trigger-skills/<name>/**` joins the layout as the one `/job` input that does NOT come from git, with the asymmetry argued rather than left to be noticed -- `.pi/` is read by oid because the serviced repo is only maintainer-trusted and an attacker can shape that tree, while `run.skillsDir` is operator-authored deploy-time config named in a reviewed file, so what remains is the ordinary filesystem hazard and the copier answers it the same way (lstat never stat, regular files only, destinations rebuilt from validated segments, bounded). It arrives on the EXISTING `/job:ro` bind. **INT-RUN-HISTORY-FILE-CONTRACT AMENDED**: six `skills-dir-*` reasons. **INT-SDK-SESSION-OPTIONS AMENDED**: the protected-root list goes to three, `/job/pi/skills` then `/job/trigger-skills` then `/opt/pi-global/skills`, consulted in that order. **INT-CONTAINER-RUNTIME-CONTRACT UNCHANGED, checked** -- and this is the entry the change was designed around: no mount is added, no flag, no env var, so a job with an injected skills dir has a docker argv byte-identical to one without, pinned by a test. |
2225
2229
| 2026-08-08 | Issue #60 (Gap 1: a repo skill's supporting files were silently dropped). **INT-CONTAINER-JOB-INPUTS AMENDED**: the materialiser's allowlist accepted exactly `.pi/APPEND_SYSTEM.md` and `.pi/skills/<name>/SKILL.md`, so a skill shipping `references/`, `scripts/` or templates had those files dropped by a bare `continue` with no error anywhere. That is worse than it sounds, and the reason is upstream: at the 0.80.7 pin `core/skills.js` instructs the model to "resolve it against the skill directory", so the skill loaded, read correctly, and pointed the agent at files that were not in the container. A confidently wrong agent, not a failure. The entry now documents whole-directory materialisation, the split-and-validate-every-segment grammar that replaced the single regex (STRONGER than the fixed template it replaces, because any separator other than `/` survives inside a piece and is refused by the anchored charset, and `..` is refused by the leading-alphanumeric rule), the two-tier charset (the skill NAME keeps the lowercase-only `SKILL_NAME_RE` it shares with the flow gate; only the segments below it are case-insensitive, because `SKILL.md` and `README.md` are the point), the Windows device-name refusal, the documented skips, and the six caps with the refuse-before-write ordering. Three sub-decisions are recorded rather than left implicit. **A subtree declaring no `SKILL.md` anywhere beneath it is not materialised**, because pi registers a skill only where a literal `SKILL.md` exists, so those bytes could never be referenced and copying them would be a data-dump channel that never has to look like a skill; the test is "anywhere beneath" and not "at the root" because pi keeps recursing while a directory has no `SKILL.md`, and a root-only rule would have recreated this very defect one level down. **`100755` stays rejected**, so a skill's scripts are invoked as `bash script.sh`: `/job` is `:ro` and files land `0444`, so accepting the mode and writing `0444` anyway would accept what the repo asked for and silently strip it, while `0555` would have the worker grant execve on repo bytes. **A cap breach REFUSES the job** rather than truncating, because a truncated skill IS this defect and which files survived would be decided by git's tree order. The `/job/pi/extensions` bullet's PREMISE was rewritten (it asserted the materialiser "only ever emits ... SKILL.md", now false) while its CONCLUSION is untouched: `extensions/` is still never written, so discovery remains the only path a repo extension has ever had. Acceptance gains the symlink-inside-a-skill-subdirectory case and the cap cases. **INT-RUN-HISTORY-FILE-CONTRACT AMENDED**: four terminal reasons (`pi-too-many-files`, `pi-file-too-large`, `pi-too-large`, `pi-path-collision`), plus `sha-gone`, which the enum had always omitted. The `pi-` prefix is load-bearing rather than decorative: the nested `session.reason` enum already carries a bare `too-large`, and two enums in one record sharing a token is how a reader misattributes a refusal. **INT-SDK-SESSION-OPTIONS UNCHANGED, checked**, and the check is the interesting one: it has always written the layout as `.pi/skills/**/SKILL.md`, one level deeper than the code implemented, so the spec was right and the code has now caught up to it rather than the other way round. **INT-CONTAINER-RUNTIME-CONTRACT UNCHANGED, checked** — no mount, no flag, no env var; the widened content rides the `/job:ro` bind that already existed. Repaired in passing, because this change made it unavoidable: `makePrepareWorkspace` leaked its `mkdtemp`'d job dir on EVERY policy refusal, since a refusal carries no `jobDir` and both teardown paths guard on `prepared?.jobDir` — true of `sha-gone` since it shipped, and about to be hit on every delivery by a repo that breaches a cap. |
0 commit comments