Governance: unlisted by default, published by explicit rule #68

Merged
a73x merged 2 commits from worktree-agent-ace0a9a921526a954 into main 2026-08-11 18:38:56 +00:00
a73x commented 2026-08-11 18:27:19 +00:00 (Migrated from git.a73x.sh)

Implements the exposure model from the revised spec section: a governed repository is unlisted and unreadable without authentication unless a rule says otherwise.

@anonymous is a reserved principal in the rule table — an access decision, resolved by the same first-match-wins evaluation, - included. It is kept out of @all by a short-circuit in user_matches: an anonymous subject matches the literal @anonymous token and nothing else, and a named principal never matches @anonymous. listed is an option, gitolite's syntax, evaluated separately and gated on the read grant.

Config errors, both rejected at push validation with a line number:

  • option listed = yes without R = @anonymous in force. Checked by evaluation over the whole file, so the grant may live in another block; a wild pattern names nothing enumerable, so the evaluator gates is_listed on the same grant and fails closed there.
  • @anonymous in any permission other than R or -.

The inverted default belongs to the governed world: with no settings.git, server.toml still decides and everything is listed and served as before. Where governance is in force, access.conf now supersedes server.toml on the anonymous axis too, in both directions — a private server.toml cannot subtract what a rule grants, and the most permissive one cannot publish what no rule grants.

The apply_governance_default / without_anonymous_access special case is removed: nothing grants @anonymous anything until an operator writes a rule, so settings.git is off the anonymous surface by the default rather than by its name. One behaviour change falls out — an unpopulated settings.git (no conf/access.conf, governing nothing) is now an ordinary repository on an ungoverned server.

Tests: end-to-end over real HTTP against live servers for all three exposure states, the ungoverned regression, both config errors (rejection plus the previously-live config still governing), wild repos, and agents/settings not being mistaken for the governance repository; release downloads following the grant; unit tests for the rule language and for Exposure. Full suite green, cargo clippy --all-targets silent.


Migrated from git-collab patche 1beeb03e3f61a0e5b7ab6b154b1554e30eb492d5.

Original author: a73x dev@a73x.sh.

Fixes #65

Revision 1: 805398875c783cf26bcd7eb90b25806b637dd3b9; base 132d9eca87075cc36055e5e7f3c4219dd0339b35

Revision 2: 18181599bcad7189f68712568637795d0e5a6436; base 132d9eca87075cc36055e5e7f3c4219dd0339b35

Close the unpopulated-settings.git window at the source.

A push to settings whose resulting tree holds keydir/ without conf/access.conf is refused — by the resulting tree rather than the diff, so pushing the roster first and deleting the rules later are the same rejection with the same message. The message names the fix, because an operator hitting this is mid-bootstrap: push both together, or conf/access.conf first.

Rule 3 holds at the boundary: no settings.git at all, and a settings.git with neither file, both stay ungoverned and behave exactly as before. Only a roster without rules is refused.

A repository already in that state is on disk where no hook can reach it, so governance::unruled_roster reports it once at startup and does nothing else. Hiding it would reinstate the second mechanism the closed-by-default posture exists to avoid, and refusing to start would take down a server over a repository it has been serving all along.

Tests: pushing keydir/ alone is refused, the message names both halves and the fix, and nothing lands (settings.git's HEAD does not resolve); pushing both together is accepted, and from that moment the server is governed and the roster is closed by the default; deleting the rules while keeping the roster hits the same check, asserted on the message so it pins which check fired; a settings repo with neither file still takes an ordinary push. Unit tests cover the tree check's four cases and the startup detector. Full suite green, cargo clippy --all-targets silent.

Implements the exposure model from the revised spec section: a governed repository is unlisted and unreadable without authentication unless a rule says otherwise. `@anonymous` is a reserved principal in the rule table — an access decision, resolved by the same first-match-wins evaluation, `-` included. It is kept out of `@all` by a short-circuit in `user_matches`: an anonymous subject matches the literal `@anonymous` token and nothing else, and a named principal never matches `@anonymous`. `listed` is an `option`, gitolite's syntax, evaluated separately and gated on the read grant. Config errors, both rejected at push validation with a line number: - `option listed = yes` without `R = @anonymous` in force. Checked by evaluation over the whole file, so the grant may live in another block; a wild pattern names nothing enumerable, so the evaluator gates `is_listed` on the same grant and fails closed there. - `@anonymous` in any permission other than `R` or `-`. The inverted default belongs to the governed world: with no `settings.git`, `server.toml` still decides and everything is listed and served as before. Where governance is in force, `access.conf` now supersedes `server.toml` on the anonymous axis too, in both directions — a private `server.toml` cannot subtract what a rule grants, and the most permissive one cannot publish what no rule grants. The `apply_governance_default` / `without_anonymous_access` special case is removed: nothing grants `@anonymous` anything until an operator writes a rule, so `settings.git` is off the anonymous surface by the default rather than by its name. One behaviour change falls out — an unpopulated `settings.git` (no `conf/access.conf`, governing nothing) is now an ordinary repository on an ungoverned server. Tests: end-to-end over real HTTP against live servers for all three exposure states, the ungoverned regression, both config errors (rejection plus the previously-live config still governing), wild repos, and `agents/settings` not being mistaken for the governance repository; release downloads following the grant; unit tests for the rule language and for `Exposure`. Full suite green, `cargo clippy --all-targets` silent. --- Migrated from git-collab patche `1beeb03e3f61a0e5b7ab6b154b1554e30eb492d5`. Original author: a73x <dev@a73x.sh>. Fixes #65 Revision 1: `805398875c783cf26bcd7eb90b25806b637dd3b9`; base `132d9eca87075cc36055e5e7f3c4219dd0339b35` Revision 2: `18181599bcad7189f68712568637795d0e5a6436`; base `132d9eca87075cc36055e5e7f3c4219dd0339b35` Close the unpopulated-`settings.git` window at the source. A push to `settings` whose resulting tree holds `keydir/` without `conf/access.conf` is refused — by the resulting tree rather than the diff, so pushing the roster first and deleting the rules later are the same rejection with the same message. The message names the fix, because an operator hitting this is mid-bootstrap: push both together, or `conf/access.conf` first. Rule 3 holds at the boundary: no `settings.git` at all, and a `settings.git` with neither file, both stay ungoverned and behave exactly as before. Only a roster *without* rules is refused. A repository already in that state is on disk where no hook can reach it, so `governance::unruled_roster` reports it once at startup and does nothing else. Hiding it would reinstate the second mechanism the closed-by-default posture exists to avoid, and refusing to start would take down a server over a repository it has been serving all along. Tests: pushing `keydir/` alone is refused, the message names both halves and the fix, and nothing lands (settings.git's HEAD does not resolve); pushing both together is accepted, and from that moment the server is governed and the roster is closed by the default; deleting the rules while keeping the roster hits the same check, asserted on the message so it pins which check fired; a settings repo with neither file still takes an ordinary push. Unit tests cover the tree check's four cases and the startup detector. Full suite green, `cargo clippy --all-targets` silent.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
a73x/git-collab!68
No description provided.