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.
Phase 7 slice 1: review on the web, so approving does not start with a clone
78912785b00227cffe3f8fb26b057cd60159a6b4 · Keenan Nemetz · 17d ago · parent 9bea7f11
Phase 7 slice 1: review on the web, so approving does not start with a clone Phase 7 is re-scoped: signing is deferred (the SSH handshake already establishes who pushed, and there is no third-party verifier here), and the phase is now about the experience of using the forge. - GET /r/<repo>/commit/<sha>: a commit's metadata and the patch it introduced, per-file hunks with line numbers; the commit list links to it - change requests show the same diff, computed three-dot: a review is what the branch adds since it diverged, not every commit main gained meanwhile - diffs are capped at 512 KiB and labelled when cut, binary files are named rather than faked, and every line is escaped (a <script> line is a test) - the repository README renders on its home page and markdown blobs render as markdown, through the sanitiser issues already use
docs/roadmap.md +56−15
| @@ -179,7 +179,7 @@ Exit criteria: a user with an editor and a key files, comments on and closes an | |||
| 179 | 179 | issue without installing Vole; an invalid tree is rejected with the offending path | |
| 180 | 180 | named and the branch unmoved; deleting the index loses no issues. | |
| 181 | 181 | ||
| 182 | ### Phase 3 — Change requests — ✅ complete (review comments and diffs deferred) | ||
| 182 | ### Phase 3 — Change requests — ✅ complete (review comments deferred; diffs landed in Phase 7) | ||
| 183 | 183 | ||
| 184 | 184 | ✅ **Done.** A change request is a directory on `refs/heads/vole`: | |
| 185 | 185 | ||
| @@ -386,20 +386,61 @@ filename is safe ✅ (and so is a traversal-shaped *request*). The former criter | |||
| 386 | 386 | are terminated in front of the instance, and a site is a subdomain of the domain that | |
| 387 | 387 | certificate already covers. | |
| 388 | 388 | ||
| 389 | ### Phase 7 — Signing and audit | ||
| 390 | |||
| 391 | Scope is provenance and tamper-evidence for a single instance — no peer trust, no federation. | ||
| 392 | |||
| 393 | - User ed25519 key upload; commit signature verification with a visible verified/unverified badge | ||
| 394 | - All metadata commits signed with the instance key; verification of instance-signed | ||
| 395 | metadata on read so a hand-edited ref is flagged | ||
| 396 | - Append-only instance audit log (auth events, key changes, ACL changes, repo deletion) | ||
| 397 | as a hash chain in a dedicated ref, signed | ||
| 398 | - `git verify-commit` interop: signatures produced must verify with stock git tooling | ||
| 399 | |||
| 400 | **Exit criteria:** a commit signed by a registered key shows verified; a metadata commit | ||
| 401 | altered out-of-band is flagged on read; the audit chain detects a deleted middle entry; | ||
| 402 | signatures verify with `git verify-commit` outside Vole. | ||
| 389 | ### Phase 7 — The forge experience | ||
| 390 | |||
| 391 | The system does the hard parts; this phase is about the ten minutes somebody spends | ||
| 392 | using it. Read-only web work first, because review is where a forge is judged and the | ||
| 393 | web is already the review surface. | ||
| 394 | |||
| 395 | ✅ **Slice 1 — review on the web.** `GET /r/<repo>/commit/<sha>` shows a commit's | ||
| 396 | metadata and the patch it introduced, with per-file hunks, line numbers and | ||
| 397 | added/removed rows; the commit list links to each one. A change request shows the same | ||
| 398 | diff, computed three-dot so a review is "what this branch adds since it diverged" | ||
| 399 | rather than every commit `main` gained meanwhile. Diffs are capped at 512 KiB and say | ||
| 400 | when they are cut, binary files are named rather than faked, and every line is escaped | ||
| 401 | — a diff is other people's content, and a test proves a `<script>` line does not | ||
| 402 | execute. A repository's README renders on its home page, and markdown blobs render as | ||
| 403 | markdown, through the same sanitiser issues use. | ||
| 404 | |||
| 405 | Planned: | ||
| 406 | |||
| 407 | - Identity without ceremony: `vole whoami` resolves the key a person pushes with to an | ||
| 408 | account (the roster is readable over SSH at `system.git`), and `vole request approve` | ||
| 409 | stops requiring `--as`. The name someone types has to match their key anyway, so | ||
| 410 | asking for it is a trap rather than a check. | ||
| 411 | - Filing an issue or opening a request without a clone (`--remote ssh://…`), because | ||
| 412 | "clone the repository to report a bug" is the roughest edge in the current flow. | ||
| 413 | - Navigation and empty states: repo tabs with counts, breadcrumbs everywhere, and an | ||
| 414 | empty state that prints the exact command that fills it. | ||
| 415 | - Per-file history (`/commits/<ref>/<path>`), so "when did this change" is one click. | ||
| 416 | |||
| 417 | **Exit criteria:** a reviewer can approve a change using only the web — diff, checks and | ||
| 418 | approvals on one page — without cloning; a new user can go from `vole init` to a pushed | ||
| 419 | commit following the README alone; every page has a working path to its neighbours. | ||
| 420 | |||
| 421 | ### Deferred — signing and audit | ||
| 422 | |||
| 423 | Dropped from the active roadmap by decision: an instance owns its own keys and its own | ||
| 424 | ACL, so a signature over an approval mostly restates what the SSH handshake already | ||
| 425 | established. Revisit when an instance is mirrored somewhere its operator does not | ||
| 426 | control, or when a third party must verify approvals without trusting the instance. | ||
| 427 | |||
| 428 | Measured findings, so the next session does not redo the experiment (git 2.53, | ||
| 429 | OpenSSH 10.2): | ||
| 430 | |||
| 431 | - `git commit -S` and `git commit-tree -S` both sign with `gpg.format=ssh`, so the | ||
| 432 | roster's keys are the signers and `vole request approve` could sign with one flag. | ||
| 433 | - `git verify-commit` against a roster-derived `allowed_signers` accepts any key in the | ||
| 434 | file and does **not** tie the signature to the committer email; identity comes from | ||
| 435 | `ssh-keygen -Y find-principals`, not from the commit. | ||
| 436 | - A signature covers a commit, not a file, so the rule would have to be "the commit | ||
| 437 | that last touched `approvals/<account>.json` is signed by that account" — plus a | ||
| 438 | format-version branch to grandfather existing approvals and mandatory signing | ||
| 439 | configuration for every approver. | ||
| 440 | |||
| 441 | If this returns, the cheaper shape is instance-signed push attestations plus a | ||
| 442 | `vole verify` that replays the tree-checkable rules from a clone — not mandatory user | ||
| 443 | signatures. | ||
| 403 | 444 | ||
| 404 | 445 | ### Phase 8 — Operate it | |
| 405 | 446 | ||