Artifact 2e6c9e34bcaa60a57a2b186683e5a58f1bf965d0405b1445b06ce53d6969a5d0:
- File docs/rfcs/0008-roles.md — part of check-in [afe83bd5e7] at 2026-08-27 16:35:48 on branch perm-8 — roles: fossil settings and config surface, blocked on zit#153 (user: dmestas, size: 13501) [annotate] [blame] [check-ins using]
- 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_roletable and the web verifier's role floor
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
Three facts from the forge as it is:
account grant --role writestoresuv, 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_roleviaRoleOf(acct, repo) >= requiredRole(need);account grantwrites 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
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
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
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
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
- A save that would remove your own
aorson 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
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
| 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 (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 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.