S1 candidate for Filbert's review on #1518: build.md, build.patch and
build-manifest.sha256 (34 files at base fef4b362). The candidate itself
is not committed.
proto-v3a-notes-correction: the schema has 36 triggers; the printed 35 is
counted after the tamper check's DROP TRIGGER. Found by Rocko.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
7.9 KiB
Row 36, S1: roles v2, business and project files, variable layers
Darkwing, 2026-10-04. Issue #1518, reviewer Filbert. Brief:
docs/plans/2026-10-04_slice-1.md, section "Slice 1 S1". Design:
agents/darkwing/work/slice1-data-model-2026-10-04.md sections 1 and 2,
addendum A sections 7 and 8, addendum B section 2. Nothing in the
candidate is committed, staged or pushed.
Base is fef4b362 (origin/refactor at build time). In a fresh clone at that
commit the patch applies, the result matches the manifest 34/34, and
node --test packages/business/tests/ passes 57/57 in three runs.
Files
build.patch (sha256
8067882052abc6e75684b318f738ef990f53c7a35913a8313a341282d259bd94) changes
4 files and adds 30. build-manifest.sha256 (sha256
d934e5af30cd646194bbf1d9bffc2b4d51486a4bb4c097ec43376352830b0195) pins all
34 after the patch.
New:
roles/pm.json,cto.json,coder.json,reviewer.json: version 2 definitions.roles/pm.md,cto.md,coder.md,reviewer.md: their duty contracts (Duties, Authority, Protocol).packages/business/src/:vocabulary.mjs(the closed lists),role.mjs(versions 1 and 2),vars.mjs(the key registry and merge),credentials.mjs(reference parse and stat checks),business.mjs,project.mjs,resolve.mjs(resolveInstance,classify),cli.mjs,util.mjs,errors.mjs,index.mjs.packages/business/tests/: 57 tests in 7 files plushelpers.mjs.packages/business/examples/mosaic-stack.example.json: the business file for Sage's SR template. It refuses as shipped (everybotIdis 0, every dateYYYY-MM-DD); a test proves it validates once filled in.packages/business/README.md,package.json.
Changed:
scripts/mosaic-task.mjs:validateRoledelegates to the package, so both versions share one validator.resolve-rolealso printsMOSAIC_ROLE_CONTRACT=<absolute path>for a version 2 role.scripts/test-task.sh: 8 new checks (version 1 prints no contract line; the four shipped roles resolve with their contracts; a minimal version 2 role passes; unknown action, gated-only action, a Vikunja verb no role may hold, a missing contract and a linked contract each exit 2).scripts/mosaic: abusinessbranch, likequeue. This is outside the brief's file list. The brief asks for a TOOLS entry for any new command, and a command needs an entry point; the change is four lines.docs/TOOLS.md: a section forscripts/mosaic business, and theresolve-rolerow now says it takes versions 1 and 2.
Does the vocabulary go under contracts/?
No. contracts/ is baked into the worker image because workers read it.
Nothing in a worker reads the action vocabulary: the broker classifies
actions on the host, and the role files are read on the host by the
launcher and the broker. Baking it in would give the image a copy that
nobody checks against. The vocabulary lives in
packages/business/src/vocabulary.mjs, and a change to it is a reviewed
commit, like a role file. Containerfile copies package.json,
contracts, src and adapters only, so neither packages/ nor
scripts/mosaic-task.mjs is in the image, and the new import doesn't
reach it. No contracts change, so no review by Sage for one.
Choices and deviations from the design
contractis a file name in the role file's own directory ("coder.md"), not the note's"roles/coder.md". A path relative to the repository breaks underMOSAIC_ROLES_DIR, whichagent.shand the tests use for sandbox role directories. The validator refuses a name with a slash, a non-.mdname, a missing, empty or linked file.- pm authority adds three actions to the note's table (section 1.2):
task.update.assigned(within): addendum A section 2 says all four roles update the tasks assigned to them, and the note's pm row left it out.task.reassign(within): addendum A's action table gives reassign to pm, cross-role for the others.task.scope.change(cross): pm splits and rewrites tasks, and a scope change on one in technical work needs the CTO, the technical arbiter. Filbert, push back if you read it differently.
- The business file must belong to the user and not be group or other writable. It holds authority, so a file someone else can edit would let them widen it. Missing file: exit 4. A linked file: exit 4.
- Bot names and bot ids are unique across role instances and the sync bot. Two instances sharing one Vikunja bot would make the tracker's record of who did what wrong.
role.launchstays within-role only for the instance the business file'slaunch.bynames. Everywhere else the resolver removes it, soclassifyreturns gated. With nolaunchblock nobody launches.limits.authorityis an allowlist. A role action it doesn't name becomes gated. It can't add an action the definition lacks.- Credential checks use
lstatandrealpathonly. A test makes a token file mode 0200 (write-only) and the check still passes, which shows the package never opens it. A token inside the repository,dataRootor a project root refuses, also through a linked directory. Vikunja: pastexpiresrefuses, within 7 days warns. Gitea: pastrotateBywarns; Gitea tokens don't expire on their own. Anenvreference that isn't set warns, because the launcher sets it. - Lookups use
Object.hasOwn. Ids may be any lowercase name, andconstructoris one, soroles[name]would have treated it as declared. Tests cover arbiters, launch, the model family and a label titled__proto__. validatetreats a declared project with no project file as a warning ("absent").resolve --projecttreats it as a missing file, exit 4. When a project file exists,validateresolves every instance against it, so a project file naming an undeclared instance refuses there.- Error text for version 1 roles now comes from the package. Every
existing
test-task.shcase still passes; only wording changed. - The package says "role file" for
roles/<name>.json, so it doesn't share a name with the Markdown contract.mosaic-task.mjsandagent.shkeep their old wording ("role contract"); I didn't touch those messages.
Tests and suites
Node 26.8.1 on the host, outputs teed:
node --test packages/business/tests/: 57/57. Files: role 12, vars 6, credentials 6, business 14, project 2, resolve 8, cli 9.scripts/test-config.sh24/24,scripts/test-task.sh98/98,scripts/test-conductor.sh17/17,scripts/test-queue.sh27/27 (scripts/mosaicchanged).
Node 24.21.0 in the node:24 image, no network, the clone mounted
read-only: node --test "packages/business/tests/*.test.mjs" 57/57. The
glob is needed because Node 24 reads node --test tests/ as a module
path and fails. That is true of every package in the repository; I
checked packages/queue the same way. Follow-up for Sage, not part of
S1.
Mutants, each run against the full test set and each caught: the
forbidden-root check off (3 failures), the role.launch gate off (1),
the token mode mask widened to other only (1), hasOwn dropped from the
instance lookup (2), the business file mode check off (1), limit
intersection replaced by overwrite (2).
The CLI tests point MOSAIC_CONFIG, HOME and MOSAIC_ROLES_DIR into a
temp directory. No test reads the real ~/.config.
For other rows
- S2 (Rocko) consumes
loadBusinessandresolveInstance. The frozen API is inpackages/business/README.md, section "API". - S3 (mine) opens token files, so it repeats the stat checks at open time and trims one trailing newline.
- S6 decides how a launcher passes
MOSAIC_ROLE_CONTRACTto the session.agent.shreads onlyMOSAIC_ROLE_TOOLStoday and ignores the new line.
Follow-ups, not in this candidate
node --test <dir>/fails on Node 24 in every package (above).scripts/mosaic-task.mjskeeps its ownSUPPORTED_TOOLSfor tasks. The package has the same list inTOOLS. One should import the other in a later change.