12 KiB
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)", blob9259ba2a1. - Design:
agents/megavolt/work/436b/DESIGN.mdr5 at1be535aca(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.mdat3330c901b(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
86f95bb27after row 273 (sage-2403, no conflicts), then onto0fa7bf421after row 191 (sage-2452, sage-2457). The 191 rebase took three unions in commit 1, 191'sprocessingfirst and then the grant options: the Broker constructor (broker.mjs:317), the runtime's Broker options (runtime.mjs:173) and the cli case line inscripts/mosaic(191'shalt|resume|processing|plusgrants).
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 thegrants.*verbs.packages/bus/src/credentials.mjs:heldpairs at start; the register and claim paths the executor uses.packages/bus/src/migrations/3j-3k.sql,schema.sql,store.mjs:gitea_usersandgitea_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.mjsandstack-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.
- Admin rotation is due once today is past
rotateBy(the date itself is still good). - A seat-token rotation moves the record at step 8, after the old token's delete (step 7).
- A step 7 failure leaves the grant applied, with the old id in
serviceLeftand the notice "The previous token X is still live". - A failure after the commit step's rename returns
settle-pending; the next start settles it. - tester-cli keeps its 5 s timeout; the grant verbs answer at once and the executor runs after.
- A revoke of a token-unknown agent takes both ids from that decision's last commit step.
- A user adopted at restart is upserted in the same transaction as its
grant.failed interrupted. #giteaUnlink(first-grant commit undo) uses a temporary reference dated9999-12-31, so it is never due while it lives.- X-G12's org-team case: the fake models team membership as collaborator reach on the repository, which is
what
/user/reposshows (F11, F12). - X-G15's look-alikes are stored under
action.refusedand under a non-grant kind, since the 3j trigger refuses the human's NULL pair onaction.allowed. The trigger cases of X-G13 (session, NULL pair,grant.host.enrolled) are ingrant-migration.test.mjs. - Step 6 faults: a pre-rename failure is injected by making
gitea.tmpa directory; a step 8 store fault by a TEMP trigger ongitea_grants. - The credentials-verb revoke close is tracked, so
giteaIdle()waits for it. openGiteaGrantsgivesagent: nullfor a spend with no step yet, sostatus.runningcan show a null agent; the CLI prints it empty.- N9: a restart ignores a settle-pending commit with a
grant.revokedafter it and ends itrevokedwith no message (notify false). A 404 on a delete by id counts as done. - N10:
serviceLeftis a list in every body and answer. - 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. - 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. - r2, Dewey B1 (dewey-0781): step 8 and both settle branches (forward, back) check for a
grant.revokedof 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 endsrevokedwith 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. - r2, Dewey B2: a sweep retry of
tokensorloginchecks the user's id first, as the revoke close does, against thegitea_usersrow (it survives a fire). A 404 skips withgitea-user-gone, a 200 with another id withgitea-user-foreign, any other status leaves the entry. Skips go ingrant.sweptand the sweep answer asskipped: [{agent, user, id, reason}]; a skipped entry is not left, so the next sweep does not retry it. - 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, notgrant-lost, and the token-unknown branch neither holds the agent again nor writesgrant-token-unknown, so no start names a file the revoke unlinked. - sage-2453, ruling (a) on megavolt-0123: 191's
#processingGaterefuses every agent verb off its read list while processing is halted.grants.statusis a read likehosts.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'sgrants.revokestays refused while halted, ashosts.revokedoes.
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:
adminTokenLeftisunknownwhen/api/v1/tokengives 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 letsrotate --finishdelete 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.mjs765 pass,packages/business/tests/228 pass,packages/cli/tests/*.test.mjs832 pass, each 0 fail and 0 skipped;- every
scripts/test-*.shrc=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
8bf442be9step 8 and the settle write the record andgrant.applied(forward) orgrant.failed interrupted settled:backafter 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
8bf442be9the retry fires the remade user (token list, delete, PATCH) and has noskipped. - 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
77dd43c9ethey endgrant-token-unknown(the agent back in tokenUnknown) andgrant-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 refusesprocessing-haltedand the test fails.