Compare commits

...
Author SHA1 Message Date
jason.woltje d5428c1a87 row 203 FED R2: cases for missing self, peer id and label (webby-0191 B3)
The pattern test coerces an absent value to the string 'undefined', which
matches, so the typeof halves are the only refusal for a missing value. Adds
a case for each, and M15-M17 in BUILD.md; keeps the config-console-key check
name on purpose. Not run yet.
2026-10-22 15:40:56 -05:00
jason.woltje 8d34a9e948 row 203 FED R2: stack-release probe reads federation; a refusal case per federation check
stack-release config_checks refuses a target whose loader refuses a key the
config holds; it looked only for console, so a federation-only config passed
to a pre-203 copy. The probe now reads console or federation. The fixture's
B loader replace follows the new rejectUnknownKeys list, which the federation
key changed.

Adds a test for each federation refusal in test-config.sh (six uncovered,
plus trailing slash and duplicate peer id), and one stack-release test for
the target-loader refusal. Mutants M7-M14 in BUILD.md. Not run yet.
2026-10-22 15:38:15 -05:00
jason.woltje e704a18aec row 203 FED R2: federation key in the system config (#1701)
The loader validates an optional federation block: self and peers[].id
labels, peers[].origin as an https origin with no path, query or userinfo,
peers[].label non-empty and at most 80 characters, peer ids unique and
distinct from self. Unknown sub-keys refuse. Labels and URLs only; the key
grants no trust. Absent key: config loads with no federation output.

Tests in scripts/test-config.sh cover each refusal and the no-key case.
Notes and mutants M1-M6 in agents/quackerjack/work/queue-203/BUILD.md.
Not run yet: the gate runs on CLEAR.
2026-10-22 15:31:35 -05:00
jason.woltje f2735bb6a1 queue: revs 3317-3324. Row 206: Launchpad claim and in-review (3317-3319), Sage review request (3320-3321), Duckworth approve record (3322), Sage landing note and done (3323-3324). 2026-10-22 15:14:46 -05:00
8 changed files with 586 additions and 14 deletions

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
# Row 203, FED R2: the federation key in the system config (#1701)
Owner Quackerjack, reviewer Webby, gate owner Sage. Brief: docs/plans/2026-10-20_federation-r1-r2-r5.md,
"Brief: FED R2". Design: DESIGN.md section 3.1 (D1), section 4 (the key shape), section 7 row R2, at
gizmoduck/federation-design b93f098f0.
Base: aca16404c, rebased onto f2735bb6a (origin/refactor, shared object store). Rebased onto 253's tip
only if 253 lands first, under a new branch name.
## Commits
- 0eb4b6f1f, rebased to e704a18ae: the loader, the first FED R2 tests, and these notes.
- Second commit on top of e704a18ae (sage-2998): B1 and B2 below, the fixture fix, and the notes
rewrite. Its sha goes in the status line once committed.
## Shape
```json
"federation": {
"self": "hub",
"peers": [{ "id": "factory", "origin": "https://mosaic.woltje.com", "label": "Software factory" }]
}
```
- Optional. Absent: the config loads and prints what it printed before (no `federation` in the output).
- `self` and each `peers[].id` match `^[a-z][a-z0-9-]{0,31}$`. A peer id may not equal `self` or another peer's id.
- `peers[].origin` must be an https origin exactly as `new URL(origin).origin` returns it: no path, no query,
no userinfo, no trailing slash.
- `peers[].label` is a non-empty string of at most 80 characters.
- Labels and URLs only. Nothing here grants trust to a peer (section 3.1 D1, section 3.2 W1).
## Readers of the key
Two readers read the raw key. Nothing else reads it (`git grep "federation"` at e704a18ae):
1. The loader, `scripts/mosaic-config.mjs` (`validate`, the `federation` branch). This is the only reader of
the validated value. Its output is read by the CLI and business code, which read named fields only.
The output is checked in `scripts/test-config.sh`.
2. The stack-release probe, `config_checks` in `scripts/stack-release.sh` (B1, below). It reads the config
file itself, before a copy is activated or rolled back to, and asks whether the target copy's loader
refuses a key the config holds.
Stale wording, NOT changed in this row (Sage rulings sage-2808 and sage-2809):
`packages/cli/src/access.mjs` is out of row 203. Megavolt's row 207 changes that file. Follow-up, to be
carried to DEFERRED by Sage:
- access.mjs lines 26-27 (`ACCESS_HELP` comment): "Peer homes are empty until the federation key lands
(row 203)". Still partly true after this row, since the access context does not read the key.
- access.mjs line 36 (inside the `ACCESS_HELP` string, user-visible help): "Peer homes are not read yet
(row 203 adds the federation key)". The same claim, in help text. No test in `packages/cli/tests` pins it
(`git grep` for "row 203" and "Peer homes" finds nothing in tests).
- access.mjs line 44 comment: "peers is empty until row 203".
- `packages/cli/README.md` line 1107: "until row 203 adds the". Outside the manifest; flagged in the
same follow-up.
Behavior after this row: `accessContextFor` still returns `peers: []` (access.mjs line 58). Row 203 adds the
config key only, so grants naming a peer home still refuse. Wiring the key into the access context is not in
the FED R2 brief.
## B1: the stack-release probe (sage-2998, in scope)
Webby's early look (webby-0189): a config holding `federation` and no `console` passed `config_checks`, because
the probe looked only for `console`. A rollback or activate to a pre-203 copy would then pass its checks, and
that copy's loader refuses `unsupported configuration key: "federation"` on the next read.
Fix (scripts/stack-release.sh, `config_checks`):
- The probe's node check now reads `console` or `federation` (`Object.hasOwn(d,"console")||Object.hasOwn(d,"federation")`).
- The refusal keeps the check name `config-console-key`, so existing callers and tests do not change. The
message now reads "holds a console or federation key". This is a wording change to an existing refusal,
and `KEY_MSG` in `packages/cli/tests/stack-release.test.mjs` moves with it.
- The new test is `row 203: a config whose federation key the target loader refuses ...` in
`packages/cli/tests/stack-release.test.mjs`, after the row 200 console test. Target B's loader has no
federation key in its list, so it refuses the key. Activate to B and rollback to B are both refused, and
a rollback succeeds once the key is removed.
Fixture (packages/cli/tests/stack-release-fixture.mjs, line 300): `LOADER_OLD` builds B's loader by a string
replace on the `rejectUnknownKeys` list. Adding `federation` to that list in this row left the replace with no
match, and the fixture's `assert.notEqual(LOADER_OLD, LOADER)` would fail at load. The replace now targets
`"execution", "console", "federation"]`, so B's loader refuses both keys. This regression came from my
e704a18ae, and the fix is in this commit.
## B2: a case and a mutant for each refusal (sage-2998: "one mutant per refusal")
Webby's list is covered below. Each refusal has a case in `scripts/test-config.sh` (FED R2 block) and a
mutant. M5 is killed by the self case; M13 covers the duplicate half of the id check that M5 did not reach.
## Refusals and their tests
`scripts/test-config.sh`, FED R2 block:
| Case | Refusal (exit 2) | Mutant (killed by) |
|---|---|---|
| valid key loads and is in the output | none | base |
| unknown top-level sub-key | `unsupported federation key: "extra"` | M1 |
| unknown peer sub-key | `unsupported federation.peers[0] key: "trust"` | M2 |
| origin over http | `federation.peers[0].origin must be an https origin ...` | M3 |
| origin with a path | same | M4 |
| origin with userinfo | same | M4 |
| origin with a trailing slash | same | M4 |
| peer id equal to self | `federation.peers[0].id ... is already named` | M5 |
| duplicate peer id | `federation.peers[1].id "factory" is already named` | M13 |
| peer label empty | `federation.peers[0].label must be a non-empty string ...` | M6 |
| peer label over 80 characters | `federation.peers[0].label must be ...` | M12 |
| federation not an object | `"federation" must be a JSON object` | M7 |
| self does not match the pattern | `federation.self must match ...` | M8 |
| peers not an array | `federation.peers must be an array` | M9 |
| peer not an object | `federation.peers[0] must be a JSON object` | M10 |
| peer id does not match the pattern | `federation.peers[0].id must match ...` | M11 |
| self missing | `federation.self must match ...` | M15 |
| peer id missing | `federation.peers[0].id must match ...` | M16 |
| peer label missing | `federation.peers[0].label must be ...` | M17 |
| no federation key | loads, no federation output | base |
| B1: target loader refuses federation | `config-console-key` refusal (stack-release.test) | M14 |
Mutant plan (run on Sage's gate only, one edit each, a named test must fail). Before the gate each edit string
must occur exactly once in its file. I checked all of M1 to M17 with grep -cF: each is 1. The
gate's mutant runner checks the count again before it applies each edit:
- M1: `rejectUnknownKeys(value, ["self", "peers"], "federation");` removed. Killed by "federation unknown sub-key".
- M2: `rejectUnknownKeys(peer, ["id", "origin", "label"], where);` removed. Killed by "federation peer unknown sub-key".
- M3: `url.protocol !== "https:" ||` removed. Killed by "origin over http".
- M4: `|| url.origin !== value` removed. Killed by "origin with a path", "origin with userinfo" and
"origin with a trailing slash".
- M5: `if (ids.has(peer.id)) {` changed to `if (false) {`. Killed by "peer id equal to self".
- M6: `peer.label.trim() === "" ||` removed. Killed by "peer label empty".
- M7: `if (!isPlainObject(value)) {` changed to `if (false) {`. Killed by "federation not an object".
- M8: `|| !FEDERATION_ID_PATTERN.test(value.self)` removed. Killed by "self does not match the pattern".
- M9: `if (!Array.isArray(value.peers)) {` changed to `if (false) {`. Killed by "peers not an array".
- M10: `if (!isPlainObject(peer)) {` changed to `if (false) {`. Killed by "peer not an object".
- M11: `|| !FEDERATION_ID_PATTERN.test(peer.id)` removed. Killed by "peer id does not match the pattern".
- M12: ` || peer.label.length > FEDERATION_LABEL_MAX` removed. Killed by "peer label over 80 characters".
- M13: ` ids.add(peer.id);` removed. Killed by "duplicate peer id".
- M14 (stack-release.sh): `||Object.hasOwn(d,"federation")` removed. Killed by the row 203 federation test in
`stack-release.test.mjs`.
- M15: `typeof value.self !== "string" || ` removed. Killed by "federation self missing". Without the typeof
half, an absent self passes the pattern (`/^[a-z]…$/.test(undefined)` is true).
- M16: `typeof peer.id !== "string" || ` removed. Killed by "federation peer id missing". Same reason.
- M17: `typeof peer.label !== "string" || ` removed. Killed by "federation peer label missing". Without it, a
missing label throws a TypeError at `.trim()` (exit 1), not the exit 2 refusal.
## Boundaries
- No config is written. `~/.config/mosaic-dev/config.json` is not touched. Tests use `$SANDBOX` only.
- No change to `packages/cli/src`, `scripts/bootstrap.sh`, or any reader beyond the stack-release probe.
## Manifest (sha256 of committed blobs; the list is the review scope)
- scripts/mosaic-config.mjs
- scripts/test-config.sh
- scripts/stack-release.sh (B1)
- packages/cli/tests/stack-release.test.mjs (B1 test, KEY_MSG)
- packages/cli/tests/stack-release-fixture.mjs (B's loader fixture; without this, the suite fails to load)
- agents/quackerjack/work/queue-203/BUILD.md
## Slot estimate (for Sage's gap)
- test-config.sh: about 5 min, padded.
- The cli suite: about 5 min, padded.
- stack-release.test.mjs: up to 1800 s, per Sage. It copies the loader, so it runs whole at the gate (Webby N3).
- Mutants M1–M13 and M15–M17 each re-run the FED block of test-config.sh. M14 re-runs stack-release.test.mjs.
The per-mutant cost is not measured: this run has not been timed.
## Status
- e704a18ae: the loader, first tests and notes, rebased onto f2735bb6a.
- 8d34a9e94: B1, B2, the fixture fix and the notes rewrite (Mos approved the two old-test edits, mos-1346).
- Third commit on top: B3 cases M15–M17 and these notes (sha in the message to Sage and Webby).
- NOT YET RUN: no node during Gander's slot. The test-config.sh, cli and stack-release gates run when Sage
announces a CLEAR. `bash -n` passes on both shell scripts.
- Not pushed, not claimed. Per invariant 8 the gate runs before any commit is called green.
## Review notes (Webby, webby-0189, answered here)
- B1: fixed as above (scope per sage-2998). The probe's check name `config-console-key` is kept on purpose (Webby
N5, Sage): renaming it would change callers and tests for no gain in this row. The message says "console or
federation key"; the name is the only old word left.
- B3 (webby-0191): missing self, missing peer id, and missing peer label each have a case and a mutant (M15–M17).
The typeof halves are the only thing refusing an absent value, since the pattern test coerces undefined.
- B2: a case and a mutant for each uncovered refusal, plus the trailing-slash case under M4 (N4) and the
duplicate-peer case (M13).
- N1: this file is rewritten with the current base and status.
- N2: consumers checked by `git grep "mosaic-config"`. The loader's output is read by `packages/cli/src/config.mjs`,
`packages/business/src/*.mjs`, `packages/webui/src/sso-config.mjs`, `packages/seat/src/seat.mjs`,
`packages/discord/src/binding.mjs`, `packages/conversation/src/guard.mjs`, and the shell scripts that call
`validate` (bootstrap.sh, common.sh, instance.sh, land-work.sh, stack-copy.sh, stack-release.sh,
conductor-apply.sh). Checked by `git grep "federation"` over packages and scripts: no reader outside the
loader and the stack-release probe mentions `federation`. I did NOT read each call site for which fields it
uses; that is still open for Webby's static read. Not run.
- N3: the cli stack-release tests copy the loader; they run whole at the gate. Listed in the manifest and
the slot estimate.
- N4: trailing-slash case added (see table; M4).
+2 -2
View File
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
<!-- mosaic-queue:begin -->
Generated from `docs/plans/queue.json` revision 3316 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
Generated from `docs/plans/queue.json` revision 3324 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|---|---|---|---|---|---|---|---|---|
@@ -242,7 +242,7 @@ Generated from `docs/plans/queue.json` revision 3316 by `queue render`. Do not e
| 203 | FED R2: the federation key in the system config (#414) | quackerjack; reviewers webby | #1701 | briefed | — | Webby approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R2, the federation key in the system config | Chained on row 192's candidate (Q177): starts on 192's candidate, lands after 192 (Sage) |
| 204 | 193a: access level check widened to viewer (#1691) | morgana; reviewers duckworth | #1702 | done | — | Duckworth approves the manifest; business suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193a, the access level check widened to viewer | Landed d0bbc5c1c on refactor (base c9f374284). Patch-id 92c18aea matches git diff 44e36fc4c..f869fd062 on the 3 manifest paths. Manifest d38de8c9, Duckworth approve 29595. Gate 2789/0 on 4385b9ec1, mutants 7/7. Unpushed. |
| 205 | 193b1a: the message filter (#1703; FED R3 and 193b1b build on it) | morgana; reviewers duckworth | #1703 | done | 196 done | Duckworth approves the manifest; bus suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b1a, the message filter | Landed ce6a96ad6 fast-forward on 0fa7bf421 (3 commits, 4 paths, manifest 858463b1). Duckworth APPROVE r1 c30375 (signed :124 test diff, mos-1262). Gate: Morgana slot205 10 files, bus 652/0, M1 M11-M14 killed, 0 skips; Sage test-*.sh 10/10, 3 by-design skips. N1/N3 to 205b or R3, N2 to 193b2 brief, N4 = #1782 before any user thread. Rebase next: 268 r4, 194, 236, 253, 274. |
| 206 | 193b2: thread shares, schema 3k-3l after the #436b build (#1691; live needs a --migrate window with Jason) | launchpad; reviewers duckworth | #1704 | briefed | 205 done, 234 done, 236 done, 275 done | Duckworth approves the manifest; bus suite, cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b2, thread shares (schema 3l) | — |
| 206 | 193b2: thread shares, schema 3k-3l after the #436b build (#1691; live needs a --migrate window with Jason) | launchpad; reviewers duckworth | #1704 | done | 205 done, 234 done, 236 done, 275 done | Duckworth approves the manifest; bus suite, cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b2, thread shares (schema 3l) | Landed 0cbe2e9b3 fast-forward on 17f415792 (10 commits, 22 paths), pushed 19:47Z. Duckworth APPROVE rev 3322 c30795 on 0cbe2e9b3; request rev 3320/3321 c30793 posted as jarvis. Gate (Launchpad, 9538e0fc8, tip differs in BUILD.md only): bus 850/0, cli 869/0, ten scripts/test-*.sh rc=0, touched files whole green, M2-M4 M7-M9 and Q2 killed. One placed fixture red (mos-1272). Schema 3l to 3m; live --migrate is Mos's go under Q173. |
| 207 | 193c: mosaic access user verbs and --level viewer (#1691) | megavolt; reviewers duckworth | #1705 | in-progress | 202 done | Duckworth approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193c, mosaic access user verbs and --level viewer | — |
| 208 | 193d: Console shared list, share controls and viewer read-only (#1691; also after FED R6 when filed) | morgana; reviewers duckworth | #1706 | queued | 192 done, 206 done | Duckworth approves the manifest; webui suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193d, the Console's shared list, share controls and viewer read-only | — |
| 209 | FED R8: the hub window on 10.1.1.54, a recorded acceptance (#414, r4.11 b93f098f0; live window run by Jason and Mos, after the Case 2 install #416, Bean and Chet live and both Hermes gateways stopped) | sage; reviewers duckworth | — | queued | — | Every entry check's output is in the R8 record with who ran it; Jason signs in to the hub and sends one message, Melanie revokes, his next request is refused; Duckworth checks the record (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R8, the hub window on 10.1.1.54 | Stays queued until its entry gates near: Case 2 install on .54 (#416, by 2026-10-27), Bean and Chet live, both Hermes gateways on .54 stopped, R7, #412 phase 2, #419 answer. Filed per mos-1073 after duckworth-0034 (Sage) |
+250 -5
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"canonicalRoot": "/mnt/storage/src/mosaic-stack",
"revision": 3316,
"revision": 3324,
"rows": [
{
"id": 1,
@@ -21914,7 +21914,7 @@
"closes": [
1704
],
"state": "briefed",
"state": "done",
"previousState": null,
"required": false,
"requiredSince": null,
@@ -21946,12 +21946,69 @@
"reviewers": [
"duckworth"
],
"review": null,
"review": {
"rounds": [
{
"n": 1,
"op": "launchpad-inreview-206",
"by": "launchpad",
"at": "2026-10-22T19:43:00.265Z",
"issue": 1704,
"candidate": {
"kind": "commit",
"digest": "0cbe2e9b35e51fe06dedde69c022b46e040f98c0",
"text": null
},
"request": "comment",
"attempts": [
{
"op": "launchpad-inreview-206",
"by": "launchpad",
"at": "2026-10-22T19:43:00.265Z",
"state": "failed",
"comment": null,
"transport": {
"outcome": "failed",
"status": null,
"comment": null,
"detail": "pre-send: the credential or account check failed"
},
"resolutions": []
},
{
"op": "sage-206-reviewreq",
"by": "sage",
"at": "2026-10-22T19:44:34.153Z",
"state": "posted",
"comment": 30793,
"transport": {
"outcome": "posted",
"status": 201,
"comment": 30793,
"detail": "created"
},
"resolutions": []
}
],
"duplicateRisk": false,
"receipts": [
{
"reviewer": "duckworth",
"op": "duckworth-0197-record",
"at": "2026-10-22T19:47:02.039Z",
"verdict": "approve",
"comment": 30795,
"candidate": "0cbe2e9b35e51fe06dedde69c022b46e040f98c0"
}
]
}
]
},
"claim": null,
"note": null,
"note": "Landed 0cbe2e9b3 fast-forward on 17f415792 (10 commits, 22 paths), pushed 19:47Z. Duckworth APPROVE rev 3322 c30795 on 0cbe2e9b3; request rev 3320/3321 c30793 posted as jarvis. Gate (Launchpad, 9538e0fc8, tip differs in BUILD.md only): bus 850/0, cli 869/0, ten scripts/test-*.sh rc=0, touched files whole green, M2-M4 M7-M9 and Q2 killed. One placed fixture red (mos-1272). Schema 3l to 3m; live --migrate is Mos's go under Q173.",
"blockedReason": null,
"createdAt": "2026-10-20T19:44:46.533Z",
"updatedAt": "2026-10-22T18:19:43.067Z",
"updatedAt": "2026-10-22T19:48:42.438Z",
"updatedBy": "sage"
},
{
@@ -116211,6 +116268,194 @@
"receipt": "ok sage-282-done rev 3316 row 282 in-review→done round 1"
},
"viewSha": "831a19048c014068fcfd43938e80ad6759140e6feec2a9520a7763bfecb1bfa3"
},
{
"rev": 3317,
"op": "launchpad-claim-206",
"verb": "move",
"args": {
"id": 206,
"to": "in-progress",
"reason": null,
"candidate": null,
"evidence": null,
"issue": null
},
"by": "launchpad",
"at": "2026-10-22T19:16:48.380Z",
"semantics": 2,
"result": {
"row": 206,
"from": "briefed",
"to": "in-progress",
"round": null,
"issue": null,
"candidate": null,
"receipt": "ok launchpad-claim-206 rev 3317 row 206 briefed→in-progress"
},
"viewSha": "e9b2e2dd36e17c5af605a08fa2bf3e8658fd9175e61f7dc16f479dd8a26c6a99"
},
{
"rev": 3318,
"op": "launchpad-inreview-206",
"verb": "move",
"args": {
"id": 206,
"to": "in-review",
"reason": null,
"candidate": "0cbe2e9b3",
"evidence": null,
"issue": null
},
"by": "launchpad",
"at": "2026-10-22T19:43:00.265Z",
"semantics": 2,
"result": {
"row": 206,
"from": "in-progress",
"to": "in-review",
"round": 1,
"issue": 1704,
"candidate": {
"kind": "commit",
"digest": "0cbe2e9b35e51fe06dedde69c022b46e040f98c0",
"text": null
},
"receipt": "ok launchpad-inreview-206 rev 3318 row 206 in-progress→in-review round 1 on #1704; request launchpad-inreview-206 requesting"
},
"viewSha": "8c075a43b38bbe10b41d73d57ada36ad08cf513f75bf994fd821f9c5fbe623fd"
},
{
"rev": 3319,
"op": "launchpad-inreview-206.outcome",
"verb": "review-outcome",
"args": {
"id": 206,
"attempt": "launchpad-inreview-206",
"outcome": "failed",
"status": null,
"comment": null,
"detail": "pre-send: the credential or account check failed"
},
"by": "launchpad",
"at": "2026-10-22T19:43:39.640Z",
"semantics": 2,
"result": {
"row": 206,
"round": 1,
"attempt": "launchpad-inreview-206",
"state": "failed",
"receipt": "ok launchpad-inreview-206.outcome rev 3319 row 206 round 1 request launchpad-inreview-206 failed"
},
"viewSha": "1ba9ac82e2e6f3df7b582addbf27f540d4e0a1bdfedc678fc2da284a5e5a10ae"
},
{
"rev": 3320,
"op": "sage-206-reviewreq",
"verb": "review-request",
"args": {
"id": 206
},
"by": "sage",
"at": "2026-10-22T19:44:34.153Z",
"semantics": 2,
"result": {
"row": 206,
"round": 1,
"attempt": "sage-206-reviewreq",
"state": "requesting",
"receipt": "ok sage-206-reviewreq rev 3320 row 206 round 1 request sage-206-reviewreq requesting"
},
"viewSha": "e11aeb1b454174a017aa5601aa42616d28da94c5941a12e883d0901adb9ca3ee"
},
{
"rev": 3321,
"op": "sage-206-reviewreq.outcome",
"verb": "review-outcome",
"args": {
"id": 206,
"attempt": "sage-206-reviewreq",
"outcome": "posted",
"status": 201,
"comment": 30793,
"detail": "created"
},
"by": "sage",
"at": "2026-10-22T19:45:14.120Z",
"semantics": 2,
"result": {
"row": 206,
"round": 1,
"attempt": "sage-206-reviewreq",
"state": "posted",
"receipt": "ok sage-206-reviewreq.outcome rev 3321 row 206 round 1 request sage-206-reviewreq posted comment 30793"
},
"viewSha": "e65ea7cd31917d3fa4dbafb99d29ab57708ea267b1a695186a024c3704913161"
},
{
"rev": 3322,
"op": "duckworth-0197-record",
"verb": "review-record",
"args": {
"id": 206,
"verdict": "approve",
"comment": 30795,
"candidate": "0cbe2e9b35e51fe06dedde69c022b46e040f98c0"
},
"by": "duckworth",
"at": "2026-10-22T19:47:02.039Z",
"semantics": 2,
"result": {
"row": 206,
"round": 1,
"verdict": "approve",
"receipt": "ok duckworth-0197-record rev 3322 row 206 round 1 approve by duckworth (comment 30795)"
},
"viewSha": "64464a3283e6bc7ddf8bc9b92a3f8048a7d484af4bf6ba48118361b5a1db751d"
},
{
"rev": 3323,
"op": "sage-206-land-note",
"verb": "note",
"args": {
"id": 206,
"text": "Landed 0cbe2e9b3 fast-forward on 17f415792 (10 commits, 22 paths), pushed 19:47Z. Duckworth APPROVE rev 3322 c30795 on 0cbe2e9b3; request rev 3320/3321 c30793 posted as jarvis. Gate (Launchpad, 9538e0fc8, tip differs in BUILD.md only): bus 850/0, cli 869/0, ten scripts/test-*.sh rc=0, touched files whole green, M2-M4 M7-M9 and Q2 killed. One placed fixture red (mos-1272). Schema 3l to 3m; live --migrate is Mos's go under Q173."
},
"by": "sage",
"at": "2026-10-22T19:48:02.970Z",
"semantics": 2,
"result": {
"row": 206,
"field": "note",
"receipt": "ok sage-206-land-note rev 3323 row 206 note"
},
"viewSha": "4e7466cfd72ecd54f6e1e0b20fef41c22fab648a49db7e13a22b7a0ba935f43a"
},
{
"rev": 3324,
"op": "sage-206-done",
"verb": "move",
"args": {
"id": 206,
"to": "done",
"reason": null,
"candidate": null,
"evidence": null,
"issue": null
},
"by": "sage",
"at": "2026-10-22T19:48:42.438Z",
"semantics": 2,
"result": {
"row": 206,
"from": "in-review",
"to": "done",
"round": 1,
"issue": null,
"candidate": null,
"receipt": "ok sage-206-done rev 3324 row 206 in-review→done round 1"
},
"viewSha": "7de3f3a2fffaf8a95d3f8cb69179d07a2def45c85f4b5a4dd1a6586b2590c44d"
}
]
}
+3 -2
View File
@@ -295,9 +295,10 @@ export const KINDS_OLD=KINDS_NEW.replace('\n harness: Object.freeze({','\n "ha
assert.notEqual(KINDS_OLD,KINDS_NEW);
// Row 200 (#1698): C1 and C take --sso and have the row 192 loader and checkConsole; C's checker
// refuses RS256, a stand-in for a target whose rules differ from the repository's. B has the loader
// from before row 192, which refuses a `console` key. The other copies have no loader.
// from before row 192, which refuses a `console` key. The other copies have no loader. Row 203: B's
// loader also refuses a `federation` key, as a pre-203 loader does (its list has no federation).
const LOADER=readFileSync(join(repo,'scripts','mosaic-config.mjs'),'utf8'),SSO_CONFIG=readFileSync(join(repo,'packages','webui','src','sso-config.mjs'),'utf8');
const LOADER_OLD=LOADER.replace('["configVersion", "environment", "dataRoot", "execution", "console"]','["configVersion", "environment", "dataRoot", "execution"]');
const LOADER_OLD=LOADER.replace('["configVersion", "environment", "dataRoot", "execution", "console", "federation"]','["configVersion", "environment", "dataRoot", "execution"]');
const SSO_CONFIG_C=SSO_CONFIG.replace("'ES256', 'RS256',","'ES256',");
assert.notEqual(LOADER_OLD,LOADER);assert.notEqual(SSO_CONFIG_C,SSO_CONFIG);
const ssoFiles=(id)=>id===B?[['scripts/mosaic-config.mjs',LOADER_OLD]]:[C1,C].includes(id)?[['scripts/mosaic-config.mjs',LOADER],['packages/webui/src/sso-config.mjs',id===C?SSO_CONFIG_C:SSO_CONFIG]]:[];
+14 -1
View File
@@ -1468,7 +1468,7 @@ const SHOW=`show-${CON}.txt`;
const setOf=(e,act)=>join(e.state,'stack-rollback',act);
const arrivalSet=(e,to)=>setOf(e,logLines(e).findLast((l)=>l.result==='ok'&&l.to===to&&l.from!==to).activation);
const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --tester-port as the pointer has it`;
const KEY_MSG=(e,to,op,why)=>`${e.cfg} holds a console key, and ${to}'s loader refuses it (${why}); remove the key first (restore the config.json backup from the window's step 1), then run the ${op} again`;
const KEY_MSG=(e,to,op,why)=>`${e.cfg} holds a console or federation key, and ${to}'s loader refuses it (${why}); remove the key first (restore the config.json backup from the window's step 1), then run the ${op} again`;
const RB_MSG=(why)=>`${B}'s Console does not support --sso, and the Console flags it ran with cannot be read: ${why}`;
// Rewrites a file in a rollback set (0700, files kept by cp -p) and, unless sums is false, its SHA256SUMS line.
function setEdit(set,name,from,to,sums=true){
@@ -1582,6 +1582,19 @@ test('T57 row 200: activate --sso, its refusals before any stop, and the rollbac
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,B);
await noLeak(e);
});
await t.test('row 203: a config whose federation key the target loader refuses stops an activate or a rollback before any stop, naming the first step',async(t)=>{
const e=await mk(t);await seedPreview(e);
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
// B's loader has no federation key in its list, as a pre-203 loader has none: it refuses the key.
const setFed=(fed)=>{const c=JSON.parse(readFileSync(e.cfg,'utf8'));if(fed===undefined)delete c.federation;else c.federation=fed;writeFileSync(e.cfg,JSON.stringify(c))};
setFed({self:'hub',peers:[{id:'factory',origin:'https://mosaic.woltje.com',label:'Software factory'}]});
const why='mosaic-config: unsupported configuration key: "federation"';
await refused(e,['activate',B,'acme'],'config-console-key',KEY_MSG(e,B,'activate',why));
await refused(e,['rollback','acme'],'config-console-key',KEY_MSG(e,B,'rollback',why));
setFed(undefined);
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,B);
await noLeak(e);
});
await t.test('item 3: a rollback from --sso to a Console without it takes the flags from the set of the activation onto the copy, past a same-copy re-pin (sage-1389)',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
+58 -1
View File
@@ -237,13 +237,66 @@ async function validateConsole(value) {
}
}
// FED R2 (#1701, DESIGN.md section 3.1 D1): the optional `federation` key names this instance and its
// peers. Ids and origins are labels, not trust: nothing here grants a peer anything (3.1, W1). The
// origin must be exactly an https origin, the form `new URL(origin).origin` returns, so a path, a
// trailing slash, a port that is the default, or userinfo refuses.
const FEDERATION_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
const FEDERATION_LABEL_MAX = 80;
function validateFederationOrigin(value, where) {
let url = null;
try {
url = new URL(value);
} catch {
url = null;
}
if (url === null || url.protocol !== "https:" || url.origin !== value) {
fail(2, `${where} must be an https origin with no path, query, or userinfo (got ${JSON.stringify(value)})`);
}
}
function validateFederation(value) {
if (!isPlainObject(value)) {
fail(2, '"federation" must be a JSON object');
}
rejectUnknownKeys(value, ["self", "peers"], "federation");
if (typeof value.self !== "string" || !FEDERATION_ID_PATTERN.test(value.self)) {
fail(2, `federation.self must match ${FEDERATION_ID_PATTERN} (got ${JSON.stringify(value.self)})`);
}
if (!Array.isArray(value.peers)) {
fail(2, 'federation.peers must be an array');
}
const ids = new Set([value.self]);
const peers = value.peers.map((peer, index) => {
const where = `federation.peers[${index}]`;
if (!isPlainObject(peer)) {
fail(2, `${where} must be a JSON object`);
}
rejectUnknownKeys(peer, ["id", "origin", "label"], where);
if (typeof peer.id !== "string" || !FEDERATION_ID_PATTERN.test(peer.id)) {
fail(2, `${where}.id must match ${FEDERATION_ID_PATTERN} (got ${JSON.stringify(peer.id)})`);
}
if (ids.has(peer.id)) {
fail(2, `${where}.id ${JSON.stringify(peer.id)} is already named (by federation.self or an earlier peer)`);
}
ids.add(peer.id);
validateFederationOrigin(peer.origin, `${where}.origin`);
if (typeof peer.label !== "string" || peer.label.trim() === "" || peer.label.length > FEDERATION_LABEL_MAX) {
fail(2, `${where}.label must be a non-empty string of at most ${FEDERATION_LABEL_MAX} characters`);
}
return { id: peer.id, origin: peer.origin, label: peer.label };
});
return { self: value.self, peers };
}
async function validate(document, file) {
if (!isPlainObject(document)) {
fail(2, "configuration must be a JSON object");
}
rejectUnknownKeys(
document,
["configVersion", "environment", "dataRoot", "execution", "console"],
["configVersion", "environment", "dataRoot", "execution", "console", "federation"],
"configuration",
);
@@ -281,6 +334,9 @@ async function validate(document, file) {
// it, so a config without it prints what it always printed.
const consoleKey = Object.hasOwn(document, "console") ? await validateConsole(document.console) : undefined;
// FED R2: the same rule as `console`. The key appears in the output only when the file has it.
const federationKey = Object.hasOwn(document, "federation") ? validateFederation(document.federation) : undefined;
return {
configVersion: document.configVersion,
environment: document.environment,
@@ -292,6 +348,7 @@ async function validate(document, file) {
adapter,
},
...(consoleKey === undefined ? {} : { console: consoleKey }),
...(federationKey === undefined ? {} : { federation: federationKey }),
};
}
+3 -3
View File
@@ -1078,12 +1078,12 @@ slot_problem() {
# target's own scripts/mosaic-config.mjs validate; its output, the resolved config, is not kept.
config_checks() {
local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first
# 0: a console key; 1: no key, or no file; 2: the file doesn't parse, so whether it holds the key
# 0: a console or federation key (row 203: a pre-203 loader refuses federation too); 1: neither, or no file; 2: the file doesn't parse, so whether it holds the key
# is unknown, and the run refuses rather than pass it (webby-0028 N2). load_config already exits on
# such a file before anything is read or stopped, so this is reached only if the file changes after it.
local key=0
node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' -- "$cfg" 2>/dev/null || key=$?
process.exit(d!==null&&typeof d==="object"&&(Object.hasOwn(d,"console")||Object.hasOwn(d,"federation"))?0:1)' -- "$cfg" 2>/dev/null || key=$?
[ "$key" != 1 ] || return 0
if [ "$key" != 0 ]; then
check_refuse config-console-key "$cfg cannot be read as JSON, so whether $TO's loader reads it is unknown; fix the config first, then run the $OP again"
@@ -1093,7 +1093,7 @@ process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' --
elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0
fi
first="${out%%$'\n'*}"
check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
check_refuse config-console-key "$cfg holds a console or federation key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
}
# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose
+66
View File
@@ -191,6 +191,72 @@ expect_refusal "console.sso slot name outside the pattern exits 2 naming the pat
expect_refusal "unknown key under console exits 2 naming the path" \
"$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider)"']},"extra":1}')" 'console.extra is not a supported key'
# --- FED R2 (#1701, DESIGN.md section 3.1): the optional federation key ---
# fed_body FEDERATION-JSON: a config whose federation key is the argument.
fed_body() {
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m"},"federation":%s}' "$DATA_ROOT" "$1"
}
FED_VALID='{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"Software factory"}]}'
cfg fed-valid.json "$(fed_body "$FED_VALID")"
OUT="$(MOSAIC_CONFIG="$SANDBOX/fed-valid.json" timeout 20 $CONFIG_OP validate)"
[ $? -eq 0 ] && printf '%s' "$OUT" | grep -q '"self": "hub"' && printf '%s' "$OUT" | grep -q '"origin": "https://mosaic.woltje.com"'
check "valid federation key loads and is in the output" $?
# expect_fed_refusal NAME PATH-IN-MESSAGE FEDERATION-JSON: exit 2, and the message names the bad key path.
expect_fed_refusal() {
cfg fed-bad.json "$(fed_body "$3")"
local err rc
err="$(MOSAIC_CONFIG="$SANDBOX/fed-bad.json" timeout 20 $CONFIG_OP validate 2>&1 >/dev/null)"
rc=$?
[ "$rc" -eq 2 ] && printf '%s' "$err" | grep -qF -- "$2"
check "$1 (exit $rc)" $?
}
expect_fed_refusal "federation unknown sub-key exits 2 naming the key" 'unsupported federation key: "extra"' \
'{"self":"hub","peers":[],"extra":1}'
expect_fed_refusal "federation peer unknown sub-key exits 2 naming the key" 'unsupported federation.peers[0] key: "trust"' \
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"F","trust":"full"}]}'
expect_fed_refusal "federation peer origin over http exits 2 naming the key" "federation.peers[0].origin" \
'{"self":"hub","peers":[{"id":"factory","origin":"http://mosaic.woltje.com","label":"F"}]}'
expect_fed_refusal "federation peer origin with a path exits 2 naming the key" "federation.peers[0].origin" \
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com/console","label":"F"}]}'
expect_fed_refusal "federation peer origin with userinfo exits 2 naming the key" "federation.peers[0].origin" \
'{"self":"hub","peers":[{"id":"factory","origin":"https://[email protected]","label":"F"}]}'
expect_fed_refusal "federation peer id equal to self exits 2 naming the key" "federation.peers[0].id" \
'{"self":"hub","peers":[{"id":"hub","origin":"https://mosaic.woltje.com","label":"F"}]}'
expect_fed_refusal "federation peer label empty exits 2 naming the key" "federation.peers[0].label" \
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":""}]}'
expect_fed_refusal "federation not an object exits 2 naming the key" '"federation" must be a JSON object' '"hub"'
expect_fed_refusal "federation self pattern exits 2 naming the key" "federation.self must match" \
'{"self":"Hub","peers":[]}'
expect_fed_refusal "federation peers not an array exits 2 naming the key" "federation.peers must be an array" \
'{"self":"hub","peers":{}}'
expect_fed_refusal "federation peer not an object exits 2 naming the key" "federation.peers[0] must be a JSON object" \
'{"self":"hub","peers":["factory"]}'
expect_fed_refusal "federation peer id pattern exits 2 naming the key" "federation.peers[0].id must match" \
'{"self":"hub","peers":[{"id":"Factory","origin":"https://mosaic.woltje.com","label":"F"}]}'
FED_LONG_LABEL="$(printf 'x%.0s' $(seq 81))"
expect_fed_refusal "federation peer label over 80 characters exits 2 naming the key" "federation.peers[0].label" \
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"'"$FED_LONG_LABEL"'"}]}'
expect_fed_refusal "federation duplicate peer id exits 2 naming the key" 'federation.peers[1].id "factory" is already named' \
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"F"},{"id":"factory","origin":"https://other.woltje.com","label":"G"}]}'
expect_fed_refusal "federation peer origin with a trailing slash exits 2 naming the key" "federation.peers[0].origin" \
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com/","label":"F"}]}'
# A missing value: the pattern test coerces undefined to "undefined", which matches, so the typeof half is what refuses.
expect_fed_refusal "federation self missing exits 2 naming the key" "federation.self must match" \
'{"peers":[]}'
expect_fed_refusal "federation peer id missing exits 2 naming the key" "federation.peers[0].id must match" \
'{"self":"hub","peers":[{"origin":"https://mosaic.woltje.com","label":"F"}]}'
expect_fed_refusal "federation peer label missing exits 2 naming the key" "federation.peers[0].label must be" \
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com"}]}'
# Without the key: no federation in the output, and the config still loads (FED R2 boundary).
cfg no-federation.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
NO_FED_OUT="$(MOSAIC_CONFIG="$SANDBOX/no-federation.json" timeout 20 $CONFIG_OP validate)"
NO_FED_RC=$?
[ "$NO_FED_RC" -eq 0 ] && ! printf '%s' "$NO_FED_OUT" | grep -q '"federation"'
check "config without federation loads and has no federation output" $?
# Without the key: the output is exactly what the loader printed before phase 1 (sage-1340, F2).
cfg no-console.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
EXPECTED="$(printf '{\n "configVersion": 1,\n "environment": "development",\n "dataRoot": "%s",\n "execution": {\n "backend": "docker",\n "provider": "zai",\n "model": "m",\n "adapter": "pi"\n }\n}' "$DATA_ROOT")"