A person's write access is read from the collaborator permission, never from an association; trial P-47 passes (#222, #231)
Trial P-32 failed for the permission: the reader App reads the org owner's authorAssociation as CONTRIBUTOR, where the Maintainer's own login reads MEMBER. So a person's write access is read from the repository's collaborator permission, GET /repos/{repo}/collaborators/{login}/permission, and never from an association. admin, maintain and write count; triage, read and none do not. The Maintainer answered on 2026-10-21 that triage is not write access. Trial P-47 shows the reader App can make that read, and with what grant.
What changes
-
Trial P-47 passes.
trials/P-47.jsonis built from the Maintainer's run oftools/trial_reader_reads.shon 2026-10-21, from its output folder alone.- It holds two installation tokens for the reader, each narrowed to one grant on the canary, ten reads, and both revocations (204).
- With either grant the reader read
adminfor don-petry,writefor Joaolfelicio (named with the Maintainer's consent),writefor the machine user (role_namemaintain), andreadfor octocat. It also read the activity oftrial/p-9. - The triage role was not tried: the canary has none, and the Maintainer chose to run without one.
-
Each read's grant is the one GitHub's own
x-accepted-github-permissionsnames. That ismetadata=readfor the permission read andcontents=readfor the activity read. The canary is public, so a token withmetadataalone read the activity all the same; a private repo would refuse it. -
config/apps.jsonnames each grant.read:facts.actor.can_writesits undermetadata: readfor the reader.read:facts.last_pushermoves frompull_requeststocontents: readfor the reader and the Controller (#231). No App's grant changes, so P-39's fixture still holds, astools/check_apps.pychecks.tools/check_apps.pyalso accepts a property of a definition of the facts as a need, such asactor.can_write. -
Which field the port reads, and why. The port (
adapters/timeline.py, #272) reads GitHub'spermissionfield, notrole_name.permissionfoldsmaintainintowrite, as the machine user's answer shows, and GitHub documents that it foldstriageintoread. So it gives the ruling's reading directly, and any custom role GitHub folds into one of its five values.role_namecarriesmaintain,triageand custom roles by their own names. Reading it would defer on an org with custom roles.- A test holds the machine user's case,
permissionwritewithrole_namemaintain, and holds the port to the fixture.
-
The issue world.
sim/issues.pyserves each login's permission as GitHub does, and buildscan_writewith its own copy of the rule. It never imports the port, and a test holds both copies to the ruling's three words.- The grounding rule
a_logins_permission_is_read_from_the_collaborator_endpointreplacesan_issues_author_has_an_association, and is grounded bytrials/P-47.json. tests/test_adapters.py::test_what_each_credential_is_shown_of_a_permission_is_recordedis held now: its@pendingis off, and L-57's check names it.
- The grounding rule
-
The trial script. It refuses before minting anything when:
- OUT exists, or resolves inside the repo through any link;
- the key cannot be read;
- the repo is the Factory's own, or not the reader's canary;
- a login or branch has a bad shape.
It also reads no
.curlrc, redacts every answer before OUT, and revokes every token on every path, including a mint cut short and a signal. Each of these has a test against a stand-in GitHub, and a mutant. -
The Kernel does not change. By the working ruling,
can_writestays a boolean the port computes, and the Kernel already defers on one it cannot read. -
Documents. Decision 0046 (renumbered at enqueue if taken), the P-32 and P-47 rows, the batch line, and the port's contract in §3.
Not covered here
- The issue world's other actors. In
sim/issues.py, the label actors, reopeners and editors still carry a fixedcan_writefromsim/world.py; only the author's comes from the permission. Filed as #299, "Issue world: every actor's write access is built from the permission it serves, not the author's alone", under #182.
Reviews and verification
- Export verify.
make verifypassed on a freshgit archiveexport withPATH=/usr/bin:/bin SIM_SEEDS=40. - Mutants. Each new mutant was applied by hand on a scratch copy and killed by its tests, except one. The mutant for the signal path survives on macOS, whose
shruns the exit trap on SIGTERM anyway. CI'sshis dash, which does not, so CI's shards kill it. - Claude review (fresh context). Two majors and minors, answered in the last commit. The majors: the activity read was mapped against GitHub's header, and no test failed if the trial script's redaction was skipped.
- Security pass. The same mapping major, plus hardening: a mint cut short, a curl configuration, the fixture's records. All are answered.
- AGY (Gemini 4 Argon). No findings.
- Muse (Muse Wonder Full). The redaction gap as a major, plus the symlink, the signal path, the mint cut short and the mutants' heading. All are answered.
🤖 Generated with Claude Code