Commit graph

154 commits

Author SHA1 Message Date
6f92b9eaa6 test: drive the retired-default label recogniser (#144)
Some checks failed
ci / check (pull_request) Successful in 55s
ci / install (pull_request) Successful in 3s
ci / db-integration (pull_request) Successful in 3s
labels / labels (pull_request) Failing after 7s
Extract labels_are_a_retired_default and assert the four cases a grep pin
cannot: pre-#144 matches, current default does not, custom --labels does
not, near-miss does not. Addresses the remaining REQUEST_CHANGES on !146.

Refs #144
2026-08-01 21:38:26 +00:00
f0f17ad2ff fix: warn only on retired default labels, not custom maps (#144)
Some checks failed
ci / check (pull_request) Successful in 54s
ci / install (pull_request) Successful in 3s
ci / db-integration (pull_request) Successful in 3s
labels / labels (pull_request) Failing after 6s
Plain converge comparing RECORDED to the current default nags every runner
registered with intentional --labels (including drill Leg 3). Match known
past DEFAULT_LABELS strings instead — same intent, no noise. Changelog
split per review.

Refs #144
2026-08-01 21:33:11 +00:00
23965bebca test: forgejo-runner version pins work when CI runs as root (#144)
Some checks failed
ci / check (pull_request) Successful in 55s
ci / install (pull_request) Successful in 3s
ci / db-integration (pull_request) Successful in 3s
labels / labels (pull_request) Failing after 6s
act-22.04 jobs are uid 0, so "must run as root" is never the next gate after
--version validation. Accept the unattended-token refuse when already root;
keep the non-root arm for GitHub-hosted runners.

Refs #144
2026-08-01 21:18:33 +00:00
ad3133d1c0 fix: slim ubuntu-latest default; install shellcheck in ci.yml (#144)
Some checks failed
ci / check (pull_request) Failing after 52s
ci / install (pull_request) Successful in 3s
ci / db-integration (pull_request) Successful in 3s
labels / labels (pull_request) Failing after 6s
Option B (andres ruling): keep act-22.04 for ubuntu-latest so box-class
ci tenants can hold the image; workflows supply tools the slim image
lacks. Opt-in ubuntu-latest-full for operators who need GH parity.
Plain converge warns when recorded labels lag the current default map.

Refs #144
2026-08-01 21:13:46 +00:00
9cb81c9f6b Merge pull request 'test: cli.sh reads fixtures, not the host it happens to run on' (#137) from build/136-cli-hermetic into main
Some checks failed
ci / check (push) Failing after 7s
ci / install (push) Successful in 4s
ci / db-integration (push) Successful in 4s
release / release (push) Successful in 7s
Reviewed-on: #137
Reviewed-by: grok-reviewer-andresmgsl <andres+3@heavyduty.builders>
Reviewed-by: kimi-reviewer-andresmgsl <andres+4@heavyduty.builders>
Reviewed-by: codex-reviewer-andresmgsl <andres+2@heavyduty.builders>
2026-07-31 21:24:04 +00:00
86b749876a Merge pull request 'feat(drill): a forgejo-runner lifecycle leg beside the GitHub one' (#130) from build/129-forgejo-runner-drill-leg into main
Some checks are pending
ci / check (push) Waiting to run
ci / install (push) Waiting to run
ci / db-integration (push) Waiting to run
release / release (push) Waiting to run
Reviewed-on: #130
2026-07-31 20:56:18 +00:00
306844daa8 Merge pull request 'fix(forgejo-runner): 'active' is not proof the runner is fetching' (#134) from build/133-status-active-is-not-health into main
Some checks are pending
ci / check (push) Waiting to run
ci / install (push) Waiting to run
ci / db-integration (push) Waiting to run
release / release (push) Waiting to run
Reviewed-on: #134
Reviewed-by: kimi-reviewer-andresmgsl <andres+4@heavyduty.builders>
Reviewed-by: grok-reviewer-andresmgsl <andres+3@heavyduty.builders>
2026-07-31 20:55:18 +00:00
7385707770 test: the third hermetic pin can actually fail
Some checks failed
ci / check (pull_request) Failing after 7s
ci / install (pull_request) Successful in 4s
ci / db-integration (pull_request) Successful in 4s
labels / labels (pull_request) Failing after 7s
kimi was right that inline_undo_is_sealed matched itself: it grepped the whole
file for a string that appears inside its own body, so it succeeded on every
tree forever. Their suggested anchoring had the identical flaw for the identical
reason — any literal this function searches for necessarily appears in this
function — which I only found by running their probe and watching the pin stay
green while the behavioural test went red.

So it locates the real check by line number and reads only that block. head -1
takes the site at :202, never this body at :3311.

Driven, since a pin nobody has watched fail is not a pin: deleting the :205
wiring now fails exactly this check, and deleting all three seals fails all
three. That is the third vacuous guard I have written on this branch family;
the comment says why the obvious shapes do not work, so the next person does
not rediscover it a fourth time.

Refs #136

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 20:32:08 +00:00
d46697034d test: pin the hermeticity, not just achieve it
Some checks failed
ci / check (pull_request) Failing after 7s
ci / install (pull_request) Successful in 4s
ci / db-integration (pull_request) Successful in 3s
labels / labels (pull_request) Failing after 8s
codex and kimi both drove the same experiment: delete the two wiring lines this
branch adds and the suite stays 786/786 on any host without a real Forgejo
runner. Hermetic today, unpinned tomorrow — and #136's task list names the
guard verbatim: "a check that fails if either group can see host state".

Three checks assert the suite's own helpers keep their seals: undo() passes
RIG_FORGEJO_RUNNER_DIR, cibox_run() passes CIBOX_BIN, and the hand-rolled undo
invocation behind "failed logout is loud" passes it too — that third one being
the site I missed first time round, which is exactly why it earns a check
rather than a comment.

They assert on the test side deliberately: the production knobs are already
covered, and the regression worth catching is a deletion in the suite.

Verified by re-running codex's experiment: with both wiring lines gone, the two
seal checks fail where previously nothing did.

Refs #136

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 20:15:35 +00:00
848e3f42ef test(forgejo-runner): drive the liveness note's state boundary
Some checks failed
ci / check (pull_request) Failing after 7s
ci / install (pull_request) Successful in 4s
ci / db-integration (pull_request) Successful in 4s
labels / labels (pull_request) Failing after 7s
codex and kimi are right: the four checks proved the LINES EXIST and nothing
proved they fire only when the unit is active. kimi deleted the state guard and
the suite stayed 790/790 green, so the boundary #133 cares about most — no
misleading liveness note on an absent or inactive unit — was unprotected. #133's
own test plan says driven, not grepped where behaviour can be executed, and this
was the line it crossed.

The note is now a function, which is what makes the boundary executable, and
the suite drives it on active (note present, remedy named), inactive (empty)
and unset (empty). Removing the guard now fails two checks instead of none.

The no-warn and no-network guards stay greps: those properties are source-level
by nature, as kimi noted.

Two things I got wrong on the way, both caught by running it rather than
reading it: the function was defined below its call site (shellcheck SC2218),
and the block reused $WORK, which is rm -rf'd at :3206 long before it — so it
now takes its own scratch dir like the file's other fixtures.

Refs #133

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 20:13:23 +00:00
9afcdf858f fix(drill): the leg row is the whole lifecycle, not just the job
Some checks failed
ci / check (pull_request) Failing after 7s
ci / install (pull_request) Successful in 4s
ci / db-integration (pull_request) Successful in 4s
labels / labels (pull_request) Failing after 7s
codex, grok and kimi all landed on the same hole at e42ba31, and they are
right. The record row keyed only on fj_took, so install/status/remove failures
called `no` without gating it. A dispatched job only needs SOMETHING answering
runs-on: drill — and this leg removes locally, telling the operator to delete
the stale runner by hand, so a leftover drill-labeled runner from the previous
drill is the designed-for aftermath, not a contrived case. It answers the
fixture exactly, and the row then read "PASS — registered, took a job,
removed" for a drill whose install never happened.

The exit code was never wrong: every one of those failures also called `no`.
What was wrong is the row, and the row is what outlives the run in
drills/<v>.md. A row claiming a lifecycle that did not happen is exactly what
the gate exists to refuse — kimi's framing, and it is the right one.

forgejo_leg_row folds all five assertions, and the leg no longer dispatches
after install or status has failed: a foreign runner's success could only ever
be evidence about someone else's runner. The regression composes the real
functions over a payload carrying a newer successful run, the way the leg
composes them.

The GitHub leg has the identical shape and is byte-frozen by #129's contract;
left alone deliberately, and worth a sibling issue for triage.

Refs #129

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 20:04:16 +00:00
a45d84892f fix(forgejo-runner): the cache server can start
Some checks failed
ci / check (pull_request) Failing after 7s
ci / install (pull_request) Successful in 4s
ci / db-integration (pull_request) Successful in 4s
labels / labels (pull_request) Failing after 7s
ProtectHome=read-only made the whole home read-only and only RUNNER_DIR was
punched back through, so forgejo-runner could not create $HOME/.cache and
disabled its cache server on every install. actions/cache was silently off on
every rig-installed Forgejo runner, evidenced by one error line in the journal
while `status` reported a healthy runner.

Both halves are required, and the obvious one-line version is WORSE than the
bug: a ReadWritePaths entry naming a path that does not exist makes systemd
refuse to start the unit at all — "Failed to set up mount namespacing", three
restart attempts, service down. Measured on a live runner before writing this.

So the directory is created at install, owned by the runner user like
RUNNER_DIR beside it, and the unit lists it. ProtectHome stays read-only: the
runner supervises job containers on this box's docker socket, and the cache is
not a reason to widen that.

Verified live from scratch: directory removed, unit removed, converge, then
zero cache-server errors and the two cache listeners bound.

Closes #135

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:59:03 +00:00
fd5c04efc9 test: cli.sh reads fixtures, not the host it happens to run on
Some checks failed
ci / check (pull_request) Failing after 7s
ci / install (pull_request) Successful in 4s
ci / db-integration (pull_request) Successful in 4s
labels / labels (pull_request) Failing after 7s
13 checks failed on any box with a Forgejo runner installed — which is
exactly a box that has been drilled or used as a ci-box. Unmodified main,
773/13 on this machine.

Seven bootstrap --undo checks fell through to the real host scan. The
production code already ships the escape hatch and documents it —
bootstrap-undo.sh:33, "RIG_FORGEJO_RUNNER_DIR mirrors RIG_RUNNER_DIR above so
tests can point this at a fixture" — and the suite simply never set it. One
check rebuilt its env by hand instead of using undo(), so it needed the same
variable a second time.

Six ci-box checks drove the real template installer, which correctly exits 0
when /usr/local/bin/forgejo-runner already exists. CIBOX_BIN is a test-only
override in the same spirit; the production default is untouched and remains
the only path the mechanism uses.

Neither guard is disarmed: driven for real, undo still refuses while a runner
exists, and the template installer still does nothing when the binary is
present. Removing either override brings its failures straight back (7 and 6).

Closes #136

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:51:45 +00:00
e42ba31a09 fix(drill): the pre-dispatch baseline folds max too, and the suite is hermetic
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Round 2 of !130. Both blockers were mine, and one was the same mistake I had
just fixed one layer up.

1. fj_pre still took `head -n1`. The verdict folds max over every id, but the
   PRE-DISPATCH snapshot did not, so an oldest-first payload named an old run
   as the baseline — and a later poll finding the same body then read the
   PREVIOUS drill's run as this dispatch's result. That is a false PASS on the
   take-a-job assertion, strictly worse than the false FAIL entry[0] caused
   inside the verdict. Both sides now share forgejo_max_task_id, and a test
   composes them the way the leg does so the pair cannot drift apart again.

2. test/drill.sh copied its pretty-printed fixture from /tmp/fjfix — a scratch
   path that existed only on the box the fix was built on. Everywhere else the
   cp failed, the guard returned pending, and the suite was 65/66. The claimed
   66/66 was true on one machine. The fixture is written inline like every
   other one; verified by deleting the scratch dir and running the suite from
   a clean tree under env -i.

Refs #129

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:29:06 +00:00
1daf7004d7 fix(forgejo-runner): 'active' is not proof the runner is fetching
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
status reports `systemctl is-active` as the service line, and that is the
strongest health signal it has — the command reads only on-disk config, by
contract. But a poller can go quiet while the process stays up: measured
2026-07-30 while probing for #129, a daemon logged "[poller] launched" and
never fetched a job dispatched four minutes later, while a daemon started
fresh claimed that same queued task in one second. Both times it read as a
label-mapping bug on the forge, which is the wrong place to look.

Says so where an operator already looks when nothing is obviously wrong, and
names the remedy. log, not warn: an idle-but-healthy runner is silent in
exactly the same way a stalled one is, so there is no signal separating them
and a warning on every run would be crying wolf.

No network call and no token read — the header contract, and test/cli.sh's
existing guard, both stand.

Refs #133

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:15:02 +00:00
9c0e508b76 fix(drill): read every task, and fail a configured leg that cannot mint
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Addresses grok's and kimi's REQUEST_CHANGES on !130.

1. forgejo_run_verdict read only the FIRST entry of actions/tasks. That
   payload accumulates, so the moment a repo is drilled twice our run shares
   it with older ones — and nothing documents the sort order. A green job
   then reports as a timeout: a false FAILURE on the gate this leg exists to
   provide. It now inspects every entry and lets the newest id above pre_id
   decide. Newlines are stripped first, so a pretty-printed payload parses
   like a compact one.

2. A mint that yielded nothing degraded to SKIPPED "no registration token
   source" — violating #129's own acceptance ("token source present but the
   instance is unreachable -> the leg FAILS; it must not skip and must not
   pass") and sending the operator to check an env var they had already set.
   forgejo_token_verdict separates absent inputs from a configured leg that
   could not mint; only the former skips.

3. The pre---yes confirm block still announced a GitHub runner alone.

Refs #129

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:13:59 +00:00
1179d3142f feat(drill): a forgejo-runner lifecycle leg beside the GitHub one
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
#109 shipped `rig forgejo-runner` and the drill never touched it, so the
release gate could pass with the whole family broken. Its only coverage was
15 argument-handling checks in test/cli.sh.

The Forgejo half cannot be a copy of the GitHub one. Measured against
forgejo.heavyduty.builders (8.0.3+gitea-1.22.0), not read from the docs:
GET actions/tasks carries NO `conclusion` field — `status` holds the terminal
outcome — `id` is a global task id while the URL ends in run_number, and the
payload lists ASSIGNED tasks only, so a queued run is invisible (measured:
200s of total_count 0 while the web UI showed the run). That makes "no new
id" the only signal the runner never took the job, which is the verdict this
leg exists to produce.

forgejo_run_verdict() is the testable core of that, grep-and-sed rather than
jq for the same reason json_field() is. Tests were written first and cover
the cases that must NOT pass: a stale run with the pre-dispatch id, an
assigned-but-still-running task, and a cancelled run.

Refs #129

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:27:36 +00:00
7aca642ba5 fix: preserve triage across both forges
Some checks failed
labels / labels (pull_request) Has been cancelled
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
2026-07-30 18:31:16 +00:00
0362e5cf43 fix: align Ceremony actors with Forgejo
Some checks failed
labels / labels (pull_request) Has been cancelled
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
2026-07-30 17:49:39 +00:00
a3ec47113a fix: emit BOX_MANUAL line-by-line; bare command for single candidate
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
claude REQUEST_CHANGES on 1c9a245: the all-candidates-fail path still
interpolated multi-line BOX_MANUAL into one warn, orphaning the or: line
and leaving try: non-pasteable on the default host.

- box_manual_emit log|warn prints one recovery line at a time
- single-candidate hosts (GitHub) get a bare pasteable command (no try:)
- multi-candidate keeps try:/or: prefixes
- never ${BOX_MANUAL} inside a log/warn string
- cli.sh asserts the emission invariant
2026-07-29 22:38:32 +00:00
1c9a245595 fix: BOX_MANUAL is pasteable try:/or: lines, not prose shell
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
codex REQUEST_CHANGES on !114 / #125: multi-candidate recovery was
rendered as `curl A | bash; if that 404s: curl B | bash`, which
`bash -n` rejects (exit 2). Operators on RIG_SKIP_BOX_INSTALL / no-curl
/ all-candidates-failed paths were handed non-executable recovery.

- box_manual_cmd / box_manual_text: one pasteable command per candidate
- display as separate `try:` / `or:` lines (newlines, no prose join)
- post-probe rewrite still collapses to the URL that worked
- execution tests: both Forgejo URLs visible; each command passes bash -n
2026-07-29 15:22:49 +00:00
ec73c86de7 fix: shellcheck SC2129, BOX_MANUAL lists candidates, drive box_install_urls
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Claude re-review on c7d56d3:
- Group the installer-fn extracts in test/release.sh (SC2129).
- BOX_MANUAL on multi-candidate hosts lists every URL so BOX_REF=main
  never hands the operator only a 404ing /raw/tag/ form.
- Execute box_install_urls under the harness (tag-before-branch order).
- Cover Forgejo's no-release 404 path in resolve_latest_tag.
2026-07-29 14:53:07 +00:00
1343cb9871 fix: release channel is tag-only; revert coolify Documentation=
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Claude REQUEST_CHANGES on !114: the latest-release path must never fall
through to refs/heads/<tag> (would install a branch while INSTALLED_FROM
still names the release). release_tag_url is the single refs/tags URL on
every forge; regression test drives the heads-only stub.

Also restore coolify Documentation= to github.com — forge flip needs an
@andres ruling, not a shipped default in this PR.
2026-07-29 14:45:40 +00:00
df075b9ecf fix: review round — pin-wins candidates, no BOX_RAW_KIND guess
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Address codex REQUEST_CHANGES + claude's live Forgejo measurements:

- ref_candidate_urls is host-only: same refs/tags then refs/heads on every
  forge (Forgejo serves both paths; bare /archive/<ref> no longer special).
- bootstrap box raw fetch tries /raw/tag/ then /raw/branch/ and never
  guesses kind from spelling; download is separate from execute.
- BOX_HOST defaults through RIG_HOST; comments narrow the zero-GitHub claim
  (box@0.9.0 installer still hardcodes GitHub for its archive).
- SC2016 suppressions on the new grep assertions; tests updated.
2026-07-29 14:42:59 +00:00
24abfbdb79 feat: install channel is forge-agnostic (RIG_HOST / BOX_HOST)
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
install.sh resolves releases and archive URLs through RIG_HOST (default
GitHub), using Forgejo's single /archive/<ref>.tar.gz form and the same
/releases/latest redirect grammar. Bootstrap's box fetch gets BOX_HOST
with forge-aware raw-file URLs. Refusal hints and coolify Documentation=
stop pointing only at GitHub.

Closes #111
2026-07-29 14:33:38 +00:00
cluade-reviewer-andresmgsl
25f337494c fix: rig's ceremony CI guards resolve on Forgejo
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
The six ceremony actions in ci.yml are step `uses:` references, and a bare
step reference resolves through FORGEJO__actions__DEFAULT_ACTIONS_URL — which
this forge points at code.forgejo.org, where heavy-duty/ceremony does not
exist. Measured with a registered runner on the instance: `git clone
https://code.forgejo.org/heavy-duty/ceremony` -> 404, so every guard fails the
moment rig's own CI runs on the forge. Those six become absolute.

The two reusable-workflow callers do NOT move, and this is the correction to
the original "make all eight absolute" ruling. A reusable `uses:` never
consults DEFAULT_ACTIONS_URL; it resolves against the runner's own instance,
so both callers already fetch ceremony from this forge and expand. Absolutising
release.yml's would actively break CI: ceremony's docs-sync reads rig's pin out
of that one line with a grep anchored to the bare spelling, so the prefix makes
the pin invisible and docs-sync exits 1 with "no pin line ... found none".

actions/checkout@v4 stays bare in all three places — the mirror serves it, and
that is exactly what DEFAULT_ACTIONS_URL is for. The pin stays @0.3.0
throughout; no .ceremony/ edit, no pin bump.

test/cli.sh pins all three shapes, including the docs-sync grep verbatim, so
ceremony's parser and rig's caller cannot drift apart silently.

Refs #112

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:10:38 +00:00
fbdce5284e fix: the ci-box token guidance says what a Forgejo token actually is
Round finding from @codex-reviewer-andresmgsl, elevated to blocking by
@grok-reviewer-andresmgsl and @kimi-reviewer-andresmgsl. Unanimous, and right.

creds.md called the registration token "short-lived" and said it was "consumed
at registration". Both are GitHub's facts, copied across the forge boundary
with the rest of the sibling's shape. Forgejo's primary source, read rather
than inferred:

  models/actions/runner_token.go — ActionRunnerToken has NO expiry field. Only
  IsActive, Created, Updated. NewRunnerToken flips IsActive false on prior
  tokens at the same scope and only there, so a token dies when somebody mints
  its replacement, never on a clock.

  routers/api/actions/runner/runner.go — Register reads the token, refuses it
  when !IsActive ("please use the latest one"), and returns WITHOUT setting
  IsActive = false. Registration does not spend it. One token registers as many
  runners as it is shown to.

So it is long-lived and reusable — the precise opposite of the adjective, and
GitHub's really does expire in about an hour, which is why runner-install.sh is
correct to use it.

This is not a wording nit because of where the wording lives. creds.md is
spliced into the ci-box's own CONTEXT.md: it is the paragraph an agent INSIDE
the box reads about its own credentials. Telling that reader the token
self-expires is telling it a leaked one stops mattering on its own, while it is
still registering runners.

Pinned, not merely fixed, per codex's ask — the phrase arrived by copying from
the GitHub sibling, so the same copy can bring it back. Four rows: absence from
both files, and presence of the true claim, so the pin cannot be satisfied by
deleting the sentence instead of correcting it. The first draft of the CIBOX
pin was a phrase match and passed against the exact text it was written to
catch — the old wording wrapped across two comment lines. It is a plain absence
check now, and the file explains the ban without spelling the word.

Mutation-checked: all four go red against the old wording, green after.
2026-07-28 16:18:33 +00:00
ab29ee78fb fix: the --version read cannot die ahead of its own refusal
Self-review of the whole head after the panel converged. One real defect,
reproduced verbatim before it was touched.

runner_version_of is a pipeline, and every forgejo-runner-* command runs under
set -euo pipefail. When the binary exits non-zero the pipeline fails, and
`PRESENT_VER="$(runner_version_of "$BIN")"` is an ASSIGNMENT — so install died
at that line, exit 1, printing nothing at all. The lib documented the opposite
("empty when it cannot answer"), and the refusal written for exactly this case
twenty lines later — "the download landed but cannot run" — was unreachable
code that could never have fired.

The shape is not hypothetical. `[ -x ]` yes, runnable no is what a truncated or
wrong-arch download leaves behind, and a ci-box's template preinstalls the
binary at mint — so the silent death sat on the one path this command family
exists to serve.

json_field, two files away, already carries `|| true` with this reasoning
written above it: "callers run under set -e with pipefail, where a grep that
matches nothing would otherwise kill the script with no message". The same
guard, for the same reason, at both sites: the lib reader and install's own
read of --version.

The test could not have caught it. vparse drove the lib through `bash -c` with
NO shell options, and its "bad" stub exits 0 — it proved garbage-in-empty-out
in a permissive shell, which is not where the function is used. vparse now runs
under set -euo pipefail, and a stub that exits non-zero is a third row.
Mutation-checked: that row exits 1 against the unpatched lib and 0 against the
fixed one. Install's site is grep-pinned, since reaching it for real needs root
and a downloaded binary.

748 passed (was 746), release 31, drill 43, shellcheck clean.
2026-07-28 09:26:17 +00:00
cf5858bb60 fix: one checksum policy, labeler coverage, orphaned-unit removal
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Net-new review findings from grok and kimi on !110. Their items 1-3 were
codex's, already fixed in 1933b07; these are the ones only they raised.

grok #4 — the two downloaders would drift. docs/templates/ci-box/install.sh
and the download block in forgejo-runner-install.sh were near-copies, and grok
named the exact consequence with the exact evidence: fail-open survived in BOTH
while a grep for "checksum mismatch" passed against both, because the string it
looked for sat right beside the branch it could not see. The whole policy —
fetch, unreadable, mismatch — is now fetch_and_verify_sha256, byte-identical in
both files and diffed by test/cli.sh. They cannot share a lib: the command
sources commands/lib/, and the template is a registry definition that runs
standalone inside a mint with rig's tree nowhere in reach, which is the same
situation valid_version faces between bin/rig and install.sh. Mutation-checked
by drifting one copy's message and confirming the diff goes red.

kimi #2 — the labeler could not see this family. scope:runner matched
commands/runner-*.sh only, so forgejo-runner-*.sh and the staged ci-box
definition scored no scope at all. Globs extended and the label's description
now says either forge rather than GitHub.

kimi #4 — remove stranded a unit whose user was gone. The missing-user check
exited 0 before the unit was ever looked at, so a deleted account with a
leftover forgejo-runner.service reported "nothing to remove" while the
absence-assert that never ran implied the opposite. The unit is now checked
independently. Auditing that fix surfaced a hazard kimi did not mention: with
the user gone RUNNER_DIR is "", and the later unguarded "$RUNNER_DIR/.rig-labels"
would have expanded to "/.rig-labels" — an rm at the filesystem root, as root.
Every RUNNER_DIR path is now gated, and a test pins that none is unguarded.

kimi #1 — the README handed out a config that breaks rig's own gates.
DEFAULT_ACTIONS_URL is a single fallback and rig's workflows need two origins;
measured: code.forgejo.org serves actions/checkout (200) but not
heavy-duty/ceremony (404), which lives on the Forgejo instance. With the value
the README recommended, all eight ceremony references fail to resolve. The
section now states the conflict with the counts, says which references would
break, and explicitly does NOT pick a side — that is an infra decision, and
rig's CI running on Forgejo is not something rig forgejo-runner depends on.
Asked the maintainer for direction.

746/31/43 pass, shellcheck clean, labeler.yml parses.

forgejo#109

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:19:33 +00:00
1933b07fd4 fix: honour --version, scope .rig-labels, close the checksum gate
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Three defects from review !110, all the same family — a stated contract the
code did not keep.

--version was swallowed on the path this command exists for. The download
block skipped on mere presence, copying runner-install.sh's shape without its
justification: actions/runner SELF-UPDATES, so freezing it would only make
GitHub refuse its jobs. forgejo-runner does not self-update, so nothing else
ever moves the version — and a ci-box's template preinstalls the binary at
mint, which meant the documented deterministic-pin lever could never fire on a
ci-box. It now converges toward the pin, downward included, because a pin is
an instruction and not a floor; absent a pin an existing binary is left alone,
since chasing latest would make a re-run an unrequested upgrade. The decision
moved to runner_download_decision in the lib as a pure function: the first
attempt at a test here grepped for a log string and survived the logic being
disabled, which is exactly the weak test the review warned about. The binary
is now renamed into place rather than written over — the converge path runs
while the daemon is live, and in-place is ETXTBSY.

.rig-labels outlived the registration it described. The write had escaped the
registration branch, where runner-install.sh correctly keeps its copy, so a
plain re-run stamped this invocation's labels over a registration made with
different ones and status then reported confidently wrong labels while Forgejo
still held the originals. Scoped again, and an EXPLICIT --labels on a re-run
now warns that Forgejo owns labels from registration time rather than letting
the request evaporate silently.

The checksum gate failed open. A missing .sha256 warned and installed anyway,
contradicting both the README and the template's own comment about unverified
root downloads. The original reasoning — do not let an upstream layout change
break installs — reasons about the wrong failure: a layout change breaks the
BINARY url too, so "binary yes, checksum no" is the shape of an interfered
fetch, which is precisely what the checksum exists to catch. Both paths refuse
now, with no bypass flag; if upstream really moves its assets that is a rig PR
editing the URL, not an operator improvising past a security gate.

Tests: the checksum paths are now DRIVEN against a stub curl through the real
template install.sh — matching, missing, mismatched and empty — instead of
grepped, and all three fixes were mutation-checked by reverting each and
confirming the suite goes red.

739/31/43 pass, shellcheck clean.

forgejo#109

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:08:15 +00:00
903d8371b3 feat: Forgejo-native CI — a ci-box tenant and a forgejo-runner family
Some checks failed
ci / check (pull_request) Has been cancelled
ci / install (pull_request) Has been cancelled
ci / db-integration (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
rig's CI story was GitHub-shaped end to end. This makes it work against a
self-hosted Forgejo, in three pieces.

The registry fetch becomes forge-aware. templates_resolve hardcoded three
github.com archive URLs; RIG_TEMPLATES_HOST now selects the grammar, because
the forges genuinely differ — GitHub serves refs/tags, refs/heads and bare
paths, Forgejo serves exactly one, and emitting the other two there would mean
two guaranteed 404s per fetch and a failure message listing URLs that never
could have worked. Measured against forgejo.heavyduty.builders, not inferred.
The default stays GitHub, so every existing caller is unchanged. install.sh's
snapshot reads the same variable through a byte-identical copy of the builder,
diffed by the tests: a snapshot cached from a forge converge would never fetch
from is worse than no snapshot, and the pin-in-the-name staleness guard cannot
catch a wrong-ORIGIN snapshot, only an old one.

ci-box is a tenant, not a machine role. The topology is a fleet machine
hosting boxes, one of which runs CI — a '-box' guest by rig's own family rule.
That also deletes the docker-in-docker layer the usual setup needs:
bootstrap-tenant.sh already installs Docker and adds the tenant user to the
group, and the isolation a privileged dind sidecar buys is already paid for by
a box that is network-isolated, inbound-less and disposable. rig runner
install refuses Docker for good reason — it converges a MACHINE, where the
blast radius is the machine. Here it is a guest that gets thrown away.

rig forgejo-runner is a new family beside rig runner, which is untouched.
Forgejo registers against an INSTANCE and the token carries the scope, so
there is no --repo to converge toward and nothing to compare; folding that
into one command would make every guard bimodal to share a flag name while the
contract underneath differs. assert_runner_instance asks the same
trust-boundary question about the axis Forgejo actually has. There is no
repoint and no --local, and both absences are explained where an operator
arriving from the GitHub sibling will hit them.

Forgejo's .runner holds the runner's own long-lived token, unlike GitHub's, so
it is installed 0600 and the mode is re-asserted on every converge — a mode
that drifted leaks the secret silently, since nothing fails and the runner
keeps working. status reports it and never prints the token.

Both downloads verify the published .sha256 before installing: this binary
lands as root and is executed by a systemd unit.

bootstrap --undo learns the guard for the same hazard on the other forge, and
it matters more here — Forgejo has no deregistration endpoint, so the ghost it
would strand has to be deleted by hand.

Known prerequisite, documented rather than assumed: the fetch is
unauthenticated by contract, and a Forgejo with REQUIRE_SIGNIN_VIEW=true
answers 404 for repos it reports as public. Hosting a registry there needs
FORGEJO__service__REQUIRE_SIGNIN_VIEW=false. The refusal names that case,
because it is indistinguishable from a wrong ref.

forgejo#109

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 20:40:54 +00:00
claude-bot-andresmgsl
559a5b23ba test: drive cron converge against stubbed systemctl
The service converge+assert block becomes converge_cron(), behavior-
identical, so test/cli.sh can lift it verbatim (the drop_incus precedent)
and drive it: the already-enabled/active re-run is a no-op, a disabled
stopped unit converges, and an unrecoverably masked or start-dead unit
dies nonzero naming cron and #162 — proven by execution, not only by
structural pins. The converge logs now state the probe fact instead of
claiming an unverified success (panel nit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 19:11:13 +00:00
claude-bot-andresmgsl
29c4a0840b fix: arm cron on agent tenant boxes
Agent tenants exist to run the cron-driven duty engine, and the engine's
installer runs unprivileged — it can detect a missing cron but never
apt-get it. Install cron with the shared toolbelt, then assert the
effective state, not the package: crontab on PATH AND cron.service
enabled and active, converging (unmask/enable/start) best-effort first.
A masked daemon with the binary present is exactly the silent-inert box
the issue reports, so the service asserts are the authority and either
failing dies naming cron.

staging-box stays exempt with the rest of the agent-assert block: no
agent, no duty engine. Machine roles (bootstrap.sh) are deliberately
not widened — the issue names tenants as the demonstrated gap.

Closes #162

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 18:54:27 +00:00
Daniel Marin
34ff1c8917
Merge pull request #161 from claude-bot-andresmgsl/build/160-self-scoped-tags
fix: scope the netmap tag read to Self, not the whole document
2026-07-25 17:16:07 +01:00
claude-bot-andresmgsl
56478d7a7d fix: scope the netmap tag read to Self
json_string_array took the first "Tags" array anywhere in the document,
justified by Self-before-Peer field order. An untagged Self omits the key
entirely (Go omitempty), so the match fell through into Peer and returned
a peer's tag — false-refusing every login join and false-verifying
untagged authkey joins on any tailnet with a tagged node. Extract Self by
brace counting (Location nests an object, so slicing to the next key
would truncate) and read the array inside it.

Refs #160

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:02:54 +00:00
codex-bot-andresmgsl
b849eb57d3 test: prove drill records registry source 2026-07-25 13:41:01 +00:00
codex-bot-andresmgsl
247d9b9ec5 feat: install and record registry snapshot 2026-07-25 13:38:54 +00:00
codex-bot-andresmgsl
5794a80d0e feat: prefer installed template snapshot 2026-07-25 13:34:58 +00:00
codex-bot-andresmgsl
c08bcc6622 fix: align machine role boundaries 2026-07-25 13:07:53 +00:00
codex-bot-andresmgsl
88e59b6ec8 test: cover machine template registry 2026-07-25 11:00:21 +00:00
claude-bot-andresmgsl
79363c2631 test: the tenant surface re-proven against fixture registries — offline, by refusal and by identity (#110)
The suite drives the whole new surface without a network: the family-suffix
dispatch and hard cut, the marker guards firing with the registry
unreachable, the unknown-role refusal listing the resolved source, every
malformed-definition refusal named by key, DIR-vs-REF identity through a
stubbed curl (tags-first precedence pinned), the renderer's box#80 guard,
the one-line pin, and template-lint's whole gate.
2026-07-24 23:08:47 +00:00
claude-bot-andresmgsl
11b02eb070 feat(drill): the record cites the rig-templates SHA the converge read (#110)
Override via RIG_TEMPLATES_REF when the drill was pointed somewhere; else
the pin read from the INSTALLED candidate tree — what actually landed,
never this checkout's copy.
2026-07-24 23:02:12 +00:00
codex-bot-andresmgsl
b6d580ea3e test: make bootstrap provenance count assertive 2026-07-24 10:17:22 +00:00
codex-bot-andresmgsl
53ff0daeff test: isolate bootstrap undo fixtures 2026-07-24 09:41:48 +00:00
codex-bot-andresmgsl
c0272cbae6 test: drive bootstrap undo safety gates 2026-07-24 09:41:48 +00:00
claude-bot-andresmgsl
5a8badd532 feat: CI drills the install lifecycle — four beats against a real tree (#106)
install from this checkout via RIG_INSTALL_SOURCE, assert what landed,
a converging re-run proven by an empty diff, uninstall --all ending in
the absence assert (! -e AND ! -L). test/install-lifecycle.sh carries
the beats plus the instrument-honesty checks (the diff and the absence
assert are broken against scratch trees on every run, so 'demonstrated,
not assumed' is mechanical); test/release.sh gains the local channel's
contract — dir, tarball, loud refusal, never a silent download fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 08:58:53 +00:00
claude-bot-andresmgsl
bd2918d0c5 test(platform): the identity contract, pinned — derivation, determinism, shape, confidentiality, loud degradation (#95)
Both fixture digests are computed outside the implementation, so a refactor
that changes the prefix, the hash or the slicing renames the whole fleet and
fails here. The negative half is the spec's: empty and 'uninitialized'
machine-ids must never be hashed (the collision id is asserted absent, not
just the unavailable line present), and the raw machine-id must never
appear in the output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 07:48:45 +00:00
Daniel Marin
8dcf52940e
Merge pull request #122 from codex-bot-andresmgsl/build/103-box-ref-release-pin
fix: pin bootstrap to a released box
2026-07-24 08:01:14 +01:00
codex-bot-andresmgsl
c02226cb66 fix: propagate pinned box ref 2026-07-24 00:39:24 +00:00
claude-bot-andresmgsl
77cb4bdd64 test: the instrument's honesty, proven without hardware
test/drill.sh awk-extracts the harness's decision functions (the
release.sh pattern) and drives them against fixtures: the ref refusal
names both refs, a loud skip never classifies as a pass, the idempotence
verdict is a real diff that goes non-empty when convergence is broken —
demonstrated mechanically on every CI run — and the record emitter
cannot produce a clean-sweep reading over a skipped leg. CI runs it in
the check job.

The tests caught three real harness bugs before any reviewer could:
printf eating a '- '-leading format as options (a silently empty Failed
section — the exact lie the record exists to prevent), tree_of trusting
GNU readlink -f's exit 0 on a dangling final component, and the arg
refusals sitting behind the root check in violation of the repo's own
validated-before-root doctrine.

(ceremony flow: issue #105)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 00:31:53 +00:00