- 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: Implemented — next amendment: the steward role (hosting is not owning), 2026-09-10. Amended 2026-09-13: Reader spells out its letters instead of routing through fossil's reader category — see §"Reader stopped routing through the reader category". The other roles are unchanged.
- 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.
RFC 0018 is that layer, and it qualifies the claim above in two places. A team also carries a membership floor — authority arriving from membership itself, with no grant behind it — which is a new source rather than a name expanding to a role. And one gate below does move:
ticket.intakereadsrinstead ofn(RFC 0017). The letters, presets, categories, expansion, guard and editor are unchanged, so the order still did not have to be undone.
Site admin stays an account flag, mapped to s at forge scope only, 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. Since #318 the flag gives
no repository: a site admin holds a repository's letters only through a role
or a team, is not listed among its people without one, and keeps only the
operator paths RFC 0004 names over repositories they do not hold.
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 | bcghjoprz2378BC |
read code, wiki, tickets and forum; post in the forum, comment on a ticket, chat, attachments, CI, email alerts, own password. Amended: see §"Reader stopped routing through the reader category" |
| 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 |
Reader stopped routing through the reader category
The table above used to give Reader the string u3bBC7 and describe
what it adds as "reader category (kptw: edit wiki, own password,
ticket reports, edit tickets)". Both halves were true, and together they
were the bug: the row said this role edits wikis and tickets, while the
words the role is granted BY — "reads, asks, discusses" — said it reads.
The RFC was honest and the role was misnamed.
u is fossil's reader category and effective() expands it, so on any
repository that has not overridden its categories it resolved through
fossil's shipped default kptw. Granting Reader granted wiki editing
and ticket editing, and the person granting it could not see that
happen.
Reader now spells its letters out: bcghjoprz2378BC. Three
decisions inside that, recorded because each could look like an
oversight:
pis kept. Fossil calls it "change own password"; quarry has also made it the signed-in marker on four cross-repository ops,me.feedamong them. Dropping it with the rest of the category would have taken the API's dashboard away from every Reader.cwithoutn. Comment on a ticket, do not mint one. Fossil's stock ANONYMOUS category carries both, so this is narrower than what fossil hands a passer-by — deliberately, because RFC 0017 makesnthe store door and the local tier the place a reader files into.- Developer and Maintainer keep
u. Their descriptions promise writing, so the expansion is not a surprise there, and keeping it means a repository that deliberately WIDENS its reader category still reaches the roles meant to grow with it. The category mechanism is untouched; only this preset stopped using it.
What does not change: RFC 0004's model. u still means the reader
category and still expands. A preset is quarry's own interface over the
letters (this RFC's whole premise), so redefining one is not a
reinterpretation of fossil — it is this layer doing its job.
The durable half of the fix is disclosure. A hand-written string
holding u has the same surprise, and always will. The person editor
now lists what an account holds that its string does not say, so
whatever arrives from a category or a team is visible at the moment of
granting rather than discovered later.
No migration. A stored string is not rewritten, so nobody loses access;
existing u holders keep what they have and can now see it.
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, a site admin's included (#318). - 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 at forge scope
only. 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 import — the settings read/write and config import run against the served bare store through zit … -R, unblocked by zit#153; an older zit that predates it sees a note to upgrade |
Rung 3 before 4, deliberately: sentences built on two verifiers are promises the other one can break.
Rung 8 reads and writes the served bare store's fossil settings and imports
its configuration areas through zit settings -R and zit config -R. Those
verbs used to refuse -R — the repository and global tiers need no checkout
(fossil's own fossil settings -R reads them), so the refusal was too broad;
that was filed as zit#153 and fixed there rather than worked around here (the
store-interface rule, 0001 §2, puts the fix upstream, not in a per-repo
throwaway checkout). A zit predating the fix falls back to a note to upgrade.
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.