Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 |