423e2c35
Governance: unlisted by default, published by explicit rule
a73x 2026-08-11 16:45
Commit message
docs/superpowers/specs/2026-08-09-repo-governance-design.md
| Old | New | ||
|---|---|---|---|
| @@ -195,13 +195,79 @@ non-recursive (`repos.rs:217`) and HTTP resolves by final component | |||
| 195 | `tools.git` and `tools` already collide there. HTTP resolution and the URL scheme | 195 | `tools.git` and `tools` already collide there. HTTP resolution and the URL scheme |
| 196 | must be reworked as part of this. | 196 | must be reworked as part of this. |
| 197 | 197 | ||
| 198 | ## Bootstrap | 198 | ## Exposure: unlisted by default, published by rule |
| 199 | |||
| 200 | Revised 2026-08-11. The original text below assumed `server.toml` could simply | ||
| 201 | be deleted; it cannot, and the reason is worth stating because it is the same | ||
| 202 | reason the rule language needs a new token. | ||
| 203 | |||
| 204 | `access.conf` grants to **named principals**, and `@all` means every *enrolled* | ||
| 205 | key — every identity with a file in `keydir/`. That is a strictly different set | ||
| 206 | from "anybody at all", so no rule in the language as specified can describe an | ||
| 207 | unauthenticated HTTP request. Deleting `server.toml` would therefore delete the | ||
| 208 | anonymous axis with nothing left to express it, which is why the implementation | ||
| 209 | (`b6dcbb7b`) kept `visibility`, `[ui] anonymous` and `[http] anonymous_clone` | ||
| 210 | there and had `settings.git` supersede it only on the access axis. | ||
| 211 | |||
| 212 | The fix is to invert the default rather than to translate the old keys. | ||
| 213 | |||
| 214 | **A repository is unlisted and unreadable without authentication unless a rule | ||
| 215 | says otherwise.** The absence of configuration is then a decision rather than a | ||
| 216 | gap, and `settings.git` only ever has to express the *exception* — a positive | ||
| 217 | grant, which is what the language is already good at. This also generalises a | ||
| 218 | special case the implementation already had to hand-code: `settings.git` is | ||
| 219 | forced off the anonymous surface unless it opts in, because creating it must not | ||
| 220 | publish the key roster to an internet-facing UI. | ||
| 221 | |||
| 222 | Two axes, deliberately separate, because "not in the list" and "404 to a direct | ||
| 223 | URL" are different states and a repo reachable if you know its name is a real | ||
| 224 | configuration: | ||
| 225 | |||
| 226 | ``` | ||
| 227 | repo tools | ||
| 228 | RW+ = @admins | ||
| 229 | RW refs/collab/ = @agents | ||
| 230 | R = @all # every enrolled key | ||
| 231 | R = @anonymous # unauthenticated read: reachable by name | ||
| 232 | option listed = yes # and advertised in the repo list | ||
| 233 | ``` | ||
| 234 | |||
| 235 | `@anonymous` is a **reserved principal** and belongs in the rule table because it | ||
| 236 | is an access decision, resolved by the same first-match-wins evaluation as any | ||
| 237 | other — including `-` to deny it back. `listed` is an **option**, not a rule, | ||
| 238 | because advertising is a display concern rather than an access one; this is the | ||
| 239 | `option` line gitolite already has, so it is stolen rather than invented. | ||
| 240 | `listed` implies nothing about readability: a repo listed but not | ||
| 241 | `R = @anonymous` appears only to viewers who can read it. | ||
| 242 | |||
| 243 | `@anonymous` must never satisfy a write grant. `RW = @anonymous` is a config | ||
| 244 | error and is rejected by push validation, not silently ignored. | ||
| 245 | |||
| 246 | ### Bootstrap, corrected | ||
| 199 | 247 | ||
| 200 | The server reads its storage location and listener addresses before it can read | 248 | The server reads its storage location and listener addresses before it can read |
| 201 | any git object, so these become startup flags: `--repos-dir`, `--http-bind`, | 249 | any git object — including before it can find `settings.git`, which lives under |
| 202 | `--ssh-bind`. They are config-file fields today (`config.rs:7-18`); the existing | 250 | `repos_dir`. Those cannot move into git without a chicken-and-egg problem, so |
| 203 | `--config` flag and `server.toml` are removed. `site_title` and | 251 | `--config` **stays**, reduced to bootstrap only: `repos_dir`, `http_bind`, |
| 204 | `max_release_size` move into `settings.git`. | 252 | `ssh_bind`, and the host key location. Equivalent startup flags are a fine |
| 253 | addition but not a replacement. | ||
| 254 | |||
| 255 | Everything that is not needed to reach `settings.git` does move into it: | ||
| 256 | `site_title`, `max_release_size`, and the exposure axes above. `authorized_keys` | ||
| 257 | (`session.rs:417`, re-read on every auth attempt) is replaced by `keydir/`, | ||
| 258 | which also removes the per-attempt file read. | ||
| 259 | |||
| 260 | An init container creates `settings.git` with one key in `keydir/` and a minimal | ||
| 261 | `conf/access.conf` granting that principal `RW+` on `settings`. From there every | ||
| 262 | change is a push. | ||
| 263 | |||
| 264 | ### Superseded text | ||
| 265 | |||
| 266 | The original section read: *"these become startup flags: `--repos-dir`, | ||
| 267 | `--http-bind`, `--ssh-bind`. They are config-file fields today | ||
| 268 | (`config.rs:7-18`); the existing `--config` flag and `server.toml` are removed. | ||
| 269 | `site_title` and `max_release_size` move into `settings.git`."* The removal of | ||
| 270 | `--config` is wrong for the reason above; the rest stands. | ||
| 205 | 271 | ||
| 206 | An init container creates `settings.git` with one key in `keydir/` and a minimal | 272 | An init container creates `settings.git` with one key in `keydir/` and a minimal |
| 207 | `conf/access.conf` granting that principal `RW+` on `settings`. From there every | 273 | `conf/access.conf` granting that principal `RW+` on `settings`. From there every |