Files

12 KiB

Row 207 build notes: 193c, mosaic access user verbs and --level viewer (#1705)

Builder Megavolt, reviewer Duckworth, gate owner Sage (sage-2790, Q220, mos-1301). Branch megavolt/row207 off aca16404c. The brief is "Brief: 193c, mosaic access user verbs and --level viewer" in docs/plans/2026-10-20_human-users-build.md (blob c74af2510). Its gate: Duckworth approves the manifest, the cli suite and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed.

Sources

  • The brief, quoted: "mosaic access user add/remove and grant --level viewer, only where R5 lacks them. They refuse tester and agent callers, write a receipt line and call access.reload (section 8, CLI). If R5 already has all of them, this row records that and closes with no code."
  • Morgana's row 193 design, section 4.5 and section 8 (agents/morgana/work/queue-193/DESIGN.md at e2d161e2e on morgana/row193): user add <id> --home local --subject <provider>:<sub>, user remove <id>, --level viewer on grant, a receipt line in <dataRoot>/state/access-log.jsonl and the access.reload call.
  • R5 as it stands at aca16404c: packages/cli/src/access.mjs, packages/cli/tests/access.test.mjs, packages/business/src/access.mjs (checkAccess, read only here), packages/bus/src/broker.mjs and runtime.mjs.

1. What R5 already has

  • grant --level viewer: present. LEVELS is ["collaborator", "viewer"] (packages/business/src/vocabulary.mjs), grant checks --level against it, and access.test.mjs already grants at viewer. This row adds no code for it and records it here.
  • user add and user remove: absent. accessVerb takes only list, grant and revoke, and the only way to add a user today is to hand-edit access.json. This row adds them.
  • access.reload: absent everywhere. No bus verb has that name. The R5 brief says "R4 later makes these verbs call access.reload, so R5 leaves no other reload path", FED R3's brief leaves it to R4, and access.mjs's proveHuman says "Row R4 swaps this call for access.reload". FED R4 is not filed.

2. Decisions (Q items for Sage and Duckworth, stated as assumptions, not blockers)

  • Q1, the reload call. 193c adds no access.reload call, because there is no verb to call. The user verbs take R5's path exactly: the same project.list human proof, and R4 swaps the proof for access.reload on all five verbs at once. Nothing is lost meanwhile: runtime.mjs hands the broker () => loadAccess(...), and the broker reads the file once per user request (broker.mjs:384), so a written change acts on the next request. If Sage or Duckworth reads the brief as "add access.reload here", that is R4's bus verb and a scope change.
  • Q2, remove with grants. user remove refuses with user-has-grants, exit 2, naming the grant ids, while any grant at any business names the user. The other reading drops the user's grants in the same revision. Refusing is the smaller, fail-closed choice: nothing disappears that the owner did not name, and the help text already says to revoke first. checkAccess would also refuse the file (a grant naming a missing user), but with a key path, not a reason, and exit 3.
  • Q3, the subject. --subject PROVIDER:SUB splits at the first colon, so a sub may hold colons. One subject per add, because parse keeps one value per flag. A second subject is a later row if anyone needs it; checkAccess allows several.
  • Q4, --business. Required on the user verbs too. The user list is instance-wide, but the human proof runs through one business's bus, and the receipt names it.
  • Q5, the receipt. {at, verb: "user.add" | "user.remove", business, user, actor: "human", revisionBefore, revisionAfter, grants: []}: grant and revoke's shape, with an empty grants list.
  • user remove of an id not in the file refuses, exit 2, and writes nothing, as revoke of nothing does.
  • user add of a duplicate id, a reserved id (human, tester), a business's human, an unknown provider, a non-local home, an owner sso subject or a subject another user holds refuses through checkAccess, exit 3, before any write. The verb does not repeat those checks.

Rulings (sage-2810, 16:40Z, cc Duckworth)

  • Q1 agreed: no reload call. The reason is broker.mjs:384-386, the #access field's comment: "() => the checked access document (R1's loadAccess), read once per user request", wired in runtime.mjs:263 as () => loadAccess(access.file, access.context). A write acts on the next request. FED R4 owns any reload verb.
  • Q2 agreed: user remove refuses with user-has-grants, exit 2, naming the grant ids, and writes nothing. Dropping the grants silently would make removal revoke access without saying so.
  • Usage and help pins: grep the tests for the changed strings, sha256 and snapshot pins included, and run every reader whole (section 6).
  • Row 203 (Quackerjack) does not touch access.mjs; its stale help lines stay a follow-up, so no overlap.

The usage and help pin sweep (sage-2810), on aca16404c plus this row

  • Source readers of ACCESS_USAGE and ACCESS_HELP: packages/cli/src/access.mjs only (cli.mjs imports accessCommand and accessContextFor, not the strings).
  • git grep over 'packages//tests/' and 'scripts/*' for ACCESS_USAGE, ACCESS_HELP, "mosaic access", "access list", "access grant", "access revoke", "Access (FED" and the help's "revoke them here first": the only hits are packages/cli/tests/access.test.mjs (positive control: accessCommand hits it twice).
  • Hash pins: no test file names packages/cli/README.md or packages/cli/src/access.mjs next to a sha256 or a 64-hex value. The one 64-hex file that mentions a README (packages/queue/tests/fixtures/queue-marked.md) names packages/queue/README.md and packages/ledger/README.md, not this one. scripts/stack-copy.sh copies packages/cli/README.md; stack-copy.test.mjs builds its own fake repo and is in the cli package, run whole.
  • packages/business/tests/access*.test.mjs import packages/business/src/access.mjs, a different file, unchanged.
  • So the readers are access.test.mjs, run whole in step 1, and the cli package whole in step 2.

3. The change (code and docs)

  • packages/cli/src/access.mjs: two ACCESS_USAGE lines and one help line; accessVerb accepts user add ID and user remove ID (three positionals) and hands off to userVerb after the shared checks (tester, business, proof, file, context, revision). userVerb takes the lock, checks the revision, builds the new users list, runs checkAccess on the whole document, writes atomically and appends the receipt.
  • packages/cli/README.md: the usage block and one paragraph.
  • No change to packages/business, packages/bus or cli.mjs (it already routes access to accessCommand).

4. Tests (packages/cli/tests/access.test.mjs, whole file)

Manifest check against Morgana's row 275 (sage-2790), 10-22: morgana/row275 has no commits past aca16404c, and row 275's brief ("193b1b, viewer verbs and board refusals") is bus only: the viewer verb list, covers level checks and board refusals, gated on the bus suite. This row touches only packages/cli (access.mjs, README, access.test.mjs) and this file, so no path is shared on the briefs. I recheck the paths when Morgana pushes.

  • T1 user add writes the user (id, home, one subject), bumps the revision by one, prints the line and appends one receipt with verb user.add and grants: []. A sub with a colon (authentik:bob:x) keeps bob:x.
  • T2 user add refusals before any write, file and receipts unchanged: missing --home or --subject, a subject with no colon, an empty provider or sub (exit 4); an unknown provider, a duplicate id, tester, the business's human, an owner sso subject, a subject alice already holds, a non-local home (exit 3).
  • T3 user remove of a user with no grants drops them, bumps the revision, receipt user.remove.
  • T4 user remove of a user with a grant refuses user-has-grants naming the grant id, exit 2, nothing written.
  • T5 user remove of an id not in the file refuses, exit 2, nothing written.
  • T6 a stale --revision refuses revision-conflict on both user verbs, exit 3.
  • T7 --tester and an agent env (MOSAIC_RUN_ID) refuse both user verbs, exit 3, and the fake human CLI is never called.
  • T8 a bad shape (user, user add, user list x, user add a b) prints the help, exit 4.
  • T9 a bus that refuses the human proof (the fixture's mode: "refuse") blocks both user verbs: exit 3, the file and receipts unchanged.

5. Mutants

Each runs the whole access.test.mjs and counts as killed only on an assertion in the named test.

  • M1 drop the user-has-grants refusal. Second fence: checkAccess then refuses the file with exit 3, so T4 must assert the user-has-grants text and exit 2, not only "nothing written". Killed by T4.
  • M2 drop the "no user to remove" check. The write then lands with an unchanged user list. Killed by T5.
  • M3 split the subject at the last colon. Killed by T1's colon case.
  • M4 skip checkAccess in userVerb (write the unchecked document). Killed by T2's exit-3 cases.
  • M5 drop the receipt in userVerb. Killed by T1 and T3.
  • M6 drop the revision check in userVerb. Killed by T6.
  • M7 exempt the user verbs from the --tester check (&& sub !== "user"). Killed by T7's tester case.
  • M8 skip proveHuman for the user verbs (if (sub !== "user")). Killed by T9. T7's agent case does not reach it: accessCommand calls refuseInsideAgent before accessVerb, so an agent env is refused whatever the order inside.

6. Gate plan (mos-1275)

On Sage's CLEAR, from a rebase onto 253's landing under a new branch name, stopping at the first red:

  1. access.test.mjs whole, then M1-M8, each under timeout -k 10 180.
  2. The cli package whole.
  3. Every scripts/test-*.sh as timeout -k 10 LIMIT bash scripts/test-X.sh, no script edited. LIMIT is row 281's table (Gander's row 278 limits, 3x the longest of three records plus 120 s): auth 126, conductor 145, config 130, discord 147, extension-package 130, foundation 240, land-work 158, queue 191, release 157, task 231 seconds. The base has the same ten scripts. Any timeout goes to Sage, then Mos, unplaced.

Runs

Gate on 712f79c0af (megavolt/row207-r3, this row's three commits rebased onto 01983cd3f after row 276 landed), Sage's CLEAR sage-3041, 22:07Z to 22:22Z 10-22. Gate worktree /mnt/storage/scratch/megavolt-r207/gate-wt, TMPDIR /mnt/storage/scratch/tmp/gbLURml, logs in /mnt/storage/scratch/megavolt-r207/logs/. Drift f2735bb6a..01983cd3f outside agents/: BUILD-LOG.md, DEFERRED.md, QUEUE.md, queue.json, cli tests case-sudo-fixture.mjs and case1-accept.test.mjs, scripts case1-accept.sh and case2-unit.sh; none is read by access.test.mjs.

  1. access.test.mjs whole: 31 tests, 31 pass, 0 fail, 0 skipped (base.log).
  2. Mutants, each the whole access.test.mjs under timeout -k 10 180, applied by mutate.py (one exact match or refuse), restored by git checkout and checked clean after each. All 8 killed, each failing only its named test: M1 T4, M2 T5, M3 T1, M4 T2, M5 T1 and T3, M6 T6, M7 T7, M8 T9 (M1.log to M8.log).
  3. cli package whole (npm test in packages/cli): 894 tests, 894 pass, 0 fail, 0 skipped, 674 s (cli.log).
  4. The ten scripts, rc 0 each, seconds against the limit: auth 0/126 (15 passed), conductor 7/145 (17), config 1/130 (31), discord 8/147 (81), extension-package 1/130 (18), foundation 43/240 (44), land-work 13/158 (86), queue 26/191 (28), release 3/157 (14), task 14/231 (91). Skips: queue's verify and render --check (the gate worktree is not the canonical root) and task's live provider cases (row 244). test-task printed no Docker-group skip.

Correction: a first run at 22:07Z was stopped after M2. Its kill check grepped TAP not ok, but node prints the spec reporter's ✖ to a file, so it reported M1 SURVIVED while M1 had failed on T4. The check was fixed to ^✖ T4 and M1-M8 were rerun from the start. That run's logs are in logs/run1-stopped/.