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]>
154 lines
7.9 KiB
Markdown
154 lines
7.9 KiB
Markdown
# 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 plus `helpers.mjs`.
|
|
- `packages/business/examples/mosaic-stack.example.json`: the business
|
|
file for Sage's SR template. It refuses as shipped (every `botId` is 0,
|
|
every date `YYYY-MM-DD`); a test proves it validates once filled in.
|
|
- `packages/business/README.md`, `package.json`.
|
|
|
|
Changed:
|
|
- `scripts/mosaic-task.mjs`: `validateRole` delegates to the package, so
|
|
both versions share one validator. `resolve-role` also prints
|
|
`MOSAIC_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`: a `business` branch, like `queue`. 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 for `scripts/mosaic business`, and the
|
|
`resolve-role` row 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
|
|
|
|
1. `contract` is 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 under `MOSAIC_ROLES_DIR`, which `agent.sh` and
|
|
the tests use for sandbox role directories. The validator refuses a
|
|
name with a slash, a non-`.md` name, a missing, empty or linked file.
|
|
2. 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.
|
|
3. 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.
|
|
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.
|
|
5. `role.launch` stays within-role only for the instance the business
|
|
file's `launch.by` names. Everywhere else the resolver removes it, so
|
|
`classify` returns gated. With no `launch` block nobody launches.
|
|
6. `limits.authority` is an allowlist. A role action it doesn't name
|
|
becomes gated. It can't add an action the definition lacks.
|
|
7. Credential checks use `lstat` and `realpath` only. 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, `dataRoot` or a
|
|
project root refuses, also through a linked directory. Vikunja: past
|
|
`expires` refuses, within 7 days warns. Gitea: past `rotateBy` warns;
|
|
Gitea tokens don't expire on their own. An `env` reference that isn't
|
|
set warns, because the launcher sets it.
|
|
8. Lookups use `Object.hasOwn`. Ids may be any lowercase name, and
|
|
`constructor` is one, so `roles[name]` would have treated it as
|
|
declared. Tests cover arbiters, launch, the model family and a label
|
|
titled `__proto__`.
|
|
9. `validate` treats a declared project with no project file as a
|
|
warning ("absent"). `resolve --project` treats it as a missing file,
|
|
exit 4. When a project file exists, `validate` resolves every instance
|
|
against it, so a project file naming an undeclared instance refuses
|
|
there.
|
|
10. Error text for version 1 roles now comes from the package. Every
|
|
existing `test-task.sh` case still passes; only wording changed.
|
|
11. The package says "role file" for `roles/<name>.json`, so it doesn't
|
|
share a name with the Markdown contract. `mosaic-task.mjs` and
|
|
`agent.sh` keep 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.sh` 24/24, `scripts/test-task.sh` 98/98,
|
|
`scripts/test-conductor.sh` 17/17, `scripts/test-queue.sh` 27/27
|
|
(`scripts/mosaic` changed).
|
|
|
|
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 `loadBusiness` and `resolveInstance`. The frozen
|
|
API is in `packages/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_CONTRACT` to the session.
|
|
`agent.sh` reads only `MOSAIC_ROLE_TOOLS` today 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.mjs` keeps its own `SUPPORTED_TOOLS` for tasks.
|
|
The package has the same list in `TOOLS`. One should import the other
|
|
in a later change.
|