vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Use flags for acl runner edits, not signs
96cbb6fab5aaaf79cc12f4c83877db22214890ca · Keenan Nemetz · 16d ago · parent 852c067d
Use flags for acl runner edits, not signs
The first cut of `acl runners` took `+name` to add and `none` to clear. That is a
local dialect, and the project's flag parser exists to prevent exactly that: git
edits a list with flags (`config --add`, `config --unset-all`, `remote set-url
--add`) or with a subcommand (`sparse-checkout add`), never with a sigil. The `+`
appeared because `-name` collides with short options — which is a reason to use
flags, not a reason to invent syntax.
vole acl runners <repo> # list
vole acl runners <repo> <name>... # replace, like `acl protect`
vole acl runners --add <repo> <name>... # add
vole acl runners --remove <repo> <name>... # remove
vole acl runners --clear <repo> # back to admins only
Two edits at once is refused rather than applied in sequence, and `--remove` fails
when the account is not on the list — `git config --unset` behaves the same way, and
a revocation that never happened should not look like one that did. The push refusal
that quotes the command now quotes this form.
Checked by hand against a throwaway instance as well as in tests, since the point of
the change is what the terminal says:
$ vole acl runners demo
runners: none named; only admins may report CI results for demo
admin 01M5N98MV0GKB48CAFBGGTMMJF keenan
$ vole acl runners --add demo wilbur
runner 01M5N98MVK8Q3S5S6D0DQJ8QG2 wilbur
(admins may also report CI results)
$ vole acl runners --add --clear demo wilbur
vole: use one of --add, --remove or --clear (exit 129)
$ vole acl runners --remove demo keenan
vole: keenan is not a runner for demo (exit 1)
scripts/ci.sh green; TestACLRunnersEditsTheList now covers --add (including a repeat
that must not duplicate), replacement, --remove of an absent account, --clear, and the
two-edits-at-once refusal.
docs/roadmap.md +5−3
| @@ -386,9 +386,10 @@ not a runner for this repository`. The same list decides who may lease a job, si | |||
| 386 | 386 | running a job executes the repository's code — so the answer arrives before the build, | |
| 387 | 387 | not after it. | |
| 388 | 388 | ||
| 389 | `vole acl runners <repo>` shows the list, bare names replace it, `+name` adds, and | ||
| 390 | `none` clears it. There is no `-name`: revoking trust is the operation where being made | ||
| 391 | to state the whole list is a feature, and the list is printed after every change. | ||
| 389 | `vole acl runners <repo>` shows the list and `<name>...` replaces it, the same shape as | ||
| 390 | `acl protect`. `--add` and `--remove` edit one name at a time and `--clear` empties it, | ||
| 391 | following git's way of editing a list (`config --add`, `config --unset-all`, | ||
| 392 | `remote set-url --add`) instead of inventing signs. | ||
| 392 | 393 | ||
| 393 | 394 | Remaining: | |
| 394 | 395 | ||
| @@ -869,6 +870,7 @@ Filled in as phases land. Append; don't rewrite. | |||
| 869 | 870 | | 2026-10-23 | The author of a metadata write is the account the key resolves to, asked with `vole-whoami`; `--author` is gone | `git config user.name` is a name somebody typed for commits: "Keenan Nemetz" sanitises to `keenan-nemetz`, which is not an account, so every issue filed from a normal machine was attributed to nobody and nothing complained. The instance is the only place that maps a key to an account, and it is one SSH verb away on the connection already open. A fallback exists for `--no-push` with the instance unreachable, and it warns loudly rather than guessing silently | | |
| 870 | 871 | | 2026-10-23 | A remote given as an SSH URL is fetched like a named one | Skipping the fetch for a URL is what made `--remote ssh://…` write on a branch that had never been updated: the first write in a fresh repository succeeded (nothing to fast-forward from) and every later one failed as a "lost race" that never happened, re-applying on the same stale tip and reporting "already exists". `git init` plus a URL is now the real no-clone filing path | | |
| 871 | 872 | | 2026-10-23 | The instance tells the client where it is served (`public_url`), so a command can print the page it wrote | A command that answers with an id leaves the reader to assemble a URL, and the instance is the only party that knows its own address behind a proxy. Optional: unset means ids only, which is what it printed before | | |
| 873 | | 2026-10-23 | CLI edits to a list are flags, not signs | The first cut of `acl runners` used `+name` to add and `none` to clear, which is a local dialect: git edits lists with flags (`config --add`, `config --unset-all`, `remote set-url --add`) or with subcommands (`sparse-checkout add`). The sigil appeared because `-name` collides with the flag parser, which is a reason to use flags, not a reason to invent syntax. Names replace the list (matching `acl protect`), `--add`/`--remove` edit, `--clear` empties. The refusal message a push prints quotes the same form, so a copy from the terminal is never stale | | ||
| 872 | 874 | | 2026-10-23 | A result counts only if the host allowed the account that reported it | An attributable record still had a hole: any writer who was an admin could green-light their own branch, and an admin of a repository is exactly the party the check is meant to constrain. So the question "whose build do we believe" became its own allowlist — `acl.runners`, with admins implicit because they can rewrite it. It also governs who may lease a job, since running one executes the repository's code. The result from an unlisted account is still recorded and visible; it simply does not count, and the page says so. Deliberately *not* a role: read/write answers "may this account touch the repository", and conflating the two would make every writer trusted about builds | | |
| 873 | 875 | | 2026-10-23 | The instance holds no signing key at all | The instance key existed to sign run records, and runner signing (Phase 4 slice 4) made it pointless. Deleting it is not tidying: an instance-signed anything is a statement that can only be checked by trusting the instance, so a mirror or a third party cannot verify it — trust would sit on the box. With users, runners and approvers each holding a key, trust sits on the parties, and the server becomes a relay that can withhold service but cannot forge a result. `/healthz` no longer reports a key fingerprint, and the host key is the only secret the instance stores | | |
| 874 | 876 | | 2026-10-23 | A finished run is signed by the runner that ran it, not by the instance | An instance-key signature is the server witnessing itself: it cannot be checked without trusting the box that produced it, and it says nothing about the party that actually ran the steps. The runner already has a key — the one it connected with — so the claim binds repo, ref, SHA, workflow blob, step digest, exit, log object, artifacts and the runner's own finishing time, and the server verifies it against the account the SSH channel authenticated as. Consequences: the runner must have its private key on disk (nothing signs arbitrary bytes through an agent), an unsigned finish is refused, and "signed" still only means "attributed" — a runner can lie about an exit code. Whether a required check must be attested by a particular runner is the next policy question, deliberately not answered here | | |