vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Phase 4: a runner can narrow what it takes, and cannot widen it
b238ac2448290fb706d0503c2e65798fc618f994 · Keenan Nemetz · 17d ago · parent 8d336083
Phase 4: a runner can narrow what it takes, and cannot widen it `vole runner --repos name` (repeatable) restricts a session to the repositories it names. The restriction is applied by the server as an intersection with what the account's runner allowlist grants, which is the whole design: the flag is a filter, not a grant, so the worst a client can do is run less than it was allowed to. Honouring the client's list directly would have made an SSH flag an access-control decision. An empty filter means "no narrowing" rather than "nothing", which keeps an unfiltered runner following allowlist edits instead of freezing its list at connect time. The filter has a failure mode with no symptom: a runner whose --repos matches nothing connects, waits, and never sees work, which looks exactly like a quiet instance. So "ping" now reports the account's grant and the runner refuses at startup, naming what it asked for and what it was allowed — the same class of check as the signing key, cheaper before a build than after one that never happened. An account with an empty grant and no filter is only a warning, because an allowlist nobody has written yet takes effect without a restart. runs.Store.Lease now takes a LeaseRequest struct. It gained a second list of names, and two adjacent []string arguments are exactly the shape that gets transposed in silence. Measured: TestARequestCannotLeaseBeyondTheAllowlist — a repository the account may *write* to but is not a runner for is refused even when asked for by name, and an admin can still lease it. TestARepoFilterTakesOnlyWhatItNames, TestNarrowCanOnlySubtract, TestRepoFilterThatCanNeverMatchIsRefused and TestARepoFilterOutsideTheGrantIsRefusedAtStartup cover the rest.
internal/server/runners_page_test.go +2−1
| @@ -81,7 +81,8 @@ func TestRunnersPageSaysWhenARepositoryIsAtItsCap(t *testing.T) { | |||
| 81 | 81 | if _, err := f.srv.Runs.Enqueue(running); err != nil { | |
| 82 | 82 | t.Fatal(err) | |
| 83 | 83 | } | |
| 84 | if _, err := f.srv.Runs.Lease(f.ownerID, []string{f.repo}, time.Minute, 1, now); err != nil { | ||
| 84 | if _, err := f.srv.Runs.Lease(runs.LeaseRequest{Owner: f.ownerID, | ||
| 85 | Repos: []string{f.repo}, TTL: time.Minute, MaxPerRepo: 1}, now); err != nil { | ||
| 85 | 86 | t.Fatal(err) | |
| 86 | 87 | } | |
| 87 | 88 | waiting := queuedRun("01WAITINGRUN00000000000000", f.repo, "waiting", now) | |