# Row 236 build notes: #436b Gitea grant path (#1742, schema 3j-3k) Builder Megavolt, reviewer Dewey, gate owner Sage. Draft; the results sections are filled in after the runs on Sage's gate. ## Sources - Brief: `docs/plans/2026-10-20_agent-credentials.md`, "Brief: #436b Gitea grant path, build (schema 3j-3k)", blob 9259ba2a1. - Design: `agents/megavolt/work/436b/DESIGN.md` r5 at 1be535aca (sha256 68df02cd…f4), unedited. Scope: sections 2 to 7, and section 9's X-G1 to X-G15 and mutants. Section 10 (GitHub half) is out of scope. - Build carry: `agents/megavolt/work/436b/BUILD-CARRY.md` at 3330c901b (N9, N10). - Schema label 3j-3k per sage-2237. If 264a or 206 lands first, this rebases onto a new branch name and reruns the whole bus suite, the schema.test counts and the T46 lists. - Rebases: onto 86f95bb27 after row 273 (sage-2403, no conflicts), then onto 0fa7bf421 after row 191 (sage-2452, sage-2457). The 191 rebase took three unions in commit 1, 191's `processing` first and then the grant options: the Broker constructor (broker.mjs:317), the runtime's Broker options (runtime.mjs:173) and the cli case line in `scripts/mosaic` (191's `halt|resume|processing|` plus `grants`). ## What changed - `packages/bus/src/gitea-grant.mjs` (new): the grant body check, the admin slot (set, rotate, remove), the admin and seat API calls, the start-time scan (`giteaStart`, `openGiteaGrants`, `tokenUnknown`). - `packages/bus/src/broker.mjs`: the gitea executor (steps 0 to 8), settle, restart reconcile, revoke and fire, sweep, status, and the `grants.*` verbs. - `packages/bus/src/credentials.mjs`: `held` pairs at start; the register and claim paths the executor uses. - `packages/bus/src/migrations/3j-3k.sql`, `schema.sql`, `store.mjs`: `gitea_users` and `gitea_grants`, new tables only. - `packages/bus/src/runtime.mjs`, `server.mjs`, `human-cli.mjs`: wiring of the start scan and the verbs. - `packages/cli/src/grants.mjs` (new), `cli.mjs`, `scripts/mosaic`: `mosaic grants`. - Tests: `gitea-grant.test.mjs`, `gitea-migration.test.mjs`, `gitea-grant-docker.test.mjs` (X-G3), `fake-gitea-api.mjs`, `fixtures/schema-3j.sql` (02da6a2ae's schema.sql), `cli/tests/grants.test.mjs`, and the pin updates in the migration tests, `schema.test.mjs`, `channel-proof.test.mjs` and `stack-release.test.mjs` (T46). - READMEs of bus and cli. ## Choices and assumptions Each is a reading of the design where it left room. Dewey may rule otherwise. 1. Admin rotation is due once today is past `rotateBy` (the date itself is still good). 2. A seat-token rotation moves the record at step 8, after the old token's delete (step 7). 3. A step 7 failure leaves the grant applied, with the old id in `serviceLeft` and the notice "The previous token X is still live". 4. A failure after the commit step's rename returns `settle-pending`; the next start settles it. 5. tester-cli keeps its 5 s timeout; the grant verbs answer at once and the executor runs after. 6. A revoke of a token-unknown agent takes both ids from that decision's last commit step. 7. A user adopted at restart is upserted in the same transaction as its `grant.failed interrupted`. 8. `#giteaUnlink` (first-grant commit undo) uses a temporary reference dated `9999-12-31`, so it is never due while it lives. 9. X-G12's org-team case: the fake models team membership as collaborator reach on the repository, which is what `/user/repos` shows (F11, F12). 10. X-G15's look-alikes are stored under `action.refused` and under a non-grant kind, since the 3j trigger refuses the human's NULL pair on `action.allowed`. The trigger cases of X-G13 (session, NULL pair, `grant.host.enrolled`) are in `grant-migration.test.mjs`. 11. Step 6 faults: a pre-rename failure is injected by making `gitea.tmp` a directory; a step 8 store fault by a TEMP trigger on `gitea_grants`. 12. The credentials-verb revoke close is tracked, so `giteaIdle()` waits for it. 13. `openGiteaGrants` gives `agent: null` for a spend with no step yet, so `status.running` can show a null agent; the CLI prints it empty. 14. N9: a restart ignores a settle-pending commit with a `grant.revoked` after it and ends it `revoked` with no message (notify false). A 404 on a delete by id counts as done. 15. N10: `serviceLeft` is a list in every body and answer. 16. A restart claims the agent's key for each open grant it undoes. One executor runs per agent (grant-busy), so a killed child leaves at most one open grant per agent. If a store ever held two, the second's claim fails and the undo runs with no admin: its token is left and named in `serviceLeft`, never deleted unclaimed. 17. X-G7 changes the binding with a fixture decision whose hash differs (row 234's G9 pattern), since the broker keeps its own copy of the business. The admin url check takes row 199's `parseGiteaUrl`, which accepts the origin with or without a trailing slash. 18. r2, Dewey B1 (dewey-0781): step 8 and both settle branches (forward, back) check for a `grant.revoked` of the agent after the decision's raise, in the same transaction as their write. A revoke that came while step 7's or the settle's delete awaited took the record and both tokens, so the grant ends `revoked` with the human's notice, as a revoke of a running executor ends it (X-G11), and no record is written back. N9's restart case stays silent: a restart never settles a revoked rotation. 19. r2, Dewey B2: a sweep retry of `tokens` or `login` checks the user's id first, as the revoke close does, against the `gitea_users` row (it survives a fire). A 404 skips with `gitea-user-gone`, a 200 with another id with `gitea-user-foreign`, any other status leaves the entry. Skips go in `grant.swept` and the sweep answer as `skipped: [{agent, user, id, reason}]`; a skipped entry is not left, so the next sweep does not retry it. 20. r3, Dewey N5 (dewey-0783): the settle's 401 branch and its grant-token-unknown branch take the same revoke check. A revoke during the whoami ends the grant `revoked`, not `grant-lost`, and the token-unknown branch neither holds the agent again nor writes `grant-token-unknown`, so no start names a file the revoke unlinked. 21. sage-2453, ruling (a) on megavolt-0123: 191's `#processingGate` refuses every agent verb off its read list while processing is halted. `grants.status` is a read like `hosts.status`, and the kill-switch plan (`docs/plans/2026-10-20_kill-switch.md`:16) says "Reads and the human's own verbs, including the clear, still work", so it joins the list. This touches row 191's list (one entry). An agent's `grants.revoke` stays refused while halted, as `hosts.revoke` does. ## Named for DEFERRED (r2, Dewey N1-N4) None is a small local change, so none is in r2. sage-2388: all four DEFERRED; N3 and N4 get a bus README sentence each in r3 (a left admin token is deleted by hand at Gitea; a running executor keeps the admin api it started with). - N1: an admin rotate cut between the mint and the slot write leaves a token at Gitea that no event names. A `grant.admin rotate-begin {oldName}` before the mint would close it; that is a new event shape. - N2: `adminTokenLeft` is `unknown` when `/api/v1/token` gives no name. Which 1.27 F-fact shows the name needs a check against the container, not a code change. - N3: admin set or remove records `adminTokenLeft: null`, so a left token drops out of status. Carrying it forward lets `rotate --finish` delete by name under a slot of another account or url; refusing remove while one is left blocks the human's remove when Gitea is down. Either needs a ruling. - N4: rotate or remove while a gitea claim is held. Refusing it blocks the human's remove during a run; the running executor keeps the api it began with. Needs a ruling or a line in the README. ## Q188 No test creates a grant except through fixtures or the human's yes on a scratch store. No test reaches git.mosaicstack.dev, tasks.woltje.com or any provider. Unit tests use the fake Gitea on 127.0.0.1; X-G3 uses a throwaway gitea/gitea:1.27.1 container from the local image (never pulled), removed after. Every token is a fake or a scratch-container token; values are compared with `assert.ok`, never printed. ## Runs Sage's CLEAR (sage-2554), 10-22 09:00-09:28Z, one slot script that stops at the first red. Logs are in `scratch/megavolt-r236/logs/` (`slot-2c.txt`, `slot-gate.txt`, `slot-mutants.txt`, `slot-all.txt`). - Section 2c files whole, in the seat worktree, TMPDIR under `/mnt/storage/scratch/tmp`: - bus (gitea-grant, the migration tests, schema, channel-proof, chief-of-staff, grant-host, project-runtime): 131 pass, 0 fail, 0 skipped; - cli (grants, stack-release): 224 pass; - route (grants, access): 28 pass; - docker (gitea-grant-docker): 1 pass. - The whole gate, in a detached worktree at bd5f11ded: - `packages/bus/tests/*.test.mjs` 765 pass, `packages/business/tests/` 228 pass, `packages/cli/tests/*.test.mjs` 832 pass, each 0 fail and 0 skipped; - every `scripts/test-*.sh` rc=0: auth, conductor, config, discord, extension-package, foundation, land-work, queue (156 pass), release, task. - Named skips: test-queue line 34 (queue verify and render --check, because the gate worktree isn't the queue's canonical root), and test-task lines 87 and 93 (live provider cases, row 244: no provider call in the gate). - The gate worktree was clean after the run. GATE_PASSED at 09:23:11Z. ## Mutants (section 9's list, each with the test that kills it; filled in after the runs) Result, at bd5f11ded after the gate (`r236-mutants.sh`): the base (gitea-grant, gitea-migration, grant-migration, schema; cli grants) was green, then all 40 killed and none survived: M1-M37, N9a, N9b and M38. Each mutant's restore left the worktree clean. SLOT_GREEN at 09:28:50Z. A static pass over the 39 mutants (all apply, problems=0) found five with no test to kill them. Four tests were added before any mutant run: - M8, M9 (user or mint step written after its call) and M16 (grant.result written before its call): "X-G6 and X-G8: the user and mint steps are written before their calls, and each result after its call". It reads the store from the fake while user-create and token-create are at Gitea, and checks every result's status. - M29 (fork parent not checked): "X-G4: a fork by the seat user of another repository than the project refuses fork-foreign". - M28 (memory and file revoked after the Gitea delete): "X-G11: a revoke unloads memory and removes the file before its Gitea delete", which holds the DELETE open and reads the file and state. - M37 (evidence without its kind filter): "X-G15: a look-alike spend under another kind with the grant writer pair is not an open grant". r2 tests, each red on 8bf442be9 by reading: - B1: "X-G11: a revoke during a rotation step 7 ends it revoked, and step 8 writes no record back" and "X-G11: a revoke while a sweep settles a rotation, forward or back, ends it revoked with no record". On 8bf442be9 step 8 and the settle write the record and `grant.applied` (forward) or `grant.failed interrupted settled:back` after the revoke deleted it. - B2: "X-G14: a sweep retries a fire only for the user it fired: a remade or gone user is skipped and named". On 8bf442be9 the retry fires the remade user (token list, delete, PATCH) and has no `skipped`. - N5 (r3, sage-2388): "X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not held" and "X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost". On 77dd43c9e they end `grant-token-unknown` (the agent back in tokenUnknown) and `grant-lost`. - Choice 21 (sage-2453): "kill switch (plan :16): while halted the chief still reads grants.status, and an agent grants.revoke refuses". M38 drops `'grants.status'` from the read list; the chief's status then refuses `processing-halted` and the test fails.