Skip to content
quarry Artifact [2e6c9e34bc]
Login

Artifact [2e6c9e34bc]

Artifact 2e6c9e34bcaa60a57a2b186683e5a58f1bf965d0405b1445b06ce53d6969a5d0:


- Feature Name: `roles` (permissions without a manual: named roles and
  sentence-form abilities that compile losslessly to fossil capability
  letters)
- Start Date: 2026-08-27
- Status: Draft for review
- Depends on: RFC 0004 (capability model — kept, this is its interface);
  RFC 0007 (protected branches — which branches; this RFC is whether);
  RFC 0001 Principle 3 (no-script baseline)
- Supersedes: the `repo_role` table and the web verifier's role floor

## Summary
[summary]: #summary

Today the settings page is a table of text inputs holding fossil capability
strings. To let someone chat you type `C`. That is a users' manual in the
shape of a form. This RFC keeps RFC 0004's model — fossil letters as the
stored truth, lossless both ways — and replaces the **interface**: five named
roles described by who they are for, a short list of abilities written as
sentences for deviating from a role, and the raw string always one click away
with its meaning decoded beside it. Roles gate nothing on their own; they are a
way of writing cap strings. The legacy `repo_role` table, which still gates the
web verifier in parallel with the letters, is retired so the page's sentences
have one verifier to be true against.

## Motivation
[motivation]: #motivation

Three facts from the forge as it is:

- `account grant --role write` stores `uv`, which expands through fossil's
  stock categories to check-in, wiki, tickets, archives — and no forum write,
  no chat, no attachments. Fossil's categories predate its forum and chat, so
  no digit or capital is in them. A "write" contributor on a fresh forge cannot
  reply to the discussion on their own landing.
- Ops gate on capability letters; the web verifier gates on `repo_role` via
  `RoleOf(acct, repo) >= requiredRole(need)`; `account grant` writes the table
  and a one-time migration copies it into strings. Two sources of truth for one
  question.
- The page shows what was typed, never what it means. An account's row says
  `uv`; what the account can do is eighteen letters the page computes and does
  not say in words.

The field agrees on the shape of the fix. GitHub: five roles named by who they
are for, custom roles as a base plus permissions. GitLab: five roles, custom
roles as a base plus granular permissions, the base immutable. Forgejo: teams
with none/read/write per unit. Fossil itself: four inherited categories and a
user editor that tags each letter with the category that already grants it.
The permission-settings UX literature says a permissions page "is not just a
list of toggles. It is an explanation system." Every good product here shows
what is needed for the decision at hand and keeps the rest on demand.

## Decisions
[decisions]: #decisions

The proposal that preceded this RFC left four questions open. They are
decided here, with the reasoning, so the build does not reopen them.

**Five roles, not six: Contributor folds into Reader.** Contributor differed
from Reader by two letters, `b` and `B` (attachments). On this forge landings
are the default path for everyone who may post — a landing is a forum thread —
so "someone whose work lands through landings" is every Reader, not a
distinct kind of person. Attachments ride the same moderation as posts (`3`
is the moderated write). A role that differs by a distinction nobody will
remember is a role nobody will choose correctly.

**Readers chat.** `C` is in the Reader string and in the signed-in category
of the public preset. Chat is the forum in real time; anyone who may post in
one may speak in the other, and the moderation letter is the same (`5`). A
forge that wants quieter chat removes one letter from one constant, once, in
a diff someone reviews.

**No teams in this RFC.** GitHub, GitLab and Forgejo assign roles to teams as
well as people, and quarry will (ticket 8318bd2819ca). The model here is
team-shaped already — a team is a name that expands to a role on a set of
repositories — and nothing below needs changing to add one. Building the
people layer first and the team layer on it is the order that does not have
to be undone.

**Site admin stays an account flag, mapped to `s` everywhere, and is shown.**
The fleet's account page lists a person's role per repository; the site-admin
flag appears beside them as what it is, so "why can dan do this" is answered
on one page.

## The model
[model]: #the-model

Three layers. Each compiles to the one below; the bottom is what is stored.

### Layer 1 — Roles

A role is a name, a sentence about who it is for, and a fixed cap string.

| Role | Who it is for | Cap string | What that adds |
|---|---|---|---|
| **Reader** | reads, asks, discusses — most accounts on a public project | `u3bBC7` | reader category (`kptw`: edit wiki, own password, ticket reports, edit tickets) plus forum posting, attachments, chat, email alerts |
| **Developer** | checks in — on any branch that is not protected | `uv3bBC7` | Reader plus the developer category (`ei`: check-in, see addresses). RFC 0007 decides *which* branches; this decides *whether*. |
| **Maintainer** | a developer who also keeps the place tidy | `uv45bBlqCA7` | Developer plus posting without moderation, moderating the forum, wiki and tickets, and announcements. Usually also an owner of protected branches — a protection row, not a letter. |
| **Admin** | changes this repository's settings | `a` | everything except setup, private branches, unversioned writes — fossil's Admin-vs-Setup rule |
| **Custom** | anything that is not exactly one of the above | the string as written | inferred on read: the nearest role and the difference, e.g. *Developer, plus moderate forum, minus chat* |

Roles are constants in one file with a table-driven test asserting each
letter set. A role is never stored; it is recovered by matching the string.

### Visibility — the two categories as presets

| Visibility | `nobody` | `anonymous` | Meaning |
|---|---|---|---|
| **Public project** | `gjorzh2` | `mnc3C` | fossil's stock defaults plus forum reading for visitors, forum posting and chat for anyone signed in |
| **Members only** | — | `gjorzh2mnc3C` | visitors see nothing; a sign-in sees a public project |
| **Private** | — | — | only people with explicit roles; RFC 0004's invisible-not-403 posture |

Fossil's stock `nobody` is `gjorz`; `anonymous` adds `hmnc`. Public project
differs by `h` for visitors (the hyperlink anti-bot letter is quarry's own
posture), `2` for visitors, and `3C` for the signed-in. Those additions are
the whole reason forum and chat work for a new account without anyone typing
a letter. A forge that wants fossil's exact defaults picks Custom.

### Layer 2 — Abilities

A sentence, a letter set, a group. Toggling one on a person whose role lacks
it makes the role Custom with the difference stated; toggling it back restores
the role. The compiler owns implications — `w ⇒ rcn`, `5 ⇒ 4 ⇒ 3 ⇒ 2`,
`k ⇒ jm`, `i ⇒ o` — so the abilities layer cannot produce a string fossil
reads differently from the way the page said it.

| Group | Ability | Letters |
|---|---|---|
| Code | Read the code, clone, download archives | `ogzh` |
| Code | Check in and push | `i` (⇒ `o`) |
| Code | See private branches · push unversioned files | `x` · `y` (advanced) |
| Tickets | Read tickets · file tickets · comment on tickets | `r` · `n` · `c` |
| Tickets | Edit tickets | `w` (⇒ `rcn`) |
| Tickets | Moderate ticket comments · create report formats | `q` · `t` |
| Wiki | Read the wiki and notes | `j` |
| Wiki | Edit wiki pages · create new pages | `k` (⇒ `jm`) · `f` |
| Wiki | Approve wiki appends | `l` |
| Forum | Read the forum · post (moderated) | `2` · `3` (⇒ `2`) |
| Forum | Post without moderation · moderate · grant exemptions | `4` (⇒ `3`) · `5` (⇒ `4`) · `6` |
| Talk | Chat · send announcements · receive email alerts | `C` · `A` · `7` |
| Files | Attach to tickets and wiki · to forum posts | `b` · `B` |
| People | See people's email addresses | `e` (advanced) |
| Repository | Change this repository's settings | `a` |

Not abilities: `d` (legacy, unused), `D` (debug), `s` (site admin — an account
flag), `u` and `v` (a role's business; the fossil trap where `v` does not
imply `u` is what the compiler hides), `L` (virtual). All reachable in Layer 3.

### Layer 3 — Letters, settings, configuration

*Advanced (fossil)*: the raw string for every person and category with a
decoder beneath each input; the `fossil settings` names quarry passes to the
store (`ignore-glob`, `binary-glob`, `crlf-glob`, `manifest`, `autosync`, …)
as name and value, versionable ones marked and tree-overridden ones flagged;
export and import of fossil's configuration areas (`user`, `skin`, `ticket`,
`project`, `shun`, …), the files `fossil configuration pull/push` move.

## Information architecture
[ia]: #information-architecture

Settings, by the question asked rather than by implementation order:

| Section | Answers | Holds |
|---|---|---|
| Who can see this | is it public? | the three visibility presets; Custom reveals the category strings decoded |
| People | who can do what? | one row per person: role chip, deviation chips, *owns trunk* chip, effective sentence; a row opens the editor |
| Branches | which are guarded, by whom? | RFC 0007's rules; owners link to their People rows |
| Repository | what is it? | description, homepage, topics, the clone line |
| Integrations | what does it talk to? | webhooks, mirror posture |
| Advanced (fossil) | the letters and the knobs | raw strings with decoders · fossil settings · config areas |
| Danger zone | the irreversible | remove, delete |

Plus: a per-person page (`/r/{repo}/people/{name}`) that is the editor on its
own URL — fossil's `/setup_uedit`, in sentences; the fleet account page with
the same editor per repository; and a fleet default for what a new repository
starts as.

## Progressive enhancement
[progressive]: #progressive-enhancement

The baseline is forms: a radio for the role, checkboxes for abilities, a text
input for the string, a reload on save showing the effective sentence computed
server-side and a line saying what changed. Script adds the same things live:
the sentence as you click, the string and the checkboxes as two views of one
signal, the change line and the lockout refusal before Save. Script never
makes a control possible.

## Bulletproofing
[bulletproof]: #bulletproofing

- A save that would remove your own `a` or `s` on the repository you are
  editing is refused with the reason; a save that would leave a repository
  with no admin is refused unless you are a site admin, who is told.
- Private is a wizard, not a switch: it lists every account that reaches the
  repository only through categories and offers each a role before blanking
  the categories — fossil's own warning, built in.
- The compiler owns implications; states fossil reads one way and a person
  another cannot be produced by a checkbox and are named when typed.
- Presets are constants with tests. Import is honest: a string that matches a
  role shows the role; the rest show Custom with the nearest role and the
  difference. Every string is decoded everywhere it is shown — page, CLI, API,
  and the timeline event, which says *alice made bob a Developer*.

## One system
[one]: #one-system

`repo_role` is retired. The verifier asks what the ops ask — does the effective
set contain the letter this needs — with the three floors mapped once:
signed-in ⇒ `L`, repository admin ⇒ `a`, site admin ⇒ `s`. `account grant
--role` stays as a command and writes the role's string. The migration runs
one last time and the table goes.

## Build order
[build]: #build-order

| Rung | Work |
|---|---|
| 1 | This RFC |
| 2 | The compiler: roles, abilities, implication closure, decode, infer; table-driven tests |
| 3 | One system: verifier on letters; grant writes strings; migration; `repo_role` gone |
| 4 | People section and the per-person editor as forms; `account.explain`; the lockout guard |
| 5 | Visibility presets, the take-private guard, the fleet default |
| 6 | Settings page reorganised; Advanced (fossil) with decoders |
| 7 | Datastar: live sentence, two-way string ⇄ checkboxes, pre-save preview |
| 8 | fossil settings editor; configuration-area export/import — surface shipped; the store read/write is blocked on [zit#153](https://github.com/danmestas/zit/issues/153) (`settings`/`config` refuse `-R` on a bare store), so the section renders an honest note until that lands |

Rung 3 before 4, deliberately: sentences built on two verifiers are promises
the other one can break.

Rung 8's UI, ops, and store methods are built and tested; the store methods
return a typed "no working tree" result because zit's `settings` and `config`
verbs still refuse `-R` on a bare store, though the repository and global tiers
they touch need no checkout (fossil's own `fossil settings -R` reads them).
That is a zit limitation, filed as zit#153; the section says so rather than
faking an editor, and fills in with no quarry change once the fix ships. Per
the store-interface rule (0001 §2) the fix belongs upstream, not in a per-repo
throwaway checkout here.

## What this is not
[not]: #what-this-is-not

Not a new capability letter (RFC 0004 §4 forbids inventing one). Not a change
to what any letter means in fossil. Not portable: roles are how this forge
writes strings; the strings are what travels.