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.
Drop the refs/vole rule, which has nothing left to guard
592710c296c678e21bc1dc89341ad71606316500 · Keenan Nemetz · 17d ago · parent ec5c519c
Drop the refs/vole rule, which has nothing left to guard The namespace held the ACL and needed a rule of its own: a client who could write refs/vole/acl could have granted itself admin. The rules live in data/users.db now, so the namespace is empty and the special case only made refusals inconsistent — an owner pushing refs/vole/x got "server-owned", which is no longer true. Only branches and tags are writable, and that rule already refuses everything else, refs/vole/* included. The test now pins that: refs/notes/commits and refs/vole/acl are refused with the same message.
docs/roadmap.md +6−3
| @@ -120,8 +120,11 @@ Deviations from the original plan, and why: | |||
| 120 | 120 | - **`pre-receive` only, no `update` hook.** One enforcement point with | |
| 121 | 121 | all-or-nothing semantics; it is what makes "a rejected push moves no refs" | |
| 122 | 122 | true by construction. | |
| 123 | - **`refs/vole/*` is rejected for client pushes.** The ACL lives there, so | ||
| 124 | permitting client writes would let a writer self-grant admin. | ||
| 123 | - **Only branches and tags are writable.** A push to `refs/notes/*`, `refs/vole/*` or | ||
| 124 | any other namespace is refused. `refs/vole/*` used to have a rule of its own because | ||
| 125 | the ACL lived there, where a client write would have been a self-granted admin; when | ||
| 126 | the rules moved to `data/users.db` the namespace emptied and the special case went | ||
| 127 | with it. | ||
| 125 | 128 | - **The roster is a SQLite database, not a repository and not a key directory.** | |
| 126 | 129 | Its first shape was `refs/heads/main` in `data/system.git`; the second was | |
| 127 | 130 | `data/trusted_keys/<user>.keys` files. The git version paid for a commit, a CAS | |
| @@ -875,7 +878,7 @@ Filled in as phases land. Append; don't rewrite. | |||
| 875 | 878 | | 2026-10-22 | Push size cap + concurrency semaphore in Phase 1 | Spike B: 1 GiB push peaked at 1.13 GB git-child RSS | | |
| 876 | 879 | | 2026-10-22 | Static sites served from trees, no export step | Spike C: cold 1.2 ms p50; 170k req/s warm with an 18.5 MiB cache | | |
| 877 | 880 | | 2026-10-22 | `pre-receive` is the only policy hook | One enforcement point; all-or-nothing, so a refusal can never half-apply | | |
| 878 | | 2026-10-22 | Clients may not push `refs/vole/*` | The ACL lived there, and allowing it would let a writer self-grant admin. The prefix stays refused as a guard even though nothing is written there now (see the 2026-10-23 row about `data/users.db`) | | ||
| 881 | | 2026-10-22 | Clients may not push `refs/vole/*` (superseded 2026-10-23: the namespace is unused and the general rule covers it) | The ACL lived there, and allowing it would let a writer self-grant admin. Once the rules moved to `data/users.db` nothing was stored there, so the special case went rather than being kept as a guard: `refs/vole/*` is refused now because it is not a branch or a tag, the same rule that refuses `refs/notes/*` | | ||
| 879 | 882 | | 2026-10-22 | SQLite via `modernc.org/sqlite` (two files: auth, index) | CGO-free single binary; the index file is disposable, auth is not | | |
| 880 | 883 | | 2026-10-22 | Bad credentials return 401 with a challenge | 403 leaked repo existence and suppressed client retry | | |
| 881 | 884 | | 2026-10-22 | Writes move to SSH; HTTP becomes anonymous read-only | Removes password storage, login, sessions and the token table outright | | |