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.
The roster is a database, not a repository
427c47611f7db09e52618a0f2d05291a8641b2d0 · Keenan Nemetz · 17d ago · parent 0f442b1c
The roster is a database, not a repository data/system.git is gone. Accounts, every name they have held and their SSH keys live in data/users.db, next to the other authoritative store (runs.db) and the disposable index. users id, name, status, created user_names every name ever held -- primary key, so a name is issued once user_keys fingerprint, type, line -- primary key, so a key has one owner Two invariants that used to be parse-time checks are now table keys, and a roster change is one transaction instead of a commit, a CAS retry and a tree walk. The snapshot, the five-second reload and the audit line per change are unchanged, so `vole user add-key` still applies without a restart and revocation is still immediate. Clients no longer clone the roster, because there is nothing to clone. The one view a client needs -- active id and name -- is served by a `vole-roster` SSH verb over the key it already pushes with, so name resolution gained no endpoint and no second credential. `vole request new --author keenan` now resolves through that verb (TestRequestCLIResolvesNamesOverSSH: the request landed with author_id = the account id, and an unknown name fails before pushing). A WAL database cannot be backed up with cp, so `vole user backup <path>` takes a consistent copy with VACUUM INTO (TestRosterIsOneDatabaseFile opens the copy and resolves a renamed account and its key from it). This replaces the "identity survives without a side database" criterion, which is withdrawn rather than left looking unmet: the roster is now exactly one file, and the backup is what proves it is not lost with the index. Verified through the binary: init -> create -> add-key -> rename -> list -> backup, the restored copy listing the same account, former name and key; then scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
README.md +15−11
| @@ -33,11 +33,13 @@ Working today: | |||
| 33 | 33 | - Policy enforced in `pre-receive`, so a rejected push moves no refs at all | |
| 34 | 34 | - Protected refs, per-blob size limits, and a push cap enforced by git's own | |
| 35 | 35 | `receive.maxInputSize` | |
| 36 | - The user roster is one file of SSH public keys per user in `data/trusted_keys/` | ||
| 37 | (plain `authorized_keys` format), with an append-only `data/audit.log` recording | ||
| 38 | every key added or revoked. A running instance re-reads the directory every few | ||
| 39 | seconds, so `vole user add-key` — or an edit by hand — applies without a restart, | ||
| 40 | and revocation takes effect the same way | ||
| 36 | - The user roster lives in one SQLite database, `data/users.db`: an account is a | ||
| 37 | stable id plus a mutable name, with its SSH public keys, and a retired name is | ||
| 38 | never issued twice. Nothing stored is secret. A running instance re-reads the | ||
| 39 | database every few seconds, so `vole user add-key` or `vole user retire` applies | ||
| 40 | without a restart and revocation takes effect the same way; an append-only | ||
| 41 | `data/audit.log` records every change. A clone has no copy of the roster — the | ||
| 42 | CLI reads the id/name map it needs over the same SSH key it pushes with | ||
| 41 | 43 | - Issues and change requests stored as files in the repository on the `vole` | |
| 42 | 44 | branch, validated at push time and rendered read-only on the web | |
| 43 | 45 | - Review as a git artifact: an approval names the exact commit it reviewed, and | |
| @@ -79,7 +81,7 @@ would restate it for a verifier this deployment does not have (see the roadmap). | |||
| 79 | 81 | ```sh | |
| 80 | 82 | go build -o vole ./cmd/vole | |
| 81 | 83 | ||
| 82 | ./vole init --dir . # config + data/trusted_keys | ||
| 84 | ./vole init --dir . # config + data/users.db | ||
| 83 | 85 | ./vole user create you | |
| 84 | 86 | ./vole user add-key you --key ~/.ssh/id_ed25519.pub | |
| 85 | 87 | ./vole repo create demo --owner you | |
| @@ -465,16 +467,18 @@ server { | |||
| 465 | 467 | ||
| 466 | 468 | ## Backup | |
| 467 | 469 | ||
| 468 | Two things, and both are small: | ||
| 470 | Three things, and all are small: | ||
| 469 | 471 | ||
| 470 | 472 | ```sh | |
| 471 | rsync -a data/trusted_keys/ backup/trusted_keys/ # users and their keys | ||
| 473 | ./vole user backup backup/users.db # accounts and their keys | ||
| 472 | 474 | cp data/instance.key data/ssh_host_ed25519_key backup/ # the only two secrets | |
| 473 | cp data/audit.log backup/ 2>/dev/null # who added what, when | ||
| 475 | cp data/audit.log backup/ 2>/dev/null # who added what, when | ||
| 474 | 476 | ``` | |
| 475 | 477 | ||
| 476 | Losing the key files means re-issuing them, not losing data — nothing in the | ||
| 477 | roster is secret, so a copied directory leaks nothing. | ||
| 478 | `vole user backup` is a consistent copy taken through SQLite, so it is safe while | ||
| 479 | the instance is running; copying `users.db` by hand can miss rows the database has | ||
| 480 | not checkpointed yet. Losing the roster means re-adding keys, not losing | ||
| 481 | repositories — nothing in it is secret, so a copied file leaks nothing. | ||
| 478 | 482 | ||
| 479 | 483 | Run everything before pushing: | |
| 480 | 484 | ||