a73x

423e2c35

Governance: unlisted by default, published by explicit rule

a73x   2026-08-11 16:45

Commit message
Governance: unlisted by default, published by explicit rule

The spec said to delete server.toml and drop --config. Both are wrong,
and for the same reason: access.conf grants to named principals and
@all means every enrolled key, so nothing in the language could
describe an unauthenticated request. Deleting server.toml deleted the
anonymous axis with nothing left to express it.

Invert the default instead. A repository is unlisted and unreadable
without authentication unless a rule says otherwise, so absence of
config is a decision and settings.git only expresses the exception.
@anonymous becomes a reserved principal in the rule table (an access
decision, first-match-wins like any other, never satisfying a write);
listed becomes an option (a display decision), stolen from gitolite
rather than invented.

--config stays, reduced to bootstrap: repos_dir and the listeners must
be read before settings.git can be found, since it lives under
repos_dir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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